文章内容
一、Linux 有 IPC,为什么 Android 还要再造 Binder
1.1 Linux 的 IPC 全家桶

Android 跑在 Linux 内核上,而 Linux 早就备齐了 IPC(Inter-Process Communication,进程间通信)手段:管道、Socket、信号、消息队列、共享内存,每一种都有几十年的工程验证。谷歌却在这之外单独造了 Binder,并把它做成 Android 跨进程调用的主通道。
这个选择在面试里几乎必被追问,因为它牵扯到性能、易用性和安全模型。
二、进程为什么不能直接通信
2.1 内存不共享
同一个进程里的对象随便传,因为它们活在同一片地址空间,谁都能拿到彼此的引用。换成两个进程,进程 A 想 new 一个属于进程 B 的对象再调它的方法,做不到——那个对象不在 A 的内存里,A 手上的地址在 B 看来毫无意义。
隔离是操作系统刻意设计的。一个应用崩溃不牵连其他应用,靠的正是这道墙。
2.2 用户空间与内核空间

墙不止一道。单个进程的地址空间还被切成用户空间和内核空间,应用代码只能在用户空间里跑。这样即使程序把自己的内存写乱,也伤不到内核和整个系统。
两边打交道要走系统调用。数据从用户空间进内核,用 copy_from_user;从内核回到用户空间,用 copy_to_user。这两个函数在内核态执行,用户程序碰不到,能做的只是发起一次系统调用请求,放不放行由内核判断:
// 两个函数都只在内核态可用,用户程序无法直接调用,它们是内核搬运数据的唯一合法通道unsigned long copy_from_user(void *to, // 目的地:内核空间里的缓冲区 const void __user *from,// 来源:用户空间地址,__user 标记提醒不可直接解引用 unsigned long n); // 字节数;返回值是未拷贝成功的字节数,0 才代表全部成功 unsigned long copy_to_user(void __user *to, // 目的地:用户空间地址 const void *from, // 来源:内核空间缓冲区 unsigned long n); // 字节数;返回值含义同上返回值的设计透露了这层关系:内核不信任用户空间给的地址,拷贝随时可能失败,所以要把没搬完的字节数报回来,而不是直接崩掉。
2.3 虚拟内存:内核空间是共享的
上面说的用户空间、内核空间都是虚拟内存,即进程看到的虚拟地址。虚拟地址到物理地址的转换由 MMU(Memory Management Unit,内存管理单元)这块硬件完成,页表由内核维护。
有一条结论对理解 Binder 很关键:每个进程的用户空间映射各自独立的物理页,但所有进程的内核空间映射的是同一块物理内存。内核只有一份,它在每个进程的地址空间里都有映射——这意味着内核空间天然是一片公共区域,可以作为跨进程数据的中转站。
三、传统 IPC 的两次拷贝

3.1 通信路径
有了公共区域,传统 IPC 的路径就顺理成章:进程 A 在用户空间准备好数据,通过 copy_from_user 把它拷进内核缓冲区;进程 B 发起接收,内核再通过 copy_to_user 把数据拷进 B 的用户空间。
用户空间 1 → 内核空间 → 用户空间 2,数据被完整搬运了两遍。管道、Socket、消息队列都是这个模式。
3.2 两次拷贝的代价
寄快递时,包裹先从寄件人手里交到快递员,再由快递员交到收件人,中间必然经手两次。IPC 也一样,每一次拷贝都意味着一段 memcpy、一次 CPU 时间和一份额外的内存占用。
传一条几十字节的消息,两次拷贝无所谓;但 Android 上每次 startActivity、每次查询 PackageManager 都要走跨进程,系统运行期间的 Binder 事务是以万次计的。在这个量级上,省掉一次拷贝就是实打实的性能收益。
另有一个隐性开销:接收方事先不知道数据多大,只能按最大可能预留缓冲区,或者多做一轮协商。
四、Binder 的三个优势

