Skip to content

Linux IPC选择边界

从真实内核路径比较 pipe、Unix socket、共享内存和 Binder 的数据所有权、消息边界、身份、同步与失败清理。

基于android-17.0.0_r1
AndroidBinderIPCLinux源码阅读

Linux IPC选择边界 ​

“哪种 IPC 最快”通常是一个没有边界的问题。管道、Unix socket、共享内存和 Binder 的数据结构、 唤醒方式、生命周期和失败返回值都不同;即使传输相同字节数,完成判据也可能不同。

本文面向已经读过 Binder调用源码地图,并理解文件描述符、虚拟内存和阻塞 I/O 基本概念的读者。 只比较四条可以从固定源码验证的路径:pipe 的环形页、Unix socket 的 sk_buff、SharedMemory 的 fd/mmap,以及 Binder 的目标进程 buffer。本文不提供通用吞吐排名,也不把“零拷贝” “一次拷贝”当作所有路径的固定承诺。

1. 比较方法 ​

五个问题 ​

比较前先回答:

  1. 数据由谁保存,接收方何时取得?
  2. 一次写入对应消息记录还是字节流?
  3. 身份、fd 或远程对象如何传递?
  4. 空读、满写、对端退出分别返回什么?
  5. 发送成功是入队、交付还是业务完成?
机制数据 owner消费入口背压最终清理
pipepipe_buffer 页read环形区满最后文件引用
Unix socketsk_buffrecvmsgsocket 队列close 与 skb destructor
SharedMemoryfd 后备页mmap 读取协议自定fd 和映射
Bindertarget binder_bufferBR_TRANSACTIONbuffer/事务队列事务清理

2. 管道路径 ​

环形页 ​

源码文件:kernel/common/fs/pipe.c

相关函数:alloc_pipe_info()

c
struct pipe_inode_info *alloc_pipe_info(void)
{
    struct pipe_inode_info *pipe;
    unsigned long pipe_bufs = PIPE_DEF_BUFFERS;

    pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
    if (pipe == NULL)
        goto out_free_uid;

    pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
                         GFP_KERNEL_ACCOUNT);
    if (pipe->bufs) {
        init_waitqueue_head(&pipe->rd_wait);
        init_waitqueue_head(&pipe->wr_wait);
        pipe->max_usage = pipe_bufs;
        pipe->ring_size = pipe_bufs;
        mutex_init(&pipe->mutex);
        return pipe;
    }
    return NULL;
}

pipe_inode_info 保存 head/tail、pipe_buffer 数组以及读写等待队列。生产者和消费者共享这个 内核对象,但不会共享彼此用户空间的地址。

写入路径 ​

源码文件:kernel/common/fs/pipe.c

相关函数:anon_pipe_write()

c
page = anon_pipe_get_page(pipe);
if (unlikely(!page)) {
    if (!ret)
        ret = -ENOMEM;
    break;
}

copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {
    anon_pipe_put_page(pipe, page);
    if (!ret)
        ret = -EFAULT;
    break;
}

pipe->head = head + 1;
buf = pipe_buf(pipe, head);
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = copied;

默认写路径把用户迭代器复制到 pipe 页,再挂入环形数组。小写入可能合并到最后一个 buffer; 满写会等待 wr_wait,非阻塞满写返回 -EAGAIN。若读端已经全部关闭,写端收到 SIGPIPE/EPIPE。

读取路径 ​

源码文件:kernel/common/fs/pipe.c

相关函数:anon_pipe_read()

c
if (!pipe_empty(head, tail)) {
    struct pipe_buffer *buf = pipe_buf(pipe, tail);
    size_t chars = min_t(size_t, buf->len, total_len);

    written = copy_page_to_iter(buf->page, buf->offset, chars, to);
    if (unlikely(written < chars)) {
        if (!ret)
            ret = -EFAULT;
        break;
    }
    ret += chars;
    buf->offset += chars;
    buf->len -= chars;
    if (!buf->len)
        tail = pipe_update_tail(pipe, buf, tail);
}

接收方再从 pipe 页复制到自己的迭代器。因此 pipe 默认路径表现为写入和读取两侧各自复制, 但 splice、页复用和不同调用方式会改变细节,不能把“两次拷贝”写成所有 pipe 操作的定律。

边界清理 ​

源码文件:kernel/common/fs/pipe.c

c
if (!pipe->writers)
    break;
if ((filp->f_flags & O_NONBLOCK) ||
        (iocb->ki_flags & IOCB_NOWAIT)) {
    ret = -EAGAIN;
    break;
}

