Android 基于 Linux 内核,继承了 Linux 所有的 IPC 机制,同时引入了专用的 Binder 作为核心 IPC 框架。理解 Android 使用的各种 IPC 机制的差异、优劣和适用场景,是深入系统架构的必修课。
一、Android 中使用的 IPC 机制概览
| IPC 机制 | 是否主要 | 典型用途 | 数据拷贝次数 | 安全性 |
|---|---|---|---|---|
| Binder | 是 | 系统服务通信、四大组件调度 | 1 次 | 基于能力+UID 检查 |
| Unix Domain Socket | 是 | Zygote ↔ system_server, init, adbd | 2 次 | 基于 UID/GID |
| ashmem(匿名共享内存) | 是 | SurfaceFlinger, AudioFlinger | 0 次(映射后) | 基于 fd 传递 |
| 管道 (pipe) / 信号 (signal) | 辅助 | 进程同步、轻量通知 | 2 次 | 基于 fd |
| dma-buf / ION | 是 | 图形缓冲区、Camera | 0 次(硬件直接访问) | 基于 fd |
| SysV IPC / POSIX 消息队列 | 极少 | 无广泛使用 | 2 次 | 较弱 |
二、Binder——Android 的核心 IPC
2.1 为什么 Android 选择了 Binder?
Android 在设计之初评估了多种 IPC 方案,选择了 Binder 作为核心 IPC 而非传统 Linux IPC(如 Socket、管道、SysV)的原因:
1. 安全模型:Binder 天然支持基于能力的访问控制。每个 Binder 对象有唯一的标识(binder_node),不能伪造。配合 UID/PID 检查(IPCThreadState 在每次调用时自动传递调用者的 UID/PID),实现了细粒度的权限控制。相比之下,Socket/管道缺乏内置的身份传递机制。
2. 一次拷贝:通过 mmap 机制,Binder 将数据拷贝从两次减少为一次,提升了大数据量场景下的性能。
3. 对象引用计数:Binder 内置强/弱引用计数(通过 binder_node 的 internal_strong_refs / local_weak_refs 等字段),配合死亡通知机制,使得跨进程对象生命周期管理自动化。
4. 线程池管理:Binder 驱动管理服务端线程池(最多 15 个线程),自动根据负载请求创建或回收线程。
5. 同步语义(RPC 模型):Binder 天然支持同步调用(Client 阻塞等待 Server 回复),也支持异步的 oneway 调用。
2.2 Binder 通信流程回顾
Client 进程 Server 进程 |
用户空间库:frameworks/native/libs/binder/
2.3 Binder 驱动的内存映射(mmap)
Binder 驱动的一次拷贝能力来源于其巧妙的 mmap 设计。在 ProcessState 初始化时,会调用 open("/dev/binder") 然后执行 mmap 映射一块内核缓冲区:
// frameworks/native/libs/binder/ProcessState.cpp |
Binder 驱动的 mmap 实现:
// kernel/drivers/staging/android/binder.c (旧版,新版在 drivers/android/) |
关键设计:内核空间和用户空间映射到同一块物理内存。
- 用户空间通过 mmap 返回的虚拟地址访问。
- 内核空间通过
proc->buffer访问。 - 两者共享相同的物理页面,使 Binder 实现了一次拷贝。
数据拷贝的实际流程:
Client (用户空间) Binder 驱动 (内核) |
2.4 Binder 的身份验证机制
Binder 在每次跨进程通信中自动附加调用者的 UID 和 PID:
// frameworks/native/libs/binder/IPCThreadState.cpp |
这个机制使得服务端可以安全地进行权限检查:
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java |
2.5 Binder 的死亡通知机制
// 客户端注册死亡通知 |
三、Unix Domain Socket
3.1 在 Android 中的应用
Unix Domain Socket(UDS)是 Android 中第二重要的 IPC 机制,用于以下关键场景:
- init -> Zygote:init 进程通过 socket 向 Zygote 发送 fork 请求
- adbd:ADB 守护进程使用 socket 与 system_server 和应用通信
- installd:PKMS 通过 socket 与 installd 守护进程通信执行 dexopt、数据目录创建等
- netd:网络管理守护进程
- keystore:密钥管理守护进程
- lmkd:低内存守护进程通过 socket 与 AMS 通信
- tombstoned:崩溃 dump 采集守护进程
- vold:卷管理守护进程
3.2 Zygote 的 Socket 通信
// frameworks/base/core/java/android/os/ZygoteProcess.java |
Zygote 的 socket 初始化在 frameworks/base/core/java/com/android/internal/os/ZygoteInit.java:
// ZygoteInit.java |
3.3 installd 的 Socket 通信
// frameworks/base/services/core/java/com/android/server/pm/Installer.java |
installd 守护进程路径:frameworks/native/cmds/installd/
installd 的 socket 通信使用 Binder 风格的命令码 + Parcel 数据格式:
InstallerConnection ──── socket ────► installd 守护进程 |
3.4 Unix Domain Socket vs TCP Socket 对比
| 特性 | Unix Domain Socket | TCP Socket |
|---|---|---|
| 地址 | 文件系统路径 | IP:Port |
| 数据拷贝 | 2 次 | 2 次 + 网络协议栈 |
| 延迟 | ~200us | ~1ms (localhost) |
| 吞吐 | ~80MB/s | ~50MB/s (localhost) |
| 安全性 | 文件系统权限 + UID/GID | IP + Port,需要额外认证 |
| 作用域 | 单机 | 跨网络 |
| sendmsg/recvmsg 传 fd | 支持 (SCM_RIGHTS) | 不支持 |
| sendmsg/recvmsg 传凭证 | 支持 (SCM_CREDENTIALS) | 不支持 |
UDS 的两个关键优势:
- fd 传递:通过
SCM_RIGHTS控制消息可以在进程间传递文件描述符。这在 Android 中广泛使用(如传递 ashmem fd)。 - 对端凭证:通过
SO_PEERCRED可以获取对端进程的 UID/GID/PID,实现基本的身份验证。
3.5 Zygote 为什么用 Socket 而不是 Binder?
Zygote 的特殊性在于它 fork 子进程。fork 会复制父进程的文件描述符表。如果 Zygote 已经打开 /dev/binder 并通过 Binder 通信,那么 fork 出的每个子进程都继承了这些 Binder 相关的 fd 和 binder_proc 状态。
具体原因:
- fd 清理:Zygote fork 子进程后,子进程通过 exec 执行具体的 Android 应用。在 exec 之前,Zygote 会关闭所有不需要的 fd(通过
Zygote.closeDescriptors())。Socket 的 fd 可以简单地关闭,但 Binder 的 fd 涉及驱动状态(binder_proc、binder_thread、todo 队列),清理复杂。 - Binder 身份冲突:fork 后的子进程继承了 Zygote 的 Binder 身份(binder_proc),如果子进程也在同一 Binder 上下文中通信,会导致 identity 混乱。
- 通信模型简单:Zygote 只需要”请求-响应”模式(”请 fork 一个进程,参数是…”),不需要 Binder 的面向对象 RPC 能力。
- 启动时序:Zygote 在 init 阶段启动,此时 Binder 的 ServiceManager 可能尚未就绪。Socket 不依赖任何其他服务,可以在最早阶段初始化。
四、共享内存机制
4.1 ashmem(Android Shared Memory)
ashmem 是 Android 对 Linux 共享内存的扩展,增加了引用计数和 pin/unpin 机制:
// system/core/libcutils/ashmem-dev.cpp |
应用场景:
- SurfaceFlinger:应用通过
GraphicBuffer(底层使用 ashmem)将渲染的帧传递给 SurfaceFlinger。 - AudioFlinger:音频数据缓冲区。
- ContentProvider 的 Cursor window:通过 ashmem 在 Provider 进程和 Client 进程间共享查询结果。
4.1.1 ashmem 的 pin/unpin 机制详解
这是 ashmem 与标准 POSIX 共享内存最本质的区别。当系统内存紧张时,内核的 LMK 或内存回收机制可以回收 unpin 的 ashmem 页:
// drivers/staging/android/ashmem.c |
在 SurfaceFlinger 的 BufferQueue 中:
- 当一帧被渲染完成并传递给 SurfaceFlinger 后,该 buffer 被 unpin。
- 如果 SurfaceFlinger 还需要该 buffer(如作为下一帧的参考),它会被重新 pin。
- 当系统内存紧张时,unpin 的 buffer 可以被内核回收,释放物理内存。
- 如果没有 pin/unpin 机制,所有已分配的 GraphicBuffer 都会一直占用物理内存,即使它们已经不再被需要。
4.2 ION / dma-buf
ION 是 Android 为多媒体场景设计的内存分配器,dma-buf 是 Linux 标准的 DMA 缓冲区共享机制:
Camera HAL -> ION buffer -> Gralloc -> SurfaceFlinger |
Android 11+ 逐步用标准的 Linux dma-buf 取代 ION。AOSP 路径:drivers/dma-buf/
4.2.1 ION 与 dma-buf 的区别
ION 是 Android 特有的内存管理器(在 drivers/staging/android/ion/),它将内存按 heap 类型分类:
ION_HEAP_TYPE_SYSTEM:通用系统内存ION_HEAP_TYPE_SYSTEM_CONTIG:连续物理内存ION_HEAP_TYPE_CARVEOUT:预留的专用内存ION_HEAP_TYPE_DMA:DMA 可访问内存
dma-buf 是 Linux 内核标准框架(3.3+),用于在不同设备驱动之间共享 DMA 缓冲区。Android 的迁移路径是将 ION 的 heap 功能逐步迁移到标准的 dma-buf heaps 框架。
4.3 共享内存协同 Binder 的优势
共享内存的典型使用模式是:
- 使用 ashmem 创建共享内存,获取 fd
- 通过 Binder 将 fd 传递给对端进程
- 对端进程通过 fd 做 mmap,直接访问同一块物理内存
- 数据读写不再经过 Binder 通道,零拷贝
这种模式结合了 Binder 的安全性和共享内存的高效性。
具体示例(ContentProvider 的 Cursor window):
// frameworks/base/core/jni/android_database_CursorWindow.cpp |
五、管道与信号(辅助 IPC)
5.1 管道(pipe)
管道在 Android 中的核心应用:
Looper 的唤醒机制:
// system/core/libutils/Looper.cpp |
Zygote 的子进程同步:USAP(Unspecialized App Process)Pool 使用 pipe 来管理预 fork 的进程池,通过 pipe 的阻塞读实现进程的同步等待。
Looper 为什么用 eventfd 而非 pipe?
- eventfd 只需要一个 fd(管道需要两个)
- eventfd 更轻量(内核中只有 16 字节的结构体,管道需要至少一页的环形缓冲区)
- eventfd 语义更清晰(计数器型,而非流型)
5.2 信号(Signal)
Android 中信号的使用场景:
- SIGCHLD:Zygote 监控子进程退出。当应用进程异常退出时,Zygote 收到 SIGCHLD,清理子进程资源。
- SIGQUIT(kill -3):ANR 时触发线程 dump。AMS 检测到 ANR 后,向目标进程发送 SIGQUIT,ART 的信号处理器捕获后打印所有线程的堆栈到
/data/anr/traces.txt。 - SIGSTOP / SIGCONT:进程的暂停和恢复。AMS 可以使用这些信号来暂停后台进程(Android 实际上不使用 SIGSTOP,而是使用 cgroup 的 freezer)。
- SIGPIPE:管道/Socket 对端关闭时的默认行为。Android 应用通常忽略 SIGPIPE,改用 EPIPE 错误处理。
- SIGUSR1:用于 ART GC 的某些场景,如 hprof 堆转储触发。
// frameworks/base/core/jni/android_os_Process.cpp |
六、System V IPC 与 POSIX IPC 在 Android 中的状态
Android 基本上不使用 SysV IPC(shmget、msgget、semget)和 POSIX 消息队列(mq_open)。原因:
安全模型不匹配:SysV IPC 使用全局 key(
IPC_PRIVATE或ftok()生成的 key),容易冲突和被恶意猜测。虽然 Android 通过CONFIG_SYSVIPC内核选项默认禁用 SysV IPC。生命周期管理困难:SysV IPC 对象是全局的、持久的(需要通过
ipcrm或程序显式删除),进程崩溃后可能残留。Binder 有自动引用计数和死亡通知。缺乏身份传递:SysV IPC 不传递调用者 UID/PID,服务端无法做权限检查。
资源限制:SysV IPC 有全局数量限制(
/proc/sys/kernel/shmmni、/proc/sys/kernel/msgmni),在大规模 Android 系统上可能不够。
Android 内核编译配置中 CONFIG_SYSVIPC 通常是关闭的,这意味着 shmget() / msgget() / semget() 调用会失败。
七、各 IPC 机制的性能对比
7.1 基准对比
| IPC 机制 | 延迟 (小数据) | 吞吐 (大数据) | 内存开销 | 安全模型 |
|---|---|---|---|---|
| Binder | ~100us | ~50MB/s | 低(一次拷贝) | 能力+UID |
| Unix Domain Socket | ~200us | ~80MB/s | 高(两次拷贝) | UID/GID |
| ashmem (mmap) | 初始 ~1ms | ~无限(物理内存带宽) | 最低 | fd 传递 |
| SysV 消息队列 | ~150us | ~10MB/s | 中 | 弱 |
| pipe | ~50us | ~200MB/s (pipe buffer) | 低 | fd 传递 |
延迟数据仅供参考(取决于内核版本和硬件)。Binder 的综合性能在小到中等数据量场景(大多数系统服务调用)下最优,这也是 Android 选择它的重要原因。
7.2 Binder 的性能瓶颈与优化
Binder 的事务大小限制为 1MB - 8KB(约 1016KB),超过此大小会导致 FAILED_TRANSACTION 错误。这是因为 Binder 驱动为每个进程分配的 mmap 缓冲区大小为 (1MB - 8KB)。
// frameworks/native/libs/binder/ProcessState.cpp |
对于大数据传输场景,应该使用 Binder + ashmem 的组合:
- Binder 传递控制命令和 ashmem fd
- ashmem 承载实际数据
八、进程间通信的 SELinux 安全控制
Android 使用 SELinux 对所有 IPC 机制施加安全限制:
# system/sepolicy/public/ |
SELinux 在 Binder 上的特殊规则:
binder { call }:允许向某进程发起 Binder 调用binder { transfer }:允许将一个 Binder 对象引用传递给另一个进程binder { impersonate }:允许以另一个进程的身份执行 Binder 调用
这些 SELinux 策略在 system/sepolicy/ 中定义,通过 MAC(Mandatory Access Control)机制确保即使应用获得了 Binder 引用,也必须满足 SELinux 策略才能成功通信。
九、核心面试题
Q1:为什么 Android 不用传统 Linux 的 SysV / POSIX IPC,而要单独设计 Binder?
主要有三点:(1) 安全:Binder 自动传递调用者 UID/PID,服务端可以通过 Binder.getCallingUid() 做权限检查;SysV IPC 依赖全局 key,容易冲突也容易被恶意进程猜测和利用。(2) 性能:Binder 的一次拷贝优于多数传统 IPC 的两次拷贝。(3) 生命周期管理:Binder 通过引用计数和死亡通知自动管理跨进程对象生命周期,传统 IPC 需要额外机制实现。
Q2:Zygote 为什么使用 Socket 而不是 Binder 来通信?
Zygote 的特殊性在于它 fork 子进程。fork 会复制父进程的文件描述符表。如果 Zygote 已经打开 /dev/binder 并通过 Binder 通信,那么 fork 出的每个子进程都继承了这些 Binder 相关的 fd 和 binder_proc 状态。这会导致混乱——子进程继承了 Zygote 的 Binder 身份。实际上,在 Zygote fork 和 exec 新进程之前,会关闭大部分不需要的 fd。Socket 在此场景下更简单:对端明确(只有 system_server 中的 ZygoteProcess),通信模型简单(发送 fork 请求,等待返回 PID),不需要复杂的能力模型。
Q3:ashmem 的 pin/unpin 机制有什么作用?和普通的 POSIX shm(如 shmget/shm_open)有什么区别?
ashmem 的 pin/unpin 是 Android 特有的内存管理功能。当系统内存紧张时,内核可以”回收”unpin 的 ashmem 页面(将其内容丢弃),这在 SurfaceFlinger 的 BufferQueue 中非常有用——当 Buffer 已经被消费且不再需要时,可以 unpin 让内核回收内存。POSIX 标准 shm 缺乏这种 fine-grained 控制,分配后会一直占用物理内存直到进程退出或显式释放。
其他区别:
- ashmem 通过引用计数确定何时真正释放物理内存(当所有 mmap 和 fd 引用都关闭时才释放)。
- ashmem 的内存可以在 pin 和 unpin 之间动态切换,而 POSIX shm 是静态的。
- ashmem 支持更大的区域(受限于虚拟内存),而 System V shm 受系统全局限制。
Q4:Binder 的 mmap 缓冲区为什么是 1MB - 8KB?如果超过这个限制怎么办?
Binder 驱动为每个进程分配一个固定的 mmap 缓冲区(默认约 1016KB)。这个值是在 ProcessState 初始化时硬编码的。原因:
- Binder 最初设计用于系统服务调用,传输的数据量通常很小(几十到几百字节)。
- 内核空间不能无限分配内存给用户空间。
- 1MB 是足够的大小,同时避免单个进程占用过多内核资源。
超过限制的处理方案:
- 减少单次传输的数据量(分批处理)。
- 使用 Binder + ashmem 组合:Binder 传递 ashmem fd,实际数据通过 ashmem 共享。
- 对于大文件/大块数据,使用 ContentProvider 的 openFile 接口(底层通过 pipe 或 fd 传递)。
- 在设备出厂时,可以通过修改 BINDER_VM_SIZE 宏来增大缓冲区(需要重新编译 Binder 驱动和 libbinder)。
Q5:Unix Domain Socket 如何实现文件描述符传递?在 Android 中哪些场景用到了这个特性?
Unix Domain Socket 通过 SCM_RIGHTS 辅助消息实现 fd 传递:
// 发送端:将 fd 附着在消息上发送 |
在 Android 中的应用场景:
- Zygote -> App:Zygote 通过 socket 将应用的相关 fd 传递给刚刚 fork 出的子进程。
- installd:installd 守护进程创建数据目录后,可以通过 socket 将目录 fd 传回 system_server。
- adb:ADB 在 host 和 device 之间传递文件时使用 socket 对传递 fd。
核心:fd 传递是零拷贝的——接收方拿到的 fd 指向同一内核文件描述符,不需要再复制数据。
AOSP 核心路径参考:
frameworks/native/libs/binder/— Binder 用户空间库drivers/android/binder.c— Binder 驱动(Android 10+ 新版路径)system/core/libcutils/ashmem-dev.cpp— ashmem 封装frameworks/base/core/java/android/os/ZygoteProcess.java— Zygote Socket 通信frameworks/native/cmds/installd/— installd 守护进程system/core/adb/— ADB 守护进程system/core/libutils/Looper.cpp— Looper 唤醒(eventfd)frameworks/base/core/jni/android_database_CursorWindow.cpp— ashmem 在 ContentProvider 中的使用drivers/staging/android/ion/— ION 内存分配器(逐步废弃)drivers/dma-buf/— dma-buf 框架




