1. 疑问
- binder的数据拷贝到底是1次还是2次?
- 跨进程通信中,为什么binder对于数据的拷贝次数少于socket?
- binder和socket数据拷贝是具体怎么操作的呢?
- 什么时候用binder,什么时候用socket?
2. binder原理
对于进程A和进程B通过binder通信,进程A通过binder传递数据给进程B。那么进程A操作的动作为write数据,进程B操作的动作为read数据。
在Android AIDL中,这个流程可以理解为 进程A为客户端,进程B为服务端。对于进程B,在起来后,就会为目标方法在内核空间中分配对应的mmap物理地址,以及用户空间里的虚拟映射地址。这个只能执行读数据的操作,没有写数据的权限。
进程A 通过上层接口在用户空间生成目标数据,记为data。接着通过binder接口传入数据data。此时程序会将包含data的事务由用户空间拷贝到内核空间的进程B的物理地址里。 这里有一点需要理解的就是,上文说到了事务,它是对数据的一个封装,内容包含了数据,目标进程的标识,元数据。这也是binder和socket的一个区别。 事务被拷贝到内核空间后,binder驱动会分析事务里的目标进程标识,将事务的地址放到进程B的处理队列中。进程B从队列里读取到这个事件后,会进行read操作,通过进程B的虚拟内存地址直接从内核空间里读取数据。
到这里,一次binder流程走完了。但是我们一般在AIDL中使用binder,AIDL是通过方法和进程B交互的,方法除了入参还有返回值,所以这里实际使用中还需要等待进程B回复结果
举个例子:我们使用AIDL时候,比如进程A 存在方法getVersion(int curTime) 这个方法从进程B获取版本,那么方法是同步的,且A 有write数据给进程B,进程B还有返回数据给到A,这个方法才算执行结束。
所以对于进程A,数据write到进程B的mmap物理内存空间后,自己也会根据自己是否需要接收B返回的数据,来开辟进程A的mmap地址,从而接收结果。
整个流程如图:

从流程图里可以看到一次Android AIDL通信有2次binder交互,一次binder交互只会存在一次数据拷贝。
3. binder原理分析中想到的几种异常场景
3.1 如果多个进程同时和B进行通信,流程如何?
对于进程B,作为服务端的存在,他创建时候,分配的binder物理内存存在多个槽,每个write的进程write进去的数据都是独立的,如果发现内存不够,那么write的进程就会阻塞,被放入到TODO队列中,直到内存释放才会进行write。
3.2 如果进程A write时候,目标进程B未存在,怎么办
- 如果在进程A访问进程B时候,发现进程B不存在,那么在A write之前,binder驱动就会告知进程A。我们使用AIDL时候,对应的代码就是获取不到目标Service对象,或者获取到的对象是Null。
- 如果在进程A write之后,进程B处理数据时候忽然挂了,那么此时binder驱动也会检测到,会通知进程A,抛出一个DeadObjectException异常。
3.3 如果进程A write之后,进程B正在处理数据中,进程A挂了
进程B会正常进行,进程B write之后,binder驱动会清除进程A的记录。
4. socket原理
Socket分为跨进程Socket和网络通信Socket,两者在接口调用上没差别,但是在底层流程上有一些区别。
4.1 跨进程Socket
对于Socket跨进程通信,目前也分为两种,Linux专门针对Socket跨进程通信优化出一种新的Socket交互方式,Unix Domain Socket。Android 的类 LocalSocket对其进行了封装。
我们先分析传统的基于tcp/ip协议的Socket跨进程通信
4.1.1 tcp/ip socket IPC
对于传统的tcp/ip socket本地跨进程通信:
- 进程A调用send方法发送数据(记为Data)后,内核会先将数据从进程A的用户空间拷贝到内核空间里的该Socket的数据发送缓存区排队等待,然后内核的TCP协议栈会依次从数据发送缓冲区里取数据封装成协议包。
- Data被封装为协议包后,IP协议栈会解析其目标地址,发现其目标地址是localhost(127.0.0.1),接着会将其发送给lo接口的输入队列中等待处理。
- lo接口从队列中读取到数据后,会将数据回环发送给TCP协议栈等待解包
- TCP协议栈拿到数据进行解包处理后,获取到Data,放到进程B的Socket的接收缓冲区里。
- 进程B的接收缓冲区收到数据后,内核会唤醒进程B,让其进行读取数据操作。将数据Data从内核空间拷贝到进程B的用户空间,至此,一次单向的Socket通信结束。

所以从图中可以看到,Socket比较binder,在内核中多了一次数据拷贝,即从发送缓冲区到接收缓冲区的拷贝。另外需要注意的是,接收端读取数据时候,还要从内核拷贝数据到用户空间,这里又多了一次拷贝。综合来说,传统的Socket IPC本地通信,一共存在3次数据拷贝。
4.1.2 Unix Domain Socket
它是一种可以使用Socket进行本地高效率通信。
- 进程A和进程B使用Unix Domain Socket 通信。
- 进程A 写数据,CPU会将数据从进程A的用户空间拷贝到内核中的Socket发送队列中
- CPU 分析数据接收方,将数据在队列中的地址指针发送给进程B
- 进程B被唤醒,主动读取数据,将数据从内核中的发送队列里拷贝到进程B的用户空间

从流程里可以看到 Unix Domain Socket对于数据的拷贝,在内核中由于不存在两个进程之间的数据拷贝,所以总拷贝次数会少一次。
- 对比
- binder
- 数据拷贝 1次
- 在Android中使用,在AIDL中使用时候,一次接口调用实际走两个binder,所以一次接口调用数据拷贝经历2次
- 一般没有特殊需求,我们使用AIDL来进行跨进程通信,舒适度和代码整洁度最佳。
- 传统Socket
- 数据拷贝3次
- 一般常用于网络通信。如果即可跨进程又可以走网络通信,使用传统Socket适配度最好。
- Unix Domain Socket
- 数据拷贝2次
- 一般如果是系统服务和APP服务跨好多层进行交互,使用AIDL实现复现的情况下,可以使用Socket,减少设计复杂度。