pipe 是字节流:一次 write 不保证对应一次 read。没有写端且 buffer 已空时读到 EOF;空但仍有 写端时阻塞或返回 EAGAIN。调用方必须自己设计 framing、版本和请求/回复关联。

3. Unix套接字 ​

skb载体 ​

源码文件:kernel/common/net/unix/af_unix.c

相关函数:unix_dgram_sendmsg()

c
skb = sock_alloc_send_pskb(sk, len - data_len, data_len,
        msg->msg_flags & MSG_DONTWAIT, &err,
        PAGE_ALLOC_COSTLY_ORDER);
if (!skb)
    goto out;

err = unix_scm_to_skb(&scm, skb, true);
if (err < 0)
    goto out_free;

skb_put(skb, len - data_len);
skb->data_len = data_len;
skb->len = len;
err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len);
if (err)
    goto out_free;

skb_queue_tail(&other->sk_receive_queue, skb);

Unix socket 把数据和控制信息放进 sk_buff,再进入对端 sk_receive_queue。datagram 保留一次 发送的记录边界;stream 可能把一段用户数据拆成多个 skb,仍需应用层 framing。

凭据与fd ​

源码文件:kernel/common/net/unix/af_unix.c

相关函数:unix_scm_to_skb()、unix_maybe_add_creds()

c
static int unix_scm_to_skb(struct scm_cookie *scm, struct sk_buff *skb,
        bool send_fds)
{
    UNIXCB(skb).pid = get_pid(scm->pid);
    UNIXCB(skb).uid = scm->creds.uid;
    UNIXCB(skb).gid = scm->creds.gid;
    UNIXCB(skb).fp = NULL;
    unix_get_secdata(scm, skb);
    if (scm->fp && send_fds)
        return unix_attach_fds(scm, skb);
    skb->destructor = unix_destruct_scm;
    return 0;
}

凭据和 SCM_RIGHTS fd 属于 skb 控制状态。接收方必须显式读取、验证并关闭 fd;skb 销毁时 unix_destruct_scm 会释放未移交的文件引用。Unix socket 能承载身份和 fd,不等于自动拥有 服务发现、方法 code 或权限检查。

连接清理 ​

源码文件:kernel/common/net/unix/af_unix.c

c
static int unix_release(struct socket *sock)
{
    struct sock *sk = sock->sk;
    if (!sk)
        return 0;
    sk->sk_prot->close(sk, 0);
    unix_release_sock(sk, 0);
    sock->sk = NULL;
    return 0;
}

close 会清空接收队列并解除对端关系。stream 发送成功通常表示字节进入 socket 路径,不表示 接收进程已解析请求;连接断开可能表现为 EPIPE、ECONNRESET 或 EOF。

4. 共享内存 ​

fd来源 ​

源码文件:frameworks/base/core/java/android/os/SharedMemory.java

相关函数:create()、fromFileDescriptor()

java
public static @NonNull SharedMemory create(@Nullable String name, int size)
        throws ErrnoException {
    if (size <= 0) {
        throw new IllegalArgumentException("Size must be greater than zero");
    }
    return new SharedMemory(nCreate(name, size));
}

public static @NonNull SharedMemory fromFileDescriptor(
        @NonNull ParcelFileDescriptor fd) {
    FileDescriptor f = new FileDescriptor();
    f.setInt$(fd.detachFd());
    return new SharedMemory(f);
}

SharedMemory 跨进程交付的是 fd。fd 可以由 Binder 或 Unix socket 传递,共享内存本身不负责 告诉对方数据何时完成。

源码文件:frameworks/base/core/jni/android_os_SharedMemory.cpp

cpp
jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) {
    const char* name = jname
            ? env->GetStringUTFChars(jname, nullptr) : nullptr;
    int fd = ashmem_create_region(name, size);
    int err = fd < 0 ? errno : 0;
    if (name)
        env->ReleaseStringUTFChars(jname, name);
    if (fd < 0) {
        jniThrowErrnoException(env, "SharedMemory_create", err);
        return nullptr;
    }
    return jniCreateFileDescriptor(env, fd);
}

源码文件:system/core/libcutils/ashmem-dev.cpp

cpp
int ashmem_create_region(const char* name, size_t size) {
    if (name == NULL)
        name = "none";
    auto create_region = use_memfd()
            ? __memfd_create_region
            : __ashmem_create_region;
    return create_region(name, size);
}