4.1 性能:一次拷贝
Binder 驱动在接收方进程启动时用 mmap(内存映射,把一段物理内存同时映射到多个虚拟地址上)做了一件事:把内核的一块缓冲区映射进接收方进程的用户空间。这块内存有两个虚拟地址,一个在内核,一个在接收方的用户空间,指向的却是同一份物理页。
于是发送方 copy_from_user 把数据写进这块缓冲区,动作一结束,接收方在自己的用户空间里已经能直接读到它,第二次拷贝彻底省掉。拷贝次数的对比很清晰:
- Socket、管道、消息队列:2 次;
- 共享内存:0 次;
- Binder:1 次。
Binder 的性能仅次于共享内存,比其余所有传统 IPC 都好。
4.2 易用:C/S 架构
共享内存是零拷贝,为什么不用它?因为它只给了一块内存,别的什么都不管。两个进程读写同一块区域,同步、互斥、时序全要自己实现,跟多线程共享变量一样容易踩坑,死锁和数据竞争的排查成本极高。
Socket 是通用性最好的方案,能跨机器通信,代价是效率——建立连接要握手,数据要经过协议栈封装解封,用来做同一台设备上两个进程的高频调用属于杀鸡用牛刀。
Binder 走的是 C/S(Client/Server,客户端/服务端)架构。服务端把能力定义成接口,客户端拿到代理对象后像调本地方法一样调用,参数打包、事务收发、线程调度都由框架和驱动完成。上层代码写起来跟调用普通对象没有区别,这是它被大规模使用的直接原因。
4.3 安全:身份由系统分配
传统 IPC 在协议层面不携带身份信息。要识别对方是谁,只能靠上层协议约定——发起方把自己的 PID(Process ID,进程号)和 UID(User ID,用户号,Android 上每个应用在安装时被分配一个)写进数据包。这个字段是发送方自己填的,填什么都行,相当于口头报一个身份证号,无从核实。
Binder 不接受自报。进程创建时 UID 由系统分配,一次 Binder 调用到达服务端时,调用方的 PID 和 UID 由内核填入,服务端通过 Binder.getCallingUid() 拿到的是内核认定的值,伪造不了。权限校验因此有了可靠依据,出了问题也能追溯到具体应用。
访问入口的控制方式也不同。传统 IPC 的节点是开放的,知道路径或端口就能连上去,像一个谁都能上车的公交站台;Binder 的服务需要先向系统申请引用,能不能拿到由服务端和权限策略决定。
五、实名与匿名:ServiceManager 的角色

5.1 判断方法:有没有注册到 SM
Binder 服务分实名和匿名两种,区分标准只有一条——有没有注册到 ServiceManager。注册过的是实名服务,任何进程凭服务名就能找到它;没注册的是匿名服务,只有拿到过它引用的进程才能调用。
AMS、PMS、WMS 这些系统服务都是实名的;自己写一个 Service,通过 bindService 把 Binder 交给客户端,那就是匿名服务。两段代码对照着看:
// 实名服务:system_server 启动时把 AMS 登记到 ServiceManagerServiceManager.addService(Context.ACTIVITY_SERVICE, mAms); // 第一个参数是服务名,注册后全局可查(该 API 是 @hide,仅系统进程可用)IBinder ams = ServiceManager.getService(Context.ACTIVITY_SERVICE); // 任何进程按同一个名字都能取回它的 IBinder 引用 // 匿名服务:应用自己的 Service,Binder 从未登记到 ServiceManagerpublic class MyService extends Service { private final IMyAidl.Stub mBinder = new IMyAidl.Stub() { // AIDL 生成的 Stub 继承自 Binder,因而具备跨进程能力 @Override public int add(int a, int b) { // 服务端真正执行的业务方法 return a + b; // 计算发生在服务端进程,结果沿原路返回客户端 } }; @Override public IBinder onBind(Intent intent) { // 客户端 bindService 时回调 return mBinder; // 引用只交给发起绑定的那个客户端,外人查不到 }}差别落在可发现性上:实名服务是登记在册的公共设施,匿名服务是私下递出去的一把钥匙。这个区分是 Binder 面试里的高频考点,也决定了自定义服务的安全边界——没注册到 SM,攻击面天然小很多。
5.2 ServiceManager 是服务大管家
ServiceManager 负责保管所有实名服务的名字和 Binder 引用。需要访问某个系统服务时,进程先向它查询,拿到对应的 IBinder 对象,再基于这个对象发起调用。整个流程类似打 110:不用知道哪位警察在哪儿,报到调度中心,由它派人。
ServiceManager 自己也是一个系统服务,跑在独立进程里。它有个特殊之处——句柄固定为 0,所有进程都能直接拿到它的引用,不需要先向谁查询。整套服务发现体系的起点就落在这个约定上。
六、总结
6.1 三点优势与取舍
Binder 的答案分三层:性能上用 mmap 把两次拷贝压缩成一次,只比零拷贝的共享内存差一点;易用上基于 C/S 架构,上层就是调用一个方法,比共享内存的手工同步和 Socket 的协议栈开销都划算;安全上身份由内核按系统分配的 UID 校验,服务访问受控,而传统 IPC 只能听发送方自报家门。
那"差的一点"是主动放弃的。共享内存快,但它把同步和访问控制的责任全推给了应用;Binder 用一次拷贝换来了清晰的调用模型和可靠的身份校验,对一个要跑几十万个第三方应用的系统来说,这笔交易划得来。
6.2 动手验证
设备上跑 adb shell ls -l /dev/binder,能看到那个字符设备节点,它就是所有跨进程调用的入口;Android 8 之后还会有 /dev/hwbinder 和 /dev/vndbinder,对应 HAL 层和厂商分区的独立通道。
再执行 adb shell ps -A | head,观察每个进程前面的 USER 列——u0_a123 这样的名字就是系统分配的 UID,同一个应用的多个进程共用一个。这个值伪造不了,正是 4.3 里那套权限校验的依据。想看服务清单,adb shell service list 会列出所有注册在 ServiceManager 里的实名服务,自己写的匿名服务不会出现在里面。
本文由 AgentPlanFlow 生成