Binder(五):Binder 机制是如何跨进程的,一次拷贝的完整链路

文章内容

一、面试题:Binder 机制是如何跨进程的

1.1 题目的本质

这道题没有新知识点。Binder 的三层定义、只拷贝一次的优势、mmap 的原理,前面几讲拆开讲过,这道题要的是把它们串成一条从发送方到接收方的链路,一口气讲完。

答不好的人通常不是不懂 mmap,而是零散地知道几个名词,说不清数据具体经过了哪几站、每一站发生了什么。

1.2 一个原理,多种问法

字节、腾讯这类公司问得细,往往从「Binder 为什么只拷贝一次」切进去,接着追 mmap 的实现,再追 binder_mmap 在驱动里干了什么。也有面试官只笼统丢一句「Binder 机制是如何跨进程的」,让你自己组织。

问法不同,答案的骨架是同一条:进程一 → 内核 → 进程二。

二、一次拷贝的完整链路

Binder 跨进程链路:进程一 → 内核 → 进程二,全程一次拷贝

2.1 起点:进程内存不共享

两个应用进程各有自己的用户空间——进程可以直接访问的那部分地址空间,彼此隔离,谁也读不到对方的内存。这是操作系统的保护设计,所以进程一没法把一个指针递给进程二让它自己去读。

2.2 中转:内核空间共享,copy_from_user 拷一次

隔离之下还留了一块公共地带:内核空间。它对所有进程是同一份,进程通过系统调用陷进去,看到的是同一套内核数据结构。跨进程通信的中转站只能设在这里。

发送方发起事务时,驱动调用 copy_from_user——内核从用户态地址读数据的标准接口,内核不能直接解引用用户传来的指针,必须走这个带校验的入口——把数据从发送方的用户空间拷进内核。这是整条链路上唯一的一次数据复制。

2.3 落地:mmap 让内核与接收方映射同一块物理内存

mmap 共享物理内存:内核与接收方的虚拟地址落到同一物理页

数据拷进内核之后不需要再动,因为落点是一块特殊的内存。接收方进程启动时通过 mmap(把一段虚拟内存和某个对象关联起来的系统调用)向 Binder 驱动申请了一块缓冲区,驱动在内核地址空间和接收方用户空间各建了一条页表映射,两边指向同一批物理页。

copy_from_user 写进去的位置正好在这块共享区里。数据一落地,接收方那侧同步可见,不存在第二次搬运。

2.4 终点:接收方在自己的用户空间直接读

驱动唤醒接收方线程,交给它的是缓冲区里的一个偏移量。接收方按这个偏移读自己的用户空间,拿到的就是发送方刚发过来的内容。全程一次拷贝,这句结论到这里才算讲完整。

三、接收方怎么知道是找自己的

3.1 发送方声明目标

接收方不需要判断这份数据是不是给自己的。发起一次 Binder 事务(一次跨进程调用的请求包,含目标、命令码和数据)时,发送方在事务结构里写明了要找谁,路由信息从源头就带上了。

3.2 内核按目标路由

发送方声明目标 handle,内核按 handle 路由到 ServiceManager

目标在用户态表现为一个 handle——发送方进程内标识远端 Binder 对象的整数句柄,驱动拿它查出对应的接收方进程和线程。比如要找 ServiceManager(Binder 的服务注册与查询中心,handle 固定为 0),驱动就把事务挂到 ServiceManager 的待办队列上并唤醒它。

分发的责任全在内核。接收方只是从自己的队列里取走属于它的事务。

3.3 快递类比

快递类比:单子写清收件人,快递员投递,收件人不用猜

寄一支口红,寄件人在单子上写清收件人是谁,快递员照着地址送上门,收件人看一眼就签收了。收件人不必去猜这个包裹是不是自己的——寄件时目标就定死了,中间环节只负责按目标投递。

四、代码验证

4.1 一次跨进程事务的示意

用户态发起 Binder 调用最终都会落到一次 ioctl 上,事务的目标和数据指针都在这次调用的参数里。下面这段体现的是参数形态,不代表 libbinder 的真实封装。

c
// 示意代码:一次 Binder 事务从用户态发出的大致形态struct binder_transaction_data tr;                    // 事务描述结构,驱动按它决定投递给谁tr.target.handle = 0;                                 // 目标句柄,0 固定代表 ServiceManagertr.code = SVC_MGR_CHECK_SERVICE;                      // 命令码,告诉对方要执行哪个方法tr.flags = 0;                                         // 0 表示同步调用,发送方会阻塞等回复tr.data_size = size;                                  // 数据长度,驱动据此决定拷贝多少字节tr.data.ptr.buffer = (uintptr_t) buf;                 // 指向发送方用户空间里打包好的数据 struct binder_write_read bwr;                         // 一次 ioctl 可以同时携带写和读bwr.write_buffer = (uintptr_t) cmd_with_tr;           // 写缓冲区:BC_TRANSACTION 命令加上面的 trbwr.write_size = cmd_len;                             // 写入长度bwr.read_buffer = (uintptr_t) reply_buf;              // 读缓冲区,用来接收驱动返回的结果bwr.read_size = sizeof(reply_buf);                    // 读缓冲区容量 ioctl(fd, BINDER_WRITE_READ, &bwr);                   // 陷入内核,fd 是打开 /dev/binder 拿到的描述符// 驱动内部:按 tr.target.handle 找到接收方进程 →// 从接收方 mmap 出来的共享缓冲区里分配一段 →// copy_from_user 把 tr.data.ptr.buffer 的内容拷进去(唯一一次拷贝)→// 挂到接收方的待办队列并唤醒它,接收方读自己的用户空间即可拿到数据。

路由和拷贝在同一次 ioctl 里完成:handle 决定数据去哪个进程,copy_from_user 决定数据怎么过去。把这两句说清楚,这道面试题的主体就答完了。

4.2 验证方法

在设备上执行 cat /proc/<pid>/maps,随便挑一个应用进程,都能看到 /dev/binder 对应的那段映射,那就是 2.3 说的共享缓冲区,大小通常在 1MB 左右。

adb shell service list 会列出所有注册到 ServiceManager 的系统服务,每一条都经过了本文这条链路才注册成功。想看实时的 IPC 行为,adb shell am trace-ipc start 与 stop --dump-file <path> 可以抓一段 Binder 调用记录,具体支持情况随 Android 版本有差异。

五、总结

5.1 一句话回答

进程之间用户空间隔离,但内核空间共享;发送方在事务里声明目标 handle,驱动按它找到接收方,用 copy_from_user 把数据拷进内核(唯一一次拷贝),而这块内存通过 mmap 同时映射在接收方的用户空间,接收方直接读就拿到了数据。

5.2 面试追问的方向

顺着这条链路往下,常见的追问有三个:mmap 具体做了什么、为什么是一次拷贝而不是零拷贝、共享内存明明零拷贝为什么 Binder 不用。第三个的答案在于取舍——共享内存两端同时映射,同步和权限全交给应用层,Binder 用发送侧一次拷贝换来了内核统一管理。

答完主链路之后主动把这几点带出来,比等着被追问要主动得多。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Binder(五):Binder 机制是如何跨进程的,一次拷贝的完整链路 · Yinchen 的博客