前言
先简单概括性地说说Linux现有的所有进程间IPC方式:
管道(Pipe):在创建时分配一个page大小的内存,缓存区大小比较有限;
消息队列(Message):信息复制两次,额外的CPU消耗;不适合频繁或信息量大的通信;
共享内存(Share Memory):无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决。
套接字(Socket):作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信。
信号量(Semaphore):常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。
信号(Signal):不适用于信息交换,更适用于进程中断控制,比如非法内存访问、杀死某个进程等。
Google为什么选择Binder?
从4个角度来展开对Binder的分析
(1)从性能的角度
数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、套接字都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。
(2)从稳定性的角度
Binder是基于C/S架构的,架构清晰明朗,Server端和Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有Client端与Server端之别,需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从稳定性角度看,Binder架构优越于共享内存。
(3)从安全的角度
传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,手机安全显得额外重要,传统Linux IPC五任何保护措施,完全由上层协议来确保。
Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。
传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开发的,无法建立私有通道。从安全角度,Binder的安全性更高。
(4)从语言层面的角度
大家都知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句)。Binder恰恰符合面向对象思想,将进程间通信转化为对某个Binder对象的引用,调用该对象的方法。而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程,而它的引用却遍布于系统的各个进程之中。Binder模糊了进程边界,淡化了进程间通信过程,让整个系统仿佛运行于同一个面向对象的程序之中。
Binder通信机制流程
Binder框架定义了四个角色:Server,Client,ServiceManager(以后简称SM)以及Binder驱动。其中Server,Client,SM运行于用户空间,驱动运行于内核空间。
Binder驱动
尽管名叫“驱动”,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的。它工作于内核中,主要任务包括:驱动负责进程之间Binder通信的建立;Binder在进程之间的传递;Binder引用计数管理;数据包在进程之间的传递和交互等一系列底层支持。
ServiceManager与实名Binder
SM的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder。
过程:Server创建了Binder实体,为其取一个字符形式名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给SM,通知SMgr注册一个名叫xxx的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及SM对实体的引用,将名字及新建的引用打包传递给SM。SM收数据包后,从中取出名字和引用填入一张查找表中。
SM是一个进程,Server是另一个进程,Server向SM注册Binder必然会涉及进程间通信,这里进程间通信用的是Binder吗?答案是肯定的,SM提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成SM时,Binder驱动会自动为它创建Binder实体。其次这个Binder的引用在所有Client中都固定为0而无须通过其它手段获得。
Client获得实名Binder的引用
Server向SM注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。
匿名Binder
并不是所有Binder都需要注册给SM广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向SM注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。
Binder内存映射和接收缓存区管理
传统IPC方式中,数据是怎样从发送端到达接收端的呢?通常的做法是:发送方将准备好的数据存放在缓存区中,通过系统API调用进入内核中。内核服务程序在内核空间分配内存,将数据从发送方缓存区复制到内核缓存区中。接收方读数据是也要提供一块缓存区,内核将数据从内核缓存区拷贝到接收方提供的缓存区中并唤醒接收线程,完成一次数据发送。这种存储-转发机制有两个缺陷:首先是效率低下,需要做两次拷贝(用户空间->内核空间->用户空间)。Linux使用copy_from_user()和copy_to_user()实现这两个跨空间拷贝。其次是接收数据的缓存要由接收方提供,可接收方不知道到底要多大的缓存才够用。只能开辟尽量打的空间或先调用API接收消息头获得消息体大小,再开辟适当的空间接收消息体。两种做法都有不足,不是浪费空间就是浪费时间。
Binder采用一种全新策略:由Binder驱动负责管理数据接收缓存。Binder驱动通过实现mmap()来创建数据接收的缓存空间。
fd = open("/dev/binder", O_RDWR);
mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。mmap()的返回值是内存映射在用户空间的地址,不过这段空间是由驱动管理,用户不必直接访问(映射类型为PROT_READ,只读映射)。