Android 17 根据运行条件选择 memfd 或 ashmem,不能把 SharedMemory 固定描述成单一内核实现。

映射保护 ​

源码文件:frameworks/base/core/java/android/os/SharedMemory.java

java
public boolean setProtect(int prot) {
    checkOpen();
    validateProt(prot);
    return nSetProt(mFileDescriptor, prot) == 0;
}

public @NonNull ByteBuffer map(int prot, int offset, int length)
        throws ErrnoException {
    checkOpen();
    validateProt(prot);
    long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED,
            mFileDescriptor, offset);
    return new DirectByteBuffer(length, address, mFileDescriptor,
            new Unmapper(address, length, mMemoryRegistration.acquire()),
            (prot & OsConstants.PROT_WRITE) == 0);
}

共享内存让双方映射同一后备页,但不提供生产/消费同步、消息边界或完成确认。setProtect 只收紧之后创建的映射;已有可写映射保留原权限。

源码文件:frameworks/base/core/java/android/os/SharedMemory.java

java
public void close() {
    mFileDescriptor.setInt$(-1);
    if (mCleaner != null) {
        mCleaner.clean();
        mCleaner = null;
    }
}

关闭当前 fd 不会立即使已有 mmap 失效。所有 fd 和映射都释放后,后备内存才可回收。

5. Binder路径 ​

目标缓冲 ​

源码文件:kernel/common/drivers/android/binder_alloc.c

相关函数:binder_alloc_new_buf()、binder_alloc_copy_user_to_buffer()、binder_alloc_free_buf()

c
struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
        size_t data_size, size_t offsets_size,
        size_t extra_buffers_size, int is_async)
{
    struct binder_buffer *buffer, *next;
    size_t size = sanitized_size(data_size, offsets_size,
            extra_buffers_size);
    int ret;
    if (!binder_alloc_is_mapped(alloc))
        return ERR_PTR(-ESRCH);
    if (!size)
        return ERR_PTR(-EINVAL);
    next = kzalloc(sizeof(*next), GFP_KERNEL);
    if (!next)
        return ERR_PTR(-ENOMEM);

    mutex_lock(&alloc->mutex);
    buffer = binder_alloc_new_buf_locked(
            alloc, next, size, is_async);
    if (IS_ERR(buffer)) {
        mutex_unlock(&alloc->mutex);
        goto out;
    }
    buffer->data_size = data_size;
    buffer->offsets_size = offsets_size;
    buffer->extra_buffers_size = extra_buffers_size;
    mutex_unlock(&alloc->mutex);

    ret = binder_install_buffer_pages(alloc, buffer, size);
    if (ret) {
        binder_alloc_free_buf(alloc, buffer);
        buffer = ERR_PTR(ret);
    }
out:
    return buffer;
}

分配函数先验证目标进程的映射状态和总大小,在 alloc 锁内选取空闲区,再安装实际页面。只有 目标 buffer 建立成功,事务路径才会把发送方数据复制进去。

c
unsigned long binder_alloc_copy_user_to_buffer(
        struct binder_alloc *alloc, struct binder_buffer *buffer,
        binder_size_t buffer_offset, const void __user *from,
        size_t bytes)
{
    while (bytes) {
        struct page *page = binder_alloc_get_page(
                alloc, buffer, buffer_offset, &pgoff);
        size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
        kptr = kmap_local_page(page) + pgoff;
        ret = copy_from_user(kptr, from, size);
        kunmap_local(kptr);
        if (ret)
            return bytes - size + ret;
        bytes -= size;
        from += size;
        buffer_offset += size;
    }
    return 0;
}

Binder 的 buffer 属于接收进程的 binder_alloc,并映射到该进程的用户地址空间。发送方数据 复制到目标 buffer 后,服务线程可以建立 Parcel 视图;这不是 SharedMemory 那种双方长期 共同维护的 payload 区域,而是一次事务拥有的 buffer。

命令状态 ​

源码文件:frameworks/native/libs/binder/IPCThreadState.cpp

相关函数:writeTransactionData()、talkWithDriver()

cpp
status_t IPCThreadState::writeTransactionData(int32_t cmd,
        uint32_t binderFlags, int32_t handle, uint32_t code,
        const Parcel& data, status_t* statusBuffer) {
    binder_transaction_data tr;
    tr.target.handle = handle;
    tr.code = code;
    tr.flags = binderFlags;
    tr.data_size = data.ipcDataSize();
    tr.data.ptr.buffer = data.ipcData();
    tr.offsets_size = data.ipcObjectsCount() * sizeof(binder_size_t);
    tr.data.ptr.offsets = data.ipcObjects();
    mOut.writeInt32(cmd);
    mOut.write(&tr, sizeof(tr));
    return NO_ERROR;
}

