Linux IPC选择边界
“哪种 IPC 最快”通常是一个没有边界的问题。管道、Unix socket、共享内存和 Binder 的数据结构、 唤醒方式、生命周期和失败返回值都不同;即使传输相同字节数,完成判据也可能不同。
本文面向已经读过 Binder调用源码地图,并理解文件描述符、虚拟内存和阻塞 I/O 基本概念的读者。 只比较四条可以从固定源码验证的路径:pipe 的环形页、Unix socket 的 sk_buff、SharedMemory 的 fd/mmap,以及 Binder 的目标进程 buffer。本文不提供通用吞吐排名,也不把“零拷贝” “一次拷贝”当作所有路径的固定承诺。
1. 比较方法
五个问题
比较前先回答:
- 数据由谁保存,接收方何时取得?
- 一次写入对应消息记录还是字节流?
- 身份、fd 或远程对象如何传递?
- 空读、满写、对端退出分别返回什么?
- 发送成功是入队、交付还是业务完成?
| 机制 | 数据 owner | 消费入口 | 背压 | 最终清理 |
|---|---|---|---|---|
| pipe | pipe_buffer 页 | read | 环形区满 | 最后文件引用 |
| Unix socket | sk_buff | recvmsg | socket 队列 | close 与 skb destructor |
| SharedMemory | fd 后备页 | mmap 读取 | 协议自定 | fd 和映射 |
| Binder | target binder_buffer | BR_TRANSACTION | buffer/事务队列 | 事务清理 |
2. 管道路径
环形页
源码文件:kernel/common/fs/pipe.c
相关函数:alloc_pipe_info()
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()
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()
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
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()
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()
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
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()
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
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
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
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
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()
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 建立成功,事务路径才会把发送方数据复制进去。
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()
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()
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 | 事务与 Parcel | AIDL 接口、权限、业务超时 |
身份对象
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 或服务实现中检查权限。
阻塞背压
| 现象 | pipe | Unix socket | SharedMemory | Binder |
|---|---|---|---|---|
| 空读 | 阻塞/-EAGAIN/EOF | 阻塞/-EAGAIN/EOF | 没有内置读等待 | 服务线程等待事务 |
| 满写 | 阻塞/-EAGAIN | sndbuf/接收队列满 | 协议自定义 | target buffer/事务限制 |
| 对端退出 | SIGPIPE/EPIPE、EOF | EPIPE/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()
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 机制差异。