status_t IPCThreadState::talkWithDriver(bool doReceive) {
    binder_write_read bwr;
    const bool needRead = mIn.dataPosition() >= mIn.dataSize();
    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;
    bwr.write_size = outAvail;
    bwr.write_buffer = reinterpret_cast<uintptr_t>(mOut.data());
    bwr.read_size = doReceive && needRead ? mIn.dataCapacity() : 0;
    bwr.read_buffer = doReceive && needRead
            ? reinterpret_cast<uintptr_t>(mIn.data()) : 0;
    status_t err;
    do {
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
    } while (err == -EINTR);
    return err;
}

Binder 还传递 handle、事务 code、flags 和对象偏移,驱动会做目标解析、对象转换、fd 修复、 服务线程选择和 reply 栈管理。因此它提供 RPC 结构,也承担更多失败分支。

事务释放 ​

源码文件:kernel/common/drivers/android/binder.c

相关函数:binder_transaction()、binder_alloc_free_buf()

c
t->buffer = binder_alloc_new_buf(&target_proc->alloc,
        tr->data_size, tr->offsets_size, extra_buffers_size,
        !reply && (t->flags & TF_ONE_WAY));
if (IS_ERR(t->buffer)) {
    return_error = PTR_ERR(t->buffer) == -ESRCH
            ? BR_DEAD_REPLY : BR_FAILED_REPLY;
    goto err_binder_alloc_buf_failed;
}
t->buffer->target_node = target_node;

目标 buffer 分配失败时,服务端不会进入 onTransact();目标进程死亡、地址空间清空、空间不足 或非法数据都会改变结果。事务完成后,驱动沿 buffer 释放路径回收目标内存。

6. 语义对照 ​

数据边界 ​

机制默认边界调用方必须补充
pipe字节流长度、消息分隔、版本
Unix stream字节流framing、请求/回复协议
Unix datagram/seqpacket消息记录方法语义、超时、重试
SharedMemory映射区域生产/消费状态、内存序、锁
Binder事务与 ParcelAIDL 接口、权限、业务超时

身份对象 ​

pipe 主要表达文件读写权限和端点关闭;Unix socket 可以把 pid/uid/gid 和 SCM_RIGHTS 附着在 skb;SharedMemory 交付 fd 和映射能力,访问控制由 fd、mmap 保护和 SELinux 共同决定;Binder 还管理 target node、handle、calling uid、事务 code 和对象引用。

“支持身份验证”不能简单写成“机制自动安全”:Unix 凭据必须被请求方启用和读取,SharedMemory 拿到可写 fd 后也要遵守自己的保护协议,Binder 服务仍需在 Stub 或服务实现中检查权限。

阻塞背压 ​

现象pipeUnix socketSharedMemoryBinder
空读阻塞/-EAGAIN/EOF阻塞/-EAGAIN/EOF没有内置读等待服务线程等待事务
满写阻塞/-EAGAINsndbuf/接收队列满协议自定义target buffer/事务限制
对端退出SIGPIPE/EPIPE、EOFEPIPE/ECONNRESET 等fd/mmap 仍需清理DEAD_OBJECT/BR_DEAD_REPLY

发送成功在 pipe/socket 中通常只表示进入内核队列,在 Binder 中还可能只表示命令被驱动接受; 业务完成必须使用 reply、回调或共享协议的确认。

清理责任 ​

7. 选择路径 ​

管道适用 ​

pipe 适合端点关系明确、数据是字节流、无需复杂身份或对象传递、关闭和 EOF 语义足够的场景。 它不适合系统服务式的方法号、远程对象和结构化 reply。

套接字适用 ​

Unix socket 适合独立连接、地址发现、凭据或 fd 传递的协议。stream 仍需 framing;datagram/ seqpacket 仍需版本、错误码、超时和重试。若服务需要 Binder handle 和统一服务注册,socket 还 要额外的注册层。

共享内存适用 ​

SharedMemory 适合大块、重复读取或长期共同访问的 payload。它不是通知机制,通常需要另一个 pipe、socket、Binder 或 futex 传递 fd 和同步事件。

Binder适用 ​

Binder 适合系统服务式请求:需要服务注册、远程对象、方法 code、Parcel/FD/Binder 对象、调用方 身份和同步 reply。代价是驱动 proc/thread/node/ref、事务 buffer、线程等待和失败清理。

8. 测试输入 ​

共享内存测试 ​

源码文件:frameworks/base/core/tests/coretests/src/android/os/SharedMemoryTest.java

相关测试:testIsRegionReadOnly()、testIsRegionReadOnlyOnClosedFd()

测试创建 1024 字节 SharedMemory,先设置 PROT_READ,再关闭 fd;另一个测试显式关闭底层 fd 后验证 Java 对象调用会报告已关闭状态。它能证明保护收紧和 fd 生命周期,不能证明已有 mmap 同步变只读,也不能证明跨进程通知协议。

Binder测试 ​

源码文件:frameworks/native/libs/binder/tests/binderLibTest.cpp

相关测试:BinderLibTest.NopTransaction()、BinderLibTest.NopTransactionOneway()、 BinderLibTest.OnewayQueueing()、BinderLibTest.LimitExceededVectorSent()、BinderLibTest.BufRejected()

cpp
TEST_F(BinderLibTest, LimitExceededVectorSent) {
    sp<IBinder> server = addServer();
    const std::vector<uint64_t> testValue(
            kSizeBytesOverFull / sizeof(uint64_t), 42);
    Parcel data, reply;
    data.writeUint64Vector(testValue);
    EXPECT_THAT(server->transact(
            BINDER_LIB_TEST_ECHO_VECTOR, data, &reply),
            StatusEq(FAILED_TRANSACTION));
}

这些测试分别输入同步/oneway 事务、单线程服务上的 oneway 排队、大 payload 和非法 binder_buffer_object,断言事务状态、回调顺序或失败码。它们不能推导所有设备的通用上限或 性能排名。

读写实验 ​

管道实验应覆盖写端关闭后的 EOF、读端关闭后的 SIGPIPE/EPIPE 和非阻塞空读的 EAGAIN;Unix socket 实验应覆盖 stream 半包、datagram 记录边界和 fd 控制消息;SharedMemory 实验应覆盖 保护位与映射生命周期。每个实验都必须把入队、对端读取和业务完成分开计时。

9. 反查问题 ​

消息边界 ​

若请求要求“一次发送对应一条完整记录”,pipe 和 Unix stream 需要 framing;SharedMemory 需要长度、序号和完成标志;Binder 的 Parcel 有事务边界,但 AIDL 仍需定义版本和错误语义。

大数据 ​

若 payload 很大,先区分“传输一次”还是“长期共享”:Binder 目标 buffer 需要分配和释放, SharedMemory 传 fd 后可长期映射;Unix socket 可能使用页片段或 splice,但仍有 socket 队列语义。 不要只凭 one-copy 决定方案。

未处理 ​

pipe/socket 检查接收队列、对端读取和 framing;SharedMemory 检查通知、内存序和消费标志; Binder 检查目标 node、服务线程读循环、BR_TRANSACTION、目标 buffer 和服务端 onTransact。

性能实验 ​

必须固定 payload、生产者/消费者线程、阻塞模式、fd 生命周期、消息边界和完成判据。pipe/socket 的写返回不能当服务处理完成;SharedMemory 的写完成也不等于消费者已读取;Binder 必须以同步 reply 或服务端回调作为完成事件。

10. 阅读边界 ​

本文基于 kernel/common 的 pipe、Unix socket、shmem 和 Binder 实现,以及 framework 的 SharedMemory API,证明的是数据 owner、边界、等待、身份和清理差异。没有展开网络协议、futex 细节、Binder 引用计数、allocator 碎片、死亡通知、SELinux 策略、特定服务超时或固定设备性能排名。

下一步可回到 Binder调用源码地图,再进入驱动打开、buffer 分配和引用管理专题。读者也可以选一条 真实调用,写出“payload owner、消息边界、阻塞条件、失败码、清理 owner”五列,再用本文对应源码逐列核对。

11. 证据矩阵 ​

为了避免把不同机制的“发送成功”混为一谈,比较时固定记录五个事件:生产者写入、载体入队、消费者取出、业务完成、载体释放。pipe/socket 的写返回通常只覆盖前两个事件;SharedMemory 的 mmap 写入甚至没有入队事件;Binder 的同步 reply 才能作为一次方法调用的业务完成信号。这个矩阵是选择方法,不是吞吐排名。

每个实验还要固定 payload 大小、生产者/消费者线程、是否允许 fd、阻塞模式和清理动作,否则测到的是实验装置差异而不是 IPC 机制差异。