最后来看一下支持这一整套Binder机制的幕后功臣,Binder驱动。作为一种IPC驱动,Binder跟Unix/Linux历史上有过的任何驱动都不相同,功能强大,同时代码又很简洁,创造这套机制的Dianne Kyra Hackborn的确也跟LinusTolvalds一样是神一级的人物。
Binder机制总共不到一千多行轻描淡写的代码,便完成了跨进程交互里各种令人头疼的设计问题和编程问题,不容易读懂,但细看明白才能明白,作者在设计每一个变量、每一个bit时的良苦用心。
对于Binder驱动,不幸的消息是,这套机制并不符合Linux内核的专款专用的设计思想,同时也将功能过多地抛到了用户态来处理,于是除了Android和另一个比较小众的ALP,几乎没人使用。Binder驱动本身也被存放在标准内核的drivers/staging/android目录下,处于待考察状态。
但好消息是,Android系统所向披靡,于是Binder驱动的使用量终于变得足够大,研究它的工作原则变得很有价值。而更重要的,Binder驱动目前在Linux内核里的IPC机制里是无敌的,没有任何IPC机制可以跟Binder媲美,Binder在性能、面向对象等多方面特性上,不可取代。以致于Linux开发者也在讨论一种能够提供跟Binder类似能力的IPC,到编写本文为止,尚无任何成果。
Binder驱动简介
Android的这套基于Binder的IPC机制,源自于传奇性但又比较悲催的OpenBinderIPC框架。
OpenBinder是由一家叫Be的公司开发,这家传奇性的法国软件公司制造了传奇性的BeOS,当年苹果公司在操作系统研发上遇到困境时,可以用来拯救苹果的操作系统方案,一个是NeXT,另一个便是BeOS。BeOS在构架上和成熟度上比NeXT更具优势,但或许是有优势的东西就会有更高的姿态,要价更高,于是机会便被留给了NeXT,于是有了今天的MacOSX和iOS。BeOS在构架上设计思路上在当年还是很先进的,就比如延用至今的OpenBinder,如果仔细看BeOS的编程文档,会发现整个系统交互与今天的Android有很大的类似之处。可惜后来BeOS最终没有避免破产的命运,BeOS就被作为软件资产,在2001年被Palm收购。
OpenBinder在Palm也曾风光一时。Palm以简洁低功耗设备迅速成长起来之后,也需要一种高效的,类似于Corba的系统级消息互通机制,以构造更复杂的系统。这样的尝试,得到了一个悲情的操作系统,Palm OS Cobalt(Palm OS 6),本来作为Palm OS 5的后继者,这一操作系统被寄予很大期望,但没有产商愿意生产基于它的设备。但得到的好处是,OpenBinder在这种商业应用前景不明的情况最终还是选择了开源,OpenBinder本身的信息可以很容易得到,见http://www.angryredplanet.com/~hackbod/openbinder/,这个网站是OpenBinder创造者Dianne Kyra Hackborn的个人网站。而OpenBinder所依存的操作系统环境很不稳定,需要考虑兼容性又被迫随着市场需求在多种操作系统内核上移植,历经BeOS、Windows、PalmOS Cobalt的微内核、Linux内核,最终使OpenBinder具备强大的可移植性。
虽然OpenBinder在技术上是一种很优秀的方案,其命运却是一再如此悲催,使用OpenBinder技术的操作系统,都没有走入主流然后就销声匿迹。在今天的操作系统世界里,使用OpenBinder的并不多,仅ALP(ACESS Linux Platform)在使用OpenBinder作为其IPC机制,但ALP所占市场份额实在太小,发展前景很不明朗。但革命性的Android操作系统,最终选择了这套方案,使OpenBinder终于发挥了其强大潜力。Android使用OpenBinder的基本构架,但并非完整的OpenBinder,所有只称其为BinderIPC。在Android系统的设计者眼中,Android跟OpenBinder并无直接联系,会强调Android世界里的Binder是独特设计过的,可能是出于法律上的顾虑。但我们对比OpenBinder和Android里的Binder实现,就会发现这两者在本质上是一样的。相对而言,Android的Binder机制是OpenBinder的一种简化版本,学习Android底层开发,如果Binder本身不容易理解,可以参考OpenBinder的文档。
至于Android为什么会选择Binder作为其底层通信机制而不是重新设计或是借用已有方案,坊间谣传是由于 Android底层开发人员大都曾是BeOS或是Palm的开发人员,更熟悉这套开发框架。但如果不是Binder机制足够优秀,可能也会在Android系统的发展中被抛弃。Binder提供了一种功能强大、简洁、高效、面向对象的跨进程传递方式,而到目前为此,还只是Binder能够提供这样的能力。
- 面向对象。跟传统的IPC机制不同,Binder驱动设计的目的就是用于进程之前来传递对象,面向对象是其本质之一,所以能提供更好地支持面向对象设计的系统。
- 面向进程。Binder不是完全以文件描述符作为其分发实现,而是文件描述符与进程ID的组合,从而能更好地提供安全性、进程调度等功能。
- 简单。Binder从原理与实现都很简单,大部分逻辑都在用户态完成,内核态的驱动仅由一个C文件提供。
- 高性能。由于简单和灵活上实现,Binder在驱动层可以更高效地分发消息,是目前Linux内核之上的IPC通信机制中最高效的一种。
- 自动化内存管理。结合面向对象,在Binder里很容易构建出自动化垃圾回收机制,即可良好地服务于Java虚拟机,也可以减小C++环境里编程时的内存管理方面的工作量。
- 灵活。面对对象的特质,使Binder相关的实现与拓展都很容易,Binder可用于多种应用场景。
当然,Binder也并非毫无缺陷。作为一种IPC机制,Binder的含义又不仅只是IPC通信,还涉及进程管理、线程管理、内存管理等诸多方面的需求,违背了设计上的低耦合性原则。Binder驱动提供的功能,将更多的操作暴露到用户态,也不符合Linux内核的设计思想。最重要的是,通过Binder的灵活,使基于Binder的相关代码不再清晰,有时不容易掌握Binder具体执行。所以现在Binder驱动 ,并非Linux主流(Mainline),在Linux内核源代码里而只是被放在drivers/staging/android目录里,处于待考察状态。
整个Binder驱动只由一个驱动文件实现,是drivers/staging/android/目录里的binder.c,而其头文件binder.h会定义一些通用数据结构,用于与用户态编程时共享。
Binder驱动如何被使用
我们可以通过上层调用来分析Binder驱动在功能上的需求。
在发生 Binder交互时,都会是Proxy对象通过IPCThreadState:transact(),走到writeTransactionData()来创建BC_TRANSACTION包,然后再通过waitForResponse()找到合适的Binder操作点,最后这一BC_TRANSCTION包会通过talkWithDriver()发送出去。在这时调用waitForResponse()时,还会同时根据是否需要取得返回值来判断是否需要进一步监听BR_REPLY包。得到的完整调用路径如下:
也就是此时Proxy端通过ioctl写入了一个BC_TRANSACTION命令,而在Binder另一端进行监听的Service进程,会通过同样的waitForResponse()得到BR_TRANSACTION命令,从而执行该命令对应的onTransact()操作。如果这一操作有返回,则Binder传输会反过来,Service进程写入BC_REPLY命令,而Proxy端通过waitForRepsonse()得到BR_REPLY命令,从而可以取回返回值:
最后Proxy端就通过BR_REPLY得到了调用的返回值,并把这个值存入一个Parcel对象里,在Proxy端编写的代码里,就可以在transact()调用之后,通过一个指定义为reply的Parcel对象得到这一返回结果。从代码执行上分析可能有点麻烦,如果把一切简化到Binder驱动相关的操作,则上述步骤只有四步:
从这也可以看出Binder传输上的特点。命令都是在两个进程间成对出现,一个进程操作BC_TRANSACTION或是BC_REPLY,另一个进程则有可能收到BR_TRANSACTION或是BR_REPLY。而TRANSACTION和REPLY得到的四条命令,则是Binder操作上唯一带有数据负载的命令,也是Binder能够成为IPC机制的核心功能。
在talkWithDriver()方法里,是Binder操作的通用方法。Binder交互在操作上相对比较简单,只通过ioctl()调用来完成Binder的读写,而通过一个binder_write_read数据结构来描述如何进行Binder读写操作。
- binder_write_read bwr;
- if(ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >=0)
- err = NO_ERROR;
- else
- err = -errno;
调用ioctl()时,第一个参数是一个fd文件描述符,这个参数会是从ProcessState得到一个通过open()系统调用得到的。第二个参数,会是一个操作命令,这是一个ioctl码,是由binder.h定义,驱动所能处理的针对Binder驱动有效的ioctl码。第三个参数则是用户态与内核态进行交互的数据结构,在上面的片断里,bwr是一个处于用户空间的局部变量,也就会处于用户态程序的栈空间里,但并不仅是传递给内核态,而是进行交互,传递参数给内核时,用户态程序填写好bwr内容再交给内核态来读取,而从内核态传回数据时,内核态改写bwr的内部然后用户态程序在ioctl()之后再读取。
Binder驱动所支持的ioctl码,只有BINDER_WRITE_READ使用最频繁,而其他的只是用于提供一些Binder操作的辅助功能。Binder驱动所支持的ioctl如下所示:
- #define BINDER_WRITE_READ _IOWR('b',1, structbinder_write_read)
- #define BINDER_SET_IDLE_TIMEOUT <span style="white-space:pre"> </span> _IOW('b',3, int64_t)
- #define BINDER_SET_MAX_THREADS <span style="white-space:pre"> </span> _IOW('b',5, size_t)
- #define BINDER_SET_IDLE_PRIORITY _IOW('b',6, int)
- #define BINDER_SET_CONTEXT_MGR <span style="white-space:pre"> </span> _IOW('b',7, int)
- #define BINDER_THREAD_EXIT _IOW('b',8, int)
- #define BINDER_VERSION _IOWR('b',9, struct binder_version)
这些Binder IOCTL码的作用有:
Binder IOCTL码 | 作用 |
BINDER_WRITE_READ | 完成对/dev/binder的读写操作,这会是最频繁的Binder IOCTL操作 |
BINDER_SET_IDLE_TIMEOUT | 设置IDLE超时 |
BINDER_SET_MAX_THREADS | 设置当前Binder实例可用的最大线程数 |
BINDER_SET_IDLE_PRIORITY | 设置Binder在进程处于IDLE时的进程优先级 |
BINDER_SET_CONTEXT_MGR | 让自己成为Binder的管理者,只有servicemanager会用到 |
BINDER_THREAD_EXIT | 退出Binder处理的内核线程 |
BINDER_VERSION | 返回Binder驱动的当前版本 |
上面这些ioctl码,是一个由_IOC宏拓展出来一个32位整数,用于标识通过ioctl()系统调用处理的数据类型,参考对应平台的ioctl.h文件,一般来说,ioctl码的各个位意义如下:
bits | 含义 |
31-30 | 00 – 无含义,使用_IO macro宏生成 10 – 读,使用_IOR宏生成 01 – 写,使用_IOW宏生成 11 – 同时支持读写,使用_IOWR生成 |
29-16 | 参数的大小 |
15-8 | 这一ioctl码对应对应的ASCII码标识,最好保持系统内唯一 |
7-0 | 具体的操作码 |
对应到Binder的IOCTL码定义,比如BINDER_WRITE_READ,是通过_IOW宏来生成的,于是最高两位会是0x01 (bit 31-30),参数大小是binder_write_read数据结构的大小,0x18(bit 29-16),标识码是’b’:0x62(bit 15-8),具体的操作码是1:0x01(bit 7-0),拼接起来就会是32位整数,0x40186201。
就像我们代码里的例子所示,如果通过ioctl()系统调用来操作一个binder_write_read数据结构时,此时我们就需要明确描述该操作是由内核态驱动到用户态程序,还是由用户态程序到内核态驱动。根据这种命令的方向性,可以分列出两类命令:BC_*系列,Binder Command的简称,说明用户态给Binder驱动发出的命令;BR_*系列,Binder Return的简称,说明是Binder驱动给用户态发回的操作结果反馈。这些命令不仅说明数据传输时的方向,同时也可以被拓展出来多种的功能,比如对象引用计数管理等功能,这样的命令出于当前的需求会有一个系列,同时在将来被使用到时也需要能够被拓展。为了规范化这两类操作,就定义了两个Enum结构:
- enum BinderDriverCommandProtocol {
- BC_TRANSACTION= _IOW('c', 0, struct binder_transaction_data),
- BC_REPLY= _IOW('c', 1, struct binder_transaction_data),
- BC_ACQUIRE_RESULT= _IOW('c', 2, int),
- BC_FREE_BUFFER= _IOW('c', 3, int),
- BC_INCREFS= _IOW('c', 4, int),
- BC_ACQUIRE= _IOW('c', 5, int),
- BC_RELEASE= _IOW('c', 6, int),
- BC_DECREFS= _IOW('c', 7, int),
- BC_INCREFS_DONE= _IOW('c', 8, struct binder_ptr_cookie),
- BC_ACQUIRE_DONE= _IOW('c', 9, struct binder_ptr_cookie),
- BC_ATTEMPT_ACQUIRE= _IOW('c', 10, struct binder_pri_desc),
- BC_REGISTER_LOOPER= _IO('c',11),
- BC_ENTER_LOOPER= _IO('c',12),
- BC_EXIT_LOOPER= _IO('c',13),
- BC_REQUEST_DEATH_NOTIFICATION= _IOW('c',14, struct binder_ptr_cookie),
- BC_CLEAR_DEATH_NOTIFICATION= _IOW('c',15, struct binder_ptr_cookie),
- BC_DEAD_BINDER_DONE= _IOW('c', 16, void *),
- };
- enum BinderDriverReturnProtocol {
- BR_ERROR= _IOR('r', 0, int),
- BR_OK= _IO('r',1),
- BR_TRANSACTION= _IOR('r', 2, struct binder_transaction_data),
- BR_REPLY= _IOR('r', 3, struct binder_transaction_data),
- BR_ACQUIRE_RESULT= _IOR('r', 4, int),
- BR_DEAD_REPLY= _IO('r',5),
- BR_TRANSACTION_COMPLETE= _IO('r',6),
- BR_INCREFS= _IOR('r', 7, struct binder_ptr_cookie),
- BR_ACQUIRE= _IOR('r', 8, struct binder_ptr_cookie),
- BR_RELEASE= _IOR('r', 9, struct binder_ptr_cookie),
- BR_DECREFS= _IOR('r', 10, struct binder_ptr_cookie),
- BR_ATTEMPT_ACQUIRE= _IOR('r', 11, struct binder_pri_ptr_cookie),
- BR_NOOP= _IO('r',12),
- BR_SPAWN_LOOPER= _IO('r',13),
- BR_FINISHED= _IO('r',14),
- BR_DEAD_BINDER= _IOR('r', 15, void *),
- BR_CLEAR_DEATH_NOTIFICATION_DONE= _IOR('r',16, void *),
- BR_FAILED_REPLY= _IO('r',17),
- };
所有的交互消息,都使用跟ioctl码的相关宏定义,_IO、_IOW或是_IOR,但这并没有实际意义,跟一个整形值1、2、3类似,只是用于标明这一消息的唯一性,如果将来我们增加了新命令,不容易因为操作命令序列的变化而出现严重错误。另外,这样的定义也辅助了解析,这些命令的定义里都包含了参数长度信息,在同一个缓冲区里封装多条命令时,可以通过4+参数长度,很快跳转到下一条命令。
在上面看到的binder_write_read操作时,无论是读还是写,都可以使用同一个BINDER_WRITE_READ的ioctl(),同时处理多个命令,同时包装多个BC_命令发多个命令到Binder驱动,或是通过一次ioctl()读出来多个返回结果,这样也节约了系统调用时的开销。此时,用户态的Binder相关的内存如下示意图所示:
作为一种IPC机制,Binder所能起到的作用是传递数据,其他命令或多或少只是提供一些辅助功能。在Binder所定义的这些命令里,能够完成数据传递功能的只有BC_TRANSACTION、BC_REPLY、BR_TRANSACTION和BR_REPLY四种。在RemoteService的分析里,也可以看到,两个进程交互时TRANSACTION和REPLY的BC_与BR_命令都有传递性,一个进程发出BC_TRANSACTION有可能触发另一个进程收到BR_TRANSACTION,进程发出BC_REPLY又会使一个进程收到BR_REPLY。
无论是TRANSACTION还是REPLY,通过ioctl()来操作时,使用的参数都会是一个叫binder_transaction_data的数据结构的指针。Transaction,一般被翻译成事务,代表着计算处理里的进行的原子操作,事务之间不会互相干扰,另外事务操作成功则会全局有效,不成功则不会产生任何影响。这样的概念一般用于数据库操作,用于维护数据库并发处理时ACID支持,在Binder传输时也借用了这种概念,也就是说每次Binder IPC操作都以事务方式进行,成功而会影响整个Binder状态,不成功则会被放弃,同时每个BinderIPC之间不会互相有干扰。
binder_transaction_data的构成如下:
- struct binder_transaction_data {
- union {
- size_t handle; /* target descriptor of command transaction */
- void *ptr; /* target descriptor of return transaction */
- }target;
- void *cookie; /* target objectcookie */
- unsigned int code; /* transaction command */
- unsigned int flags;
- pid_t sender_pid;
- uid_t sender_euid;
- size_t data_size; /* number of bytesof data */
- size_t offsets_size; /* number of bytes ofoffsets */
- union {
- struct {
- const void __user *buffer;
- const void __user *offsets;
- }ptr;
- uint8_t buf[8];
- }data;
- };
像binder_transaction_data这样的数据结构,将会描述Binder传输时一特征信息,一般也称为元数据,metadata。也就是说在这样的结构体时,只会描述数据变动的需求,而会避免将数据漫无目的地拷贝来拷贝去。如果数据本身没有变动,比如多次访问对同一对象进行操作,此时就有可能数据完全不拷贝,而只是有binder_transaction_data这样的meta data的拷贝。无论是读或者写,都将使用这样的结构体,使得这一结构体在Proxy和Stub端都会有不同的含义。这一数据结构成员变量的含义如下表如示:
成员变量 | 类型 | 作用 | |
target | union | BC_命令时,size_t | 在通过BC_系列命令向Binder驱动发出操作请求时,我们需要target.handle指定到需要访问的Binder对象 |
BR_命令时, void* | 通过BR_系列命令从Binder驱动里读回返回值时,需要指定返回的一段地址,target.ptr则会指向这个地址空间 | ||
cookie | void * | 附加属性,这可用于不同使用情况下加入一个附加值,辅助上层的处理。比如可用于unlinkToDeath(),一次将所有同一cookie的对象清除 | |
code | unsigned int | 这便是我们在编写AIDL和Native Service的IBinder命令,由IBinder::FIRST_CALL_TRANSACTION开始的命令序列 | |
flags | unsigned it | 定义传输时可使用的参数 | |
sender_pid | pid_t | 发出Binder命令的进程的pid | |
sender_euid | uid_t | 发出Binder命令的进程的euid,在进程执行过程中可能通过setuid()调用改变执行时的uid,euid则用于指定当前进程处于哪个uid的执行权限之下 | |
data_size | size_t | 数据区的大小 | |
offsets_size | size_t | 所包含数据的偏移量 | |
data | union | struct { void *[2] } ptr; ptr.buffer ptr.offsets | 当我们需要包含额外的数据负载时,由data来保存缓冲区的首地址与偏移。data.ptr.buffer指向首地址,data.ptr.offsets指向具体的偏移量。同时这两个值可与上面的data_size、offsets_size结构,通过首址加大小,将某部分需要操作的内容拷贝出来,而offsets也是地址,指向的可能会是数组,于是可以在同一buffer里保存多个并且大小不一的对象 |
uint8_t buf[8] | 如果数据量很小,可以直接重用这内置的8个字节的data来保存,通过data.buf来进行访问 |
通过这样的binder_transaction_data,就可以很精确地描述在某一次Binder IPC传递过程里需要做什么操作(target定位到对象、code定位到方法),操作的数据在什么地方(data_size、data_size、offsets_size定位作为参数的对象)。在这些信息里还会包含发起请求的pid、euid等信息,可进一步用于权限控制。至此,基于Binder的数据传输,可以表现为如下的组织形式:
无论是发送还是指收,会将需要操作的区域封装到一个TRANSACTION或是REPLY命令的列表。在需要处理内存区,而在具体的4字节的BC_或是BR_命令之后,会填写入所需要操作的binder_transaction_data内容。如果数据是内置的,直接会将数据包含在data.buf[]的8字节存储空间里,如果数据量大,可以通过data.ptr.buffer指向保存了需要被操作数据的一段用户态内存,同时由于存在data.ptr.offsets,可以进一步索引这段内存,offsets[0]是第一个部分,offset[1]是第二部分,offsets[3]是第三部分,等等。当这样的binder_write_read通过ioctl传入Binder驱动之后,Binder驱动会根据transaction的描述来进行具体的IPC操作。
如果只是使用binder_transaction_data,可以以面向对象的方式来实现RPC,但还不是全部,如果传递的参数只是按部就班地填到data.ptr索引的区域,对于参数的面向对象式的访问没有完成。试想一下,在远程调用时会使用两个方法:func1(obj1,obj2)和func2(obj1),如果只是把参数顺次填到data.ptr索引的区域,则Binder驱动需要维护(obj1,obj2)和(obj1)两份内容,但实际上obj1的存储区域是重叠的。出于纯粹面向对象的设计角度考虑,Binder驱动需要针对每个对象来维护引用。还好,用户态的libbinder在处理对象时,会通过Parcelable接口将对象平整化(flatten),对象会被肢解,然后填充到一个线性存储区域buf。每个Parcel对象的buf区域可通过data.ptr.offsets索引到,就达到传输时针对对象级别的精度。在Binder驱动里处理Parcel映射的数据结构是flat_binder_object,其结构如下:
- struct flat_binder_object {
- unsigned long type;
- unsigned long flags;
- union {
- void __user *binder; /* local object */
- signed long handle; /* remote object */
- };
- void __user *cookie;
- };
我们可以看到flat_binder_object的结构很简单,除去辅助性的类型、flag和cookie,穷得就只剩下一个指针,而且这个指针还是union,针对其引用的不同对象有不同含义。flat_binder_object对于本地对象的引用,会是一个用户态的指针,针对远程对象,则是一个long类型的handle,引用到远程对象。所以一个完整的Binder交互数据可能会是下面这样的形式:
data.ptr.buffer指向用户态存放的某个Parcel对象的列表,而data.ptr.offsets则会标明具体是需要操作这组对象的哪个。
有了flat_binder_object这样的表示形式,我们就可以使用16字节引用任何对象,增加的开销非常小,但可以得到一个完整的面向对象的传输能力。
Binder驱动的内部数据结构
任何驱动,都会在实现上维护一些局部数据结构,以提供硬件本身的一些上下文环境等,Binder也不例外,但Binder驱动的局部数据结构,更多地是用来映射Binder在用户态的一些概念,提供一种内核态的辅助手段。比如使用Binder的进程必然会有ProcessState和IPCThreadState对象,这两个对象必须要在Binder驱动里被映射并管理起来。另外,binder_transaction_data只是上次的访问接口,需要在驱动内部管理这些传输时的事务,针对于具体对象引用,在binder驱动里也需要维护对象的引用信息,从而实现对象重用。总结下来的话,就可以得到Binder驱动会使用的内部变量:
Binder 驱动变量 | 映射的用户态概念 | 作用 |
binder_proc | ProcessState | 用于描述当前操作Binder的进程的上下文状态,比如内存信息、线程信息、进程优先级等。Binder驱动的open()函数被调用后就会创建一个binder_proc结构,这样的结构针对于每个进程都是唯一的,跟ProcessState一样 |
binder_thread | IPCThreadState | 虽然被命名为binder_thread,但这名字会有误导,实际上跟内核线程没任务关系,只是用于描述用户态Binder线程状态,也就是IPCThreadState的状态。比如当前Binder线程有多少任务需要完成,是否有失败等 |
binder_transaction | binder_transaction_data | binder_transaction_data只是操作的外部接口,内部描述则是使用binder_transaction。在binder_transaction里虽然也只存储元数据,记录如何进行数据操作,但由于需要通过binder_transaction来完成具体的传输,它会包含更多的信息,比如这一transaction所关联的进程、线程、调度优先级等信息。 |
binder_node | flat_binder_object | Binder传输使用过的对象,都会通过binder_node维护一个引用计数,对于任何操作过的对象,在binder_node构成的检索树里,都会保存引用记录,从而可以快速查找到这一对象,并进行所需要的处理。binder_node只用于索引用户态的Binder对象 |
binder_buffer | - | 对象被使用过后,就可以通过binder_buffer存储起来,当下次被用到时,可以绕开内存拷贝的需求 |
binder_ref | - | Binder的内部对象,可索引到binder_node、binder_proc以及用于垃圾回收的binder_ref_death,从而可以加快查找 |
这些内部对象,都会在binder.c里定义,出于C式的面向对象技巧,因为只用于内部使用,于是没有必要暴露出来。这些数据结构的定义都很简练,可以结构代码来看到它们具体是如何被使用的,binder.c的编写者对于Linux内核运行原理理解很深刻,有良好的Linux内核的编程风格。但由于整个Binder驱动只是辅助上层的libbinder,所以驱动本身提供的自我解释性并不好,所以也不是很受Linux内核社区欢迎。
在Binder驱动的这些内部变量里,大量借用了Linux内核里已有的数据结构与算法。使用最多的数据结构有list_head、hlist_head和rb_node。
- list_head,list_head是Linux内核里标志性的通用算法之一,一个list_head结构里包含next和prev两个指针,通过这两个指针指向其他的list_head结构来构成双链接。但使用时则非常方便,只需要在某个数据结构的加入一个list_head成员变量即可,一般会被放在头部,这样可以自动实现存取时的地址对齐,查找时直接通过一个数据结构指针指向这一区域就得到具体的内容,一般不会手动去做,会通过list_entry()这个宏来完成。list_head可以高效地构建双链表和进行数据遍历,但每次插入节点或是删除节点时必须访问两个数据结构才能完成。
- hlist_node,hlist_node则是list_head的一个变种,原理跟list_head一样,但它本质上只是单链表,主要是实现只访问一个结点来达到插入节点和删除节点的目的,可以更高效地用于修改频繁的链表。它在实现的上的技巧是,hlist_node包含的next指针不变,而把prev换成pprev。pprev顾名思义也就是上个指针的上个指针,这样在某个结点前加入或是删除一个新结点时,因为pprev会跳过一个结点,于是只需要修改一个结点的链接关系就可以将新结点加入链表,这点就可以减小内存访问以及有可能造成的Cache失效的开销。当然,要是在结点后加入新结点,则跟原来一样需要两个结点修改才能完成插入。当此时得到链表则会是后进先出,类似于栈的结构,就不适合顺序处理的逻辑了。
- rb_node,rb_node是Red Black Tree(红黑树)的数据结构。红黑树是一种近似平衡树算法,查找和修改时的计算开销总会控制在O(log n)。它通过给树的每个结点标上上红黑标记,根结点和叶结点必须为黑,红结点的子结点必须是黑结点,同时保证从根结点到叶结点的每条路径上的黑结点数都一样。这样的规则保证了红黑树在插入结点后进行平衡时的开销被减小,只需要颜色改变或是少量翻转来维持这个规则,最后得到的树并不一定是完全平衡的,但接近平衡的状态,从而不像完全平衡树那样需要过多计算来保持平衡。红黑树算法在Linux内核里最开始被用于内存管理算法,后来也被CFS(Complete Faire Scheduler)调度器,后来几乎所有涉及频繁插入和查找的代码都使用这一算法。Binder驱动里也会大量使用红黑树,所有需要被查找的内容,像binder_node、binder_ref、binder_buffer、binder_thread都会使用红黑树来进行索引。
Binder驱动的实现
我们看看数据结构之后,再来看binder驱动的具体实现。Binder驱动最终会在系统里生成一个/dev/binder的字符设备文件,/dev/binder并不对应到任何的硬件,只是使用内存虚拟出来的字符设备。Binder驱动的实现非常简练,如果剥离到调试代码,基本上就是编写一个字符设备驱动的示例代码:
- static conststruct file_operations binder_fops = {
- .owner= THIS_MODULE,
- .poll= binder_poll,
- .unlocked_ioctl= binder_ioctl,
- .mmap= binder_mmap,
- .open= binder_open,
- .flush= binder_flush,
- .release= binder_release,
- };
- static struct miscdevice binder_miscdev = {
- .minor= MISC_DYNAMIC_MINOR,
- .name= "binder",
- .fops= &binder_fops
- };
- static int __init binder_init(void)
- {
- int ret;
- binder_deferred_workqueue= create_singlethread_workqueue("binder");
- if(!binder_deferred_workqueue)
- return -ENOMEM;
- ret= misc_register(&binder_miscdev);
- return ret;
- }
- device_initcall(binder_init);
在Linux内核启动过程中,会调用到do_initcalls()函数循环地调用.initcall段位的每个函数,于是就会调用到通过device_initcall()宏把自己编译到.initcall段位的binder_init()函数。但这里没有定义__exit,于是binder驱动不支持卸载。在binder_init()里会将一个binder_miscdev数据结构注册到系统,并创建一个维护驱动的workqueue,就此而已。也就是说,如果编译了binder驱动支持,则binder_init()函数会在启动过程中被执行,之后通过/dev/binder就可以访问到驱动,当此时binder并不提供任何功能。而在binder_miscdev数据结构,把一个binder_fops数据结构挂载在binder_miscdev的fops指针下,这样所有与/dev/binder相关的操作,都将对应到binder_fops里定义的函数,比如open()系统调用,会通过open() è sys_open() è binder_fops->open() è binder_open(),执行具体的binder_open()函数。
binder_open()的代码也不复杂,它会根据当前的上下文件环境,初始化一些后续操作所需要的基本链接:
- static int binder_open(struct inode *nodp,struct file *filp)
- {
- struct binder_proc*proc;
- proc= kzalloc(sizeof(*proc), GFP_KERNEL);
- if (proc == NULL)
- return -ENOMEM;
- get_task_struct(current);
- proc->tsk= current;
- INIT_LIST_HEAD(&proc->todo);
- init_waitqueue_head(&proc->wait);
- proc->default_priority= task_nice(current);
- mutex_lock(&binder_lock);
- binder_stats_created(BINDER_STAT_PROC);
- hlist_add_head(&proc->proc_node,&binder_procs);
- proc->pid= current->group_leader->pid;
- INIT_LIST_HEAD(&proc->delivered_death);
- filp->private_data= proc;
- mutex_unlock(&binder_lock);
- return 0;
- }
从binder驱动的初始化、binder设备的打这两个基本过程,可以看出binder驱动的开销非常小,引入binder所带来的开销,直到open()操作之后的状态点,都几乎可以被忽略不计。每次进入binder_open()函数之后,会对每个进程创建一个binder_proc数据结构,所以事实上每个使用Binder驱动的用户态进程都会得到一个binder_proc,而系统里所有的binder_proc都会挂载到binder_procs这个单链接之后。
在Android系统里, Binder几乎无论不在,比如基于IBinder的远程对象、基于IBinder的Parcel对象、进行系统Service管理的servicemanager进程、RemoteService或是Native Service,都是基于Binder驱动来进行通信的。对于servicemanager进程来说,我们可以看到Binder是通过binder.c里的函数来完成Binder驱动的读写;而其他情况,都会通过IPCThreadState对象的talkWithDriver()方法来完成Binder驱动的读写。
Binder本身本身并不支持直接read()/write()系统调用,而只使用ioctl()系统调用。在Unix系统里一般IPC通信机制中,可能使用多种多样的系统调用,再配合一大套的参数,以应付可能存在的多种编程需求。这样做当然有其历史原因,在长期的技术演进过程中,现存的IPC机制也证明它们是久经考验,有其存在的理由。但对于编程角度来说,这种历史对于编程是种负担,需要花过多的时间去研究这些API所造成的微妙影响,同时编程的可能性众多也给调试代码的行为制造了阻力。使用ioctl()则是比较符合Linux需求的方案,此时不会增强新的系统调用,保证了Linux内核的兼容性(Android也是唯一一个使用标准Linux内核+Android拓展的系统,其他系统则或多或少将Linux内核改得跟标准Linux运行环境不兼容。对于那些upstreaming做得比较好的平台来说,我们是可以使用标准Linux内核将Android系统运行起来的)。对于用户态来说,只使用ioctl()降低了不必要的灵活性,从而强化了访问内核时的一致性。同时,ioctl也简化了驱动,Binder驱动的总共代码行数在三千行左右,而能够提供类似IPC能力的Socket通信则需要超过五千多行代码来实现。
比如Socket的内核实现,需要实现的函数调用实现有:
- static conststruct file_operationssocket_file_ops = {
- .owner = THIS_MODULE,
- .llseek = no_llseek,
- .aio_read = sock_aio_read,
- .aio_write = sock_aio_write,
- .poll = sock_poll,
- .unlocked_ioctl = sock_ioctl,
- #ifdef CONFIG_COMPAT
- .compat_ioctl = compat_sock_ioctl,
- #endif
- .mmap = sock_mmap,
- .open = sock_no_open,
- .release = sock_close,
- .fasync = sock_fasync,
- .sendpage = sock_sendpage,
- .splice_write = generic_splice_sendpage,
- .splice_read = sock_splice_read,
- };
- static conststruct proto_ops unix_stream_ops = {
- .family= PF_UNIX,
- .owner= THIS_MODULE,
- .release= unix_release,
- .bind= unix_bind,
- .connect= unix_stream_connect,
- .socketpair= unix_socketpair,
- .accept= unix_accept,
- .getname= unix_getname,
- .poll= unix_poll,
- .ioctl= unix_ioctl,
- .listen= unix_listen,
- .shutdown= unix_shutdown,
- .setsockopt= sock_no_setsockopt,
- .getsockopt= sock_no_getsockopt,
- .sendmsg= unix_stream_sendmsg,
- .recvmsg= unix_stream_recvmsg,
- .mmap= sock_no_mmap,
- .sendpage= sock_no_sendpage,
- .set_peek_off= unix_set_peek_off,
- };
上述两个数据结构都与Socket编程相关,一个用于socket文件的操作,一个用于socket被打开之后的文件描述符的相关操作。这时,我们也可以理解Socket编程的痛苦之处,虽然完成的功能还不如Binder,但在编程上需要涉及socket()、bind()、listen()、connect()、send()、recv()等多个函数,同时,socket驱动至少也应该支持这些上层会调用到的这些函数。
Binder相对来说简单得多,只会是简单了open(),ioctl(),以及有支持servicemanager会使用到的mmap。于是,binder驱动里需要使用的系统调用非常简单:
- static conststruct file_operations binder_fops = {
- .owner= THIS_MODULE,
- .poll= binder_poll,
- .unlocked_ioctl= binder_ioctl,
- .mmap= binder_mmap,
- .open= binder_open,
- .flush= binder_flush,
- .release= binder_release,
- };
Binder驱动的核心 ---binder_ioctl()
在使用Binder驱动的地方,当打开/dev/binder设备之后,也就是运行我们前面看到的binder_open()驱动方法之后,就会调用下面的代码来进行Binder的读取:
- binder_write_read bwr;
- if(ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >=0)
- err = NO_ERROR;
- else
- err = -errno;
第一个参数是打开Binder的fd,第二个参数是Binder相关操作的IOCTL码,第三个参数则是使用读写的通用数据结构binder_write_read。
这样一个binder_write_read数据结构,就会通过ioctl()系统调用进入内核之后,同样会调用到binder_ioctl()来进行处理。binder_ioctl()会通过同样的三个参数,得到文件描述符、ioctl码和ioctl对应的参数,然后通过一条switch语言便可以处理这些相应的命令了。
- static long binder_ioctl(struct file *filp,unsigned int cmd,unsigned long arg)
- {
- ...
- switch (cmd) {
- caseBINDER_WRITE_READ: {
- ...
- break;
- }
- caseBINDER_SET_MAX_THREADS:
- ...
- break;
- caseBINDER_SET_CONTEXT_MGR:
- ...
- break;
- caseBINDER_THREAD_EXIT:
- ...
- break;
- case BINDER_VERSION:
- ...
- break;
- default:
- ret= -EINVAL;
- goto err;
- }
- }
在binder_ioctl()实现里,其他几个ioctl的操作都比较简单,只是读取一个用户态设置的参数,然后把相应的参数设置到binder驱动里,比如BINDER_VERSION,就只是返回binder驱动的当前的版本号。最重要的,使用最频繁的只是BINDER_WRITE_READ,这一ioctl会读取用户态传过来的值,或是把一个操作过的结果通过传入的binder_write_read数据结构写回到用户态。完整的BINDER_WRITE_READ处理如下:
- case BINDER_WRITE_READ: {
- struct binder_write_read bwr;
- if (size != sizeof(struct binder_write_read)) {
- ret= -EINVAL;
- goto err;
- }
- if(copy_from_user(&bwr, ubuf,sizeof(bwr))) {
- ret= -EFAULT;
- goto err;
- }
- if (bwr.write_size> 0) {
- ret= binder_thread_write(proc, thread, (void __user *)bwr.write_buffer, bwr.write_size,&bwr.write_consumed);
- if (ret < 0) {
- bwr.read_consumed= 0;
- if (copy_to_user(ubuf,&bwr,sizeof(bwr)))
- ret= -EFAULT;
- goto err;
- }
- }
- if (bwr.read_size >0) {
- ret= binder_thread_read(proc, thread, (void __user *)bwr.read_buffer, bwr.read_size, &bwr.read_consumed,filp->f_flags & O_NONBLOCK);
- if(!list_empty(&proc->todo))
- wake_up_interruptible(&proc->wait);
- if (ret < 0) {
- if (copy_to_user(ubuf,&bwr,sizeof(bwr)))
- ret= -EFAULT;
- goto err;
- }
- }
- if (copy_to_user(ubuf,&bwr, sizeof(bwr))) {
- ret= -EFAULT;
- goto err;
- }
- break;
- }
在BINDER_WRITE_READ的ioctl处理里,结构还是很清晰的,就是把用户态传递过来的binder_write_read数据结构从用户态拷贝到内核态,然后再根据这一数据结构里的read_size和write_size是否大于0来决定是否进行后续操作。由于这两个值都可能同时存在,于是一个ioctl周期里,有可能同时进行读写。
- 如果write_size大于0,说明用户态传递了数据过来,于是会后续调用binder_thread_write()来处理用户态写内核态读的操作;
- 如果read_size大于0,说明用户态期待从内核态读取某些状态,于是此时会调用binder_thread_read()来处理内核态提供给用户态读取的数据;
- 如果write_size和read_size都不大于0,此时binder交互无法进行,可被认为是出错了,于是返回错误。
无论是哪种情况,最后都会将内核态的binder_write_read结构体拷贝回用户态。值得注意的是,write_size或是read_size,是用户态告诉内核态当前可操作的内存区域有多大,则真正返回的结果会通过返回的binder_write_read结构体里的write_consumed或是read_consumed标明。
之后的代码则可以进入到binder_thread_write()或是binder_thread_read(),write和read都是相对于用户态来说的,thread也是,就是如果调度到合适的IPCThreadState对象,以线程方式处理用户态的写请求或是读请求。
Binder驱动的读写接口 -- binder_thread_write()与binder_thread_read()
Binder驱动在Binder IPC过程中的作用容易明了,它只是一种中间容器,把一个进程的请求,转发给另一个进程。出于这样的设计,binder_thread_write()的实现相对来说就比较简单,只是通过解析binder_ioctl()得到的BC_*命令,然后再根据命令作出处理。
- int binder_thread_write(struct binder_proc *proc,struct binder_thread *thread,
- void __user *buffer, int size, signed long *consumed)
- {
- uint32_tcmd;
- void __user *ptr =buffer + *consumed;
- void __user *end =buffer + size;
- while (ptr < end&& thread->return_error == BR_OK) {
- if (get_user(cmd,(uint32_t __user *)ptr))
- return -EFAULT;
- switch (cmd) {
- case BC_INCREFS:
- case BC_ACQUIRE:
- case BC_RELEASE:
- case BC_DECREFS:
- ...
- case BC_INCREFS_DONE:
- case BC_ACQUIRE_DONE:
- ...
- case BC_TRANSACTION:
- case BC_REPLY: {
- structbinder_transaction_data tr;
- if(copy_from_user(&tr, ptr,sizeof(tr)))
- return -EFAULT;
- ptr+= sizeof(tr);
- binder_transaction(proc,thread, &tr, cmd == BC_REPLY);
- break;
- }
- caseBC_REGISTER_LOOPER:
- thread->looper|= BINDER_LOOPER_STATE_REGISTERED;
- break;
- case BC_ENTER_LOOPER:
- thread->looper|= BINDER_LOOPER_STATE_ENTERED;
- break;
- case BC_EXIT_LOOPER:
- thread->looper|= BINDER_LOOPER_STATE_EXITED;
- break;
- caseBC_REQUEST_DEATH_NOTIFICATION:
- caseBC_CLEAR_DEATH_NOTIFICATION:
- ...
- case BC_DEAD_BINDER_DONE:
- ...
- break;
- default:
- ...
- *consumed= ptr - buffer;
- }
- return 0;
- }
出于效率的考虑,在binder_thread_write()函数里的写操作,几乎都是同步进行的。甚至在Binder驱动里来维护用户态数据的引用计数时(由于Binder交互总处于两个进程间,所以引用计数必须由Binder驱动来维护一份计数),也不会发生内存拷贝,是直接通过get_user()函数来验证用户态内存是否有效。这种方式也许放在其他环境下会不安全,但对于Binder来说却是合理的,因为处于引用计数下内存区必然会是有效的。
对于其他的BC_系列命令,处理只是直接完成即可,但对于附带数据的BC_TRANASACTION和BC_REPLY,因为有可能涉及到复杂数据结构的解析以及数据拷贝,于是会通过binder_transaction()来进行传输。从基本操作上来看,整个binder_transaction()操作,有点类似于Mailbox,就是把需要发送的数据直接找到目的,然后再归类整理好,等待接收者在它的读取周期里取走数据。而由于Binder驱动在传输时会使用transaction,这种具有原子性的事务概念,于是只有所有验证都通过,成功把数据传送到目标进程之后(不一定是会被接收处理,但发送操作必须成功之后,才有可能进入读取周期)接收方才能访问到这一传输的结果,于是只到最后的成功点,这一操作才会被挂载到目标进程的binder_thread的todo链表,使后续的读取周期里可以读出来。
binder_transaction()代码比较长,去除掉一些验证性代码后,其基本实现如下:
- static void binder_transaction(struct binder_proc *proc,
- struct binder_thread *thread,
- struct binder_transaction_data*tr,intreply)
- {
- structbinder_transaction *t;
- struct binder_work*tcomplete;
- size_t*offp, *off_end;
- struct binder_proc*target_proc;
- struct binder_thread*target_thread =NULL;
- struct binder_node*target_node = NULL;
- struct list_head*target_list;
- wait_queue_head_t*target_wait;
- structbinder_transaction *in_reply_to =NULL;
- structbinder_transaction_log_entry *e;
- uint32_treturn_error;
- if (reply) { 1
- in_reply_to= thread->transaction_stack;
- binder_set_nice(in_reply_to->saved_priority);
- thread->transaction_stack= in_reply_to->to_parent;
- target_thread= in_reply_to->from;
- target_proc= target_thread->proc;
- }else { 2
- if(tr->target.handle) { 3
- struct binder_ref*ref;
- ref= binder_get_ref(proc, tr->target.handle);
- target_node= ref->node;
- }else {
- target_node= binder_context_mgr_node;
- }
- e->to_node= target_node->debug_id;
- target_proc= target_node->proc;
- if (!(tr->flags& TF_ONE_WAY) && thread->transaction_stack) { 4
- structbinder_transaction *tmp;
- tmp= thread->transaction_stack;
- while (tmp) {
- if (tmp->from&& tmp->from->proc == target_proc)
- target_thread= tmp->from;
- tmp= tmp->from_parent;
- }
- }
- }
- if (target_thread) { 5
- e->to_thread= target_thread->pid;
- target_list= &target_thread->todo;
- target_wait= &target_thread->wait;
- }else {
- target_list= &target_proc->todo;
- target_wait= &target_proc->wait;
- }
- t= kzalloc(sizeof(*t), GFP_KERNEL); 6
- tcomplete= kzalloc(sizeof(*tcomplete), GFP_KERNEL);
- if (!reply &&!(tr->flags & TF_ONE_WAY)) 7
- t->from= thread;
- else
- t->from= NULL;
- t->sender_euid= proc->tsk->cred->euid;
- t->to_proc= target_proc;
- t->to_thread= target_thread;
- t->code= tr->code;
- t->flags= tr->flags;
- t->priority= task_nice(current);
- t->buffer= binder_alloc_buf(target_proc, tr->data_size,
- tr->offsets_size,!reply && (t->flags & TF_ONE_WAY)); 8
- if (t->buffer == NULL) {
- return_error= BR_FAILED_REPLY;
- gotoerr_binder_alloc_buf_failed;
- }
- t->buffer->allow_user_free= 0;
- t->buffer->debug_id= t->debug_id;
- t->buffer->transaction= t;
- t->buffer->target_node= target_node;
- if (target_node)
- binder_inc_node(target_node,1,0, NULL);
- offp= (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
- if(copy_from_user(t->buffer->data, tr->data.ptr.buffer,tr->data_size)) { 9
- binder_user_error("binder: %d:%d got transaction withinvalid "
- "data ptr\n",proc->pid, thread->pid);
- return_error= BR_FAILED_REPLY;
- gotoerr_copy_data_failed;
- }
- if(copy_from_user(offp, tr->data.ptr.offsets, tr->offsets_size)) {
- binder_user_error("binder: %d:%d got transaction withinvalid "
- "offsets ptr\n", proc->pid, thread->pid);
- return_error= BR_FAILED_REPLY;
- gotoerr_copy_data_failed;
- }
- off_end= (void*)offp + tr->offsets_size;
- for (; offp <off_end; offp++) { 10
- structflat_binder_object *fp;
- fp= (structflat_binder_object *)(t->buffer->data + *offp);
- switch (fp->type) {
- caseBINDER_TYPE_BINDER:
- caseBINDER_TYPE_WEAK_BINDER: { 11
- struct binder_ref*ref;
- struct binder_node*node = binder_get_node(proc, fp->binder);
- if (node == NULL) {
- node= binder_new_node(proc, fp->binder, fp->cookie);
- if (node == NULL) {
- return_error= BR_FAILED_REPLY;
- gotoerr_binder_new_node_failed;
- }
- node->min_priority= fp->flags & FLAT_BINDER_FLAG_PRIORITY_MASK;
- node->accept_fds= !!(fp->flags & FLAT_BINDER_FLAG_ACCEPTS_FDS);
- }
- ref= binder_get_ref_for_node(target_proc, node);
- if (fp->type ==BINDER_TYPE_BINDER)
- fp->type= BINDER_TYPE_HANDLE;
- else
- fp->type= BINDER_TYPE_WEAK_HANDLE;
- fp->handle= ref->desc;
- binder_inc_ref(ref,fp->type == BINDER_TYPE_HANDLE,
- &thread->todo);
- }break;
- caseBINDER_TYPE_HANDLE: 12
- caseBINDER_TYPE_WEAK_HANDLE: {
- struct binder_ref *ref= binder_get_ref(proc, fp->handle);
- if(ref->node->proc == target_proc) {
- if (fp->type ==BINDER_TYPE_HANDLE)
- fp->type= BINDER_TYPE_BINDER;
- else
- fp->type= BINDER_TYPE_WEAK_BINDER;
- fp->binder= ref->node->ptr;
- fp->cookie= ref->node->cookie;
- binder_inc_node(ref->node,fp->type == BINDER_TYPE_BINDER,0, NULL);
- }else {
- struct binder_ref*new_ref;
- new_ref= binder_get_ref_for_node(target_proc, ref->node);
- if (new_ref == NULL) {
- return_error= BR_FAILED_REPLY;
- gotoerr_binder_get_ref_for_node_failed;
- }
- fp->handle= new_ref->desc;
- binder_inc_ref(new_ref,fp->type == BINDER_TYPE_HANDLE,NULL);
- }
- }break;
- case BINDER_TYPE_FD: { 13
- int target_fd;
- struct file *file;
- file= fget(fp->handle);
- target_fd= task_get_unused_fd_flags(target_proc, O_CLOEXEC);
- task_fd_install(target_proc,target_fd, file);
- fp->handle= target_fd;
- }break;
- default:
- gotoerr_bad_object_type;
- }
- }
- t->work.type= BINDER_WORK_TRANSACTION; 14
- list_add_tail(&t->work.entry,target_list);
- tcomplete->type= BINDER_WORK_TRANSACTION_COMPLETE;
- list_add_tail(&tcomplete->entry,&thread->todo);
- if (target_wait)
- wake_up_interruptible(target_wait);
- return;
- }
- 同样是BC_命令,BC_TRANSACTION与BC_REPLY是不一样的,对于BC_REPLY,一般会是基于某个BR_TRANSACTION处理之后的结果,前面处理的transaction也会被保存到binder_thread的transaction_stack里。于是,对于BC_REPLY的处理,就会是通过transaction_stack,再基于它处理BC_REPLY请求。首先会设置进程优先级,返回值传输时的优先级总会由发送时来决定,于是这里会重设优先级。然后把transaction_stack弹出一级(之所有叫stack,会记录transaction的栈式关系),相当于函数调用上已经退出了,然后回退一次栈。最后会设置好target_thread和target_proc,为后续操作作好准备。
- 对于BC_TRANSACTION,不是基于某个已有的transaction,相反是要发起一个新的,会决定后续BC_REPLY操作的transaction。
- 如果有用户态传过来的binder_transaction包含target.handle,这说明是通过Proxy找到明确的操作目标,则会尝试在Binder驱动里查找该target.handle是否已经存在,找到则会直接把它作为操作目标。当然这时可能找不到,因为Binder必然是先有Service,然后才由Proxy引用,如果找不到这时就可以认为是非法访问,出错退出。如果用户态进程没有指定target.handle,这只能说明是通过servicemanager来查找Service的请求,会直接把目标设置为binder_context_mgr_node。
- 如果传输不是单向的,并且binder_thread的transaction_stack不为空,说明此时正在处理某个或者多个BC_TRANSACTION过程中。我们也知道栈式结构里栈顶永远会是最新的上下文现场,这不符合传输的实际需求。于是,这里会通过一次循环找到transaction_stack的目标符合的target_thread。这也有助于线程池操作,如果存在于transaction_stack中,则说明该transaction肯定处于某个处理过程中,运行这个处理过程的Binder线程必然无法被用于接收这一transaction,而主Binder线程按照同样的原则(命令交给处于栈底的Binder线程来读,交互则发生在栈顶的Binder线程里),总会是最“闲”的。
- 跟3的处理原理类似,如果找到目标binder_thread,则说明该线程是属于某个已经被访问过的Service的,直接更新target_list和target_wait信息。如果找不到,则根据目标的binder_proc来不更新这两个列表信息,此时则发生在ServiceManager访问或是Service的Binder线程没有就绪的情况,可以存起来等待就绪后再执行。
- 分配传输时,内核态需要使用的binder_transaction备份。所有经由内核而在两个进程间进行消息传输的数据,出于进程独立性考虑,必须两个用户进程空间内各一份,然后在内核态有一份,处理传输出错恢复或是重试。而另一个t_complete的申请,只是建立一个操作成功的链表而已。
- 根据用户态的当前信息,来更新内核的binder_transaction备份,并把查找到的target_thread和target_proc填入这一binder_transaction。
- 分配内核态binder_transaction所需要使用的存储空间,使用内部的binder_buffer来分配,这一空间源自于进程空间的mmap()得到的用户空间页面,这些页面会映射到内核态,所以也不会造成内核分配上的开销。
- 处于一个binder_transaction内的数据,都会保存在data.ptr.buffer的用户内存里,并通过用户内存里的另一段data.ptr.offsets来索引buffer里各个对象的拆分情况。于是这里会把这两部分内容通过copy_from_user()拷贝到内核态。
- 通过一次循环处理data.ptr.buffer里所包含的每个对象,当然,在传输到binder时,这些对象已经是被“压扁”了的对象,flat_binder_object。
- 如果对象的类型是Binder对象(WEAK_BINDER,有可能被自动回收的Binder对象),则尝试查找系统使用过的binder_node是否存在这样的对象,不存在则创建一个。然后给其加上引用计数。
- 如果对象类型是HANDLE(或是WEAK_HANDLE,有可能被自动回收的HANDLE对象),这时必须有提供者(Service)提供这一对象,不存在便出错退出。如果存在,并非target_proc与查找到的handle的proc一致,则将对象与handle关联起来,不一致时必然发生在对servicemanager的调用时,则尝试创建一起对handle的引用。
- 如果对象类型是文件描述符FD,则需要对文件本身的一些引用信息进行更新。
- 如果执行到这一步,证明该transaction就已经完成了,把它加入到target_thread的todo尾部,等待下一个binder_thread_read()周期的读取过程。因为加入链表是最后一步操作,于是传输过程里的原子性可以得到保证。
相对于简单直接的binder_thread_write(),binder_thread_read()函数所需要完成的工作则要复杂得多,是一个被动过程。写时操作总是同步写,可以同步返回操作成功与否的结果,但读操作则会是异步发生,用户态程序在某个状态点通过ioctl来监听read请求时,由Binder驱动主动通知用户态程序,BR_系列的命令总会是一系列内核态的线程化操作触发的。所以binder_thread_read()函数,会跟Socket编程时一样,通过复杂的状态检测之后才会通过BR_*命令返回某个线程化操作的结果,甚至会需要在无事可做时强迫用户态进程进入休眠。
binder_thread_read()的实现:
- static int binder_thread_read(struct binder_proc *proc,
- struct binder_thread *thread,
- void __user *buffer,int size,
- signed long *consumed, int non_block)
- {
- void __user *ptr =buffer + *consumed;
- void __user *end =buffer + size;
- int ret = 0;
- intwait_for_proc_work;
- if (*consumed == 0) { 1
- if (put_user(BR_NOOP,(uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- }
- retry: 2
- wait_for_proc_work= thread->transaction_stack == NULL &&
- list_empty(&thread->todo);
- if(thread->return_error != BR_OK && ptr < end) { 3
- if(thread->return_error2 != BR_OK) {
- if(put_user(thread->return_error2, (uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- if (ptr == end)
- goto done;
- thread->return_error2= BR_OK;
- }
- if(put_user(thread->return_error, (uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- thread->return_error= BR_OK;
- goto done;
- }
- thread->looper|= BINDER_LOOPER_STATE_WAITING; 4
- if(wait_for_proc_work)
- proc->ready_threads++;
- mutex_unlock(&binder_lock);
- if(wait_for_proc_work) {
- if(!(thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
- BINDER_LOOPER_STATE_ENTERED))){
- binder_user_error("binder: %d:%d ERROR: Thread waiting"
- "for process work before callingBC_REGISTER_"
- "LOOPER or BC_ENTER_LOOPER (state%x)\n",
- proc->pid,thread->pid, thread->looper);
- wait_event_interruptible(binder_user_error_wait,
- binder_stop_on_user_error < 2);
- }
- binder_set_nice(proc->default_priority);
- if (non_block) {
- if(!binder_has_proc_work(proc, thread))
- ret= -EAGAIN;
- }else
- ret= wait_event_interruptible_exclusive(proc->wait, binder_has_proc_work(proc,thread));
- }else {
- if (non_block) {
- if(!binder_has_thread_work(thread))
- ret= -EAGAIN;
- }else
- ret= wait_event_interruptible(thread->wait, binder_has_thread_work(thread));
- }
- mutex_lock(&binder_lock);
- if(wait_for_proc_work)
- proc->ready_threads--;
- thread->looper&= ~BINDER_LOOPER_STATE_WAITING;
- if (ret)
- return ret;
- while (1) { 5
- uint32_tcmd;
- structbinder_transaction_data tr;
- struct binder_work *w;
- structbinder_transaction *t =NULL;
- if(!list_empty(&thread->todo)) 6
- w= list_first_entry(&thread->todo, struct binder_work, entry);
- else if(!list_empty(&proc->todo) && wait_for_proc_work)
- w= list_first_entry(&proc->todo, struct binder_work, entry);
- else {
- if (ptr - buffer == 4 &&!(thread->looper & BINDER_LOOPER_STATE_NEED_RETURN)) /* no data added */
- goto retry; 7
- break;
- }
- if (end - ptr < sizeof(tr) + 4)
- break;
- switch (w->type) { 8
- caseBINDER_WORK_TRANSACTION: { 9
- t= container_of(w, struct binder_transaction, work);
- }break;
- caseBINDER_WORK_TRANSACTION_COMPLETE: { 10
- cmd= BR_TRANSACTION_COMPLETE;
- if (put_user(cmd,(uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- list_del(&w->entry);
- kfree(w);
- }break;
- case BINDER_WORK_NODE:{ 11
- struct binder_node*node = container_of(w,struct binder_node, work);
- uint32_tcmd = BR_NOOP;
- const char *cmd_name;
- int strong =node->internal_strong_refs || node->local_strong_refs;
- int weak =!hlist_empty(&node->refs) || node->local_weak_refs || strong;
- if (weak &&!node->has_weak_ref) {
- cmd= BR_INCREFS;
- cmd_name= "BR_INCREFS";
- node->has_weak_ref= 1;
- node->pending_weak_ref= 1;
- node->local_weak_refs++;
- }else if (strong &&!node->has_strong_ref) {
- cmd= BR_ACQUIRE;
- cmd_name= "BR_ACQUIRE";
- node->has_strong_ref= 1;
- node->pending_strong_ref= 1;
- node->local_strong_refs++;
- }else if (!strong &&node->has_strong_ref) {
- cmd= BR_RELEASE;
- cmd_name= "BR_RELEASE";
- node->has_strong_ref= 0;
- } else if (!weak &&node->has_weak_ref) {
- cmd= BR_DECREFS;
- cmd_name= "BR_DECREFS";
- node->has_weak_ref= 0;
- }
- if (cmd != BR_NOOP) {
- if (put_user(cmd,(uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- if(put_user(node->ptr, (void * __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(void *);
- if(put_user(node->cookie, (void * __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(void *);
- }else {
- list_del_init(&w->entry);
- if (!weak &&!strong) {
- rb_erase(&node->rb_node,&proc->nodes);
- kfree(node);
- binder_stats_deleted(BINDER_STAT_NODE);
- }
- }
- }break;
- caseBINDER_WORK_DEAD_BINDER:
- caseBINDER_WORK_DEAD_BINDER_AND_CLEAR:
- caseBINDER_WORK_CLEAR_DEATH_NOTIFICATION: {
- structbinder_ref_death *death;
- uint32_tcmd;
- death= container_of(w, struct binder_ref_death, work);
- if (w->type ==BINDER_WORK_CLEAR_DEATH_NOTIFICATION)
- cmd= BR_CLEAR_DEATH_NOTIFICATION_DONE;
- else
- cmd= BR_DEAD_BINDER;
- if (put_user(cmd,(uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- if(put_user(death->cookie, (void * __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(void *);
- if (w->type ==BINDER_WORK_CLEAR_DEATH_NOTIFICATION) {
- list_del(&w->entry);
- kfree(death);
- binder_stats_deleted(BINDER_STAT_DEATH);
- }else
- list_move(&w->entry,&proc->delivered_death);
- if (cmd ==BR_DEAD_BINDER)
- goto done;
- }break;
- }
- if (!t)
- continue;
- BUG_ON(t->buffer== NULL);
- if(t->buffer->target_node) { 12
- struct binder_node*target_node = t->buffer->target_node;
- tr.target.ptr= target_node->ptr;
- tr.cookie= target_node->cookie;
- t->saved_priority= task_nice(current);
- if (t->priority< target_node->min_priority &&
- !(t->flags & TF_ONE_WAY))
- binder_set_nice(t->priority);
- else if (!(t->flags& TF_ONE_WAY) ||
- t->saved_priority >target_node->min_priority)
- binder_set_nice(target_node->min_priority);
- cmd= BR_TRANSACTION;
- }else {
- tr.target.ptr= NULL;
- tr.cookie= NULL;
- cmd= BR_REPLY;
- }
- tr.code= t->code;
- tr.flags= t->flags;
- tr.sender_euid= t->sender_euid;
- if (t->from) { 13
- struct task_struct*sender = t->from->proc->tsk;
- tr.sender_pid= task_tgid_nr_ns(sender,
- current->nsproxy->pid_ns);
- }else {
- tr.sender_pid= 0;
- }
- tr.data_size= t->buffer->data_size;
- tr.offsets_size= t->buffer->offsets_size;
- tr.data.ptr.buffer= (void*)t->buffer->data +
- proc->user_buffer_offset;
- tr.data.ptr.offsets= tr.data.ptr.buffer +
- ALIGN(t->buffer->data_size,
- sizeof(void *));
- if (put_user(cmd,(uint32_t __user *)ptr))
- return -EFAULT;
- ptr+= sizeof(uint32_t);
- if (copy_to_user(ptr,&tr, sizeof(tr))) 14
- return -EFAULT;
- ptr+= sizeof(tr);
- list_del(&t->work.entry);
- t->buffer->allow_user_free= 1;
- if (cmd ==BR_TRANSACTION && !(t->flags & TF_ONE_WAY)) {
- t->to_parent= thread->transaction_stack;
- t->to_thread= thread;
- thread->transaction_stack= t;
- }else {
- t->buffer->transaction= NULL;
- kfree(t);
- binder_stats_deleted(BINDER_STAT_TRANSACTION);
- }
- break;
- }
- done: 15
- *consumed= ptr - buffer;
- if(proc->requested_threads + proc->ready_threads ==0 &&
- proc->requested_threads_started <proc->max_threads &&
- (thread->looper &(BINDER_LOOPER_STATE_REGISTERED |
- BINDER_LOOPER_STATE_ENTERED))
- proc->requested_threads++;
- if(put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))
- return -EFAULT;
- }
- return 0;
- }
- 如果此时,consumed为零,则会是一个新的读写周期,应该忽略掉4字节的命令信息。否则,此时可认为是借用前面某一些正在进行中的Binder读写周期,ptr指向的会正在需要填写的区域。
- retry标记一般用来做循环,以降低while使用上的混乱。当我们需要在当前读写周期里进行重试时,都会跳转到retry重新开始检测,比如在休眠中被唤醒,但又无事可做就需要再次进入休眠等待。
- 任何的出错信息,都可以直接返回,不需要再进行后续的操作,比如thread的return_error与return_error2两个标识,以及内存越界等情况。
- thread->loop是一个标识位,标明现在Binder当前使用的内核线程处于什么操作过程中。这是一个按位来标记状态的变量,于是可能会出现多位同时存在的情况。在这里,会先置上BINDER_LOOPER_STATE_WAITING位,然后检查是否应该进入休眠中,比如用户态进程没有注册或是调用ENTER_LOOP来驱动循环监听,或是没有Binder操作等情况下,都会通过wait_event_interruptible()系列函数进入休眠。需要注意是,这里会通过一个binder_lock来上锁,保护binder驱动的可重入性,于是这里会在休眠前解锁,从休眠函数退出后马上又继续上锁。当退出休眠时,会将BINDER_LOOPER_STATE_WAITING位清空。
- 通过循环来进行读操作。跟Socket的读取一端的实现一样,外围循环进行读取重试,内层循环来保证读取到一个完整的包。在Binder的读过程里,这样的循环也是必要,因为是多线程处理,在外围循环里可以检测到有任务在进行中,而发回发馈前是必须通过循环来得到完整的处理结果的。
- 通过检查thread->todo和proc->todo来判断是否在Binder命令需要给出反馈。
- 如果包仅包含一个4字节命令,而又没有设置需要马上返回操作状态,此时会尝试重试,争取在这个读周期里加载更多内容。
- 根据读取回来不同任务类型来进行具体的返回操作。
- 如果任务是BINDER_WORK_TRANSACTION,则该反馈需要进一步的处理,会尝试在此在todo列表里取出该任务
- 如果任务是BINDER_WORK_TRANSACTION_COMPLETE,说明某个TRANSACTION操作已经完成,于是通过BR_TRANSACTION_COMPLETE命令来通知用户态进程。
- 如果任务是BINDER_WORK_NODE,则说明该任务只是修改某个引用对象属性的操作,于是通过内部索引找到该对象的引用,加以修改,然后再结果通过BR_系列命令返回。此时会产生BR_INCREFS、BR_ACQUIRE、BR_RELEASE、BR_DECREFS四种回馈,当然没有任何相当操作,则是返回BR_NOOP。如果任务是BINDER_WORK_DEAD_BINDER之类的,也会是比较简单的列表操作,并将结果返回。
- 处理完任务相关类型的判断,此时基本上可以认为非线程化操作都已经完成了,而需要对线程化操作过的TRANSACTION的返回过程进行处理。如果步骤9取回来的transaction为空,则证明TRANSACTION操作还不成功,于是会继续循环等待其操作成功。如果transaction不为空,则会通过transaction所标明的信息,进行返回后处理,将TRANSACTION任务的操作结果存储起来,指定返回命令是BR_TRANSACTION,并且根据不同传输类型来修改调度优先级。如果是单向传输并且传输时提供的优先级又低于传输目标的最低优先级需求时,使用传输指定的优先级;如果是双向传输,传输时指定的优先级大于传输目标的最低优先级需求,设置该次调度使用传输目标的最低优先级。这样,Binder返回过程总会处于一个比较低的进程优先级状态下。最后,如果transaction任务所指向的操作目标t->buffer->target_node为空,则该操作只是用于返回操作结果,此时不再拷贝数据,只以BR_REPLY返回操作结果。
- 如果t->from不为空,则可以查找到发送者的相关信息,于是将发送进程的pid信息填入binder_transaction_data。如果为空,则肯定是由binder驱动发出的消息,pid被置为0。注意这时的使用的pid,由于Linux是使用的1:1映射的线程实现,所有使用的会是tgid。当然,t->buffer指向的操作结果,也将保存到binder_transaction_data里。
- 将通过binder_transaction处理得到的binder_transaction_data数据结构拷贝回用户态。到这一步,针对TRANSACTION命令的相关读操作也就完成了。我们可以注意到binder_transaction_data数据结构的类型,它跟servicemanager进程使用的binder_txn数据结构一致,而IPCThreadState也是使用同一binder_transaction_data来与binder驱动交互,于是这样的消息体就可用于用户态与Binder驱动之间交换大量数据了。
- 最后,就是返回结果,结束这次binder_thread_read()的调用。最后的操作一个是更新consumed计数,另一个是当线程池无法处理当前操作时,通过一个BR_SPAWN_LOOP来创建新的IPC线程。
通过对binder_thread_write()和binder_thread_read()的分析,我们可以知道binder在处理简单命令交互,以及在transaction完成之后的后处理是如何完成的。通过binder_ioctl()函数提供的命令,再经由上述两个函数的处理,最终可以达到一个进程发起的请求,可交由另一个进程来进行响应。
另外,Binder驱动实现还包括binder_mmap()的实现,binder_poll()的实现。binder_mmap()实际上是一个假实现,它的目的,只是用于在用户态内存空间里创建虚址空间,存放binder传输所需要的buffer,所以虽然说是copy_from_user(),但对于大量数据的读写,也还是发生在用户进程的内存空间里,详情见binder_buffer相关的代码。而binder_poll(),则是用于poll方式来访问binder驱动,这在Android系统里见不到类似的用法。
总结 --- 独特的Binder驱动
总结Binder驱动,几乎可以说,通过binder_ioctl(),就得到了完整的binder实现。整个Binder驱动,在内部实现上就可以证实我们前面提供的各种优点和缺点:
1) 高效,相对与其他IPC,在很多情况下Binder可以减小内存拷贝的开销,而且由于代码精练,又使用了红黑树等算法来索引,Binder必然得到比较高的的性能。
2) 面向对象,整个Binder驱动,只是libbinder的一种辅助手段,于是会继承libbinder实现上的面向对象的特点。而Binder驱动对于数据传输时的处理,从里到外,都具有面向对象的特点,甚至内部还会使用一个work_queue来完成自动垃圾回收的功能
3) 开销小。Binder驱动在不使用时,所造成的开销几乎可以忽略不记。对于每次数据的读写操作,Binder驱动都是通过binder_transcation这样的元数据来记录,只记录数据变动,开销也降到了最低。而对于大数据量的读写,使用从用户空间“偷”来的内存空间,只针对进程有效,不会影响到整个系统的内存分配,只有一些内部维护的开销。
4) 面向进程。Binder IPC传输的本质便是面向进程,这点可以从binder_transaction的结构构成中可以看出来,而只使用进程的用户空间来处理大数据量buffer,也进一步加强binder的进程化。如果处于binder传输过程中的进程出错死掉,则完全不会带来系统级的开销。
5) 简洁。虽然实现了这么复杂的功能,但代码实现上还是比较简洁的。当然,也有过于简洁的毛病,Binder不拥有Linux内核在实现上的风格,通过代码自我解释,需要结合libbinder的实现才能理解Binder驱动里进行的到底是什么处理。
最后,我们可以拿Linux世界里现有的一些特性来说明Binder的唯一性和优越性。如果拿Socket跟Binder作对比,在性能上也许不会高出多少,还导致了无法跨平台,但对于内存使用、面向对象等特点,则足以让Socket汗颜。如果拿Linux世界里另一个具备面对对象能力,在Android系统也会被用到的D-Bus来跟Binder作对比,D-Bus需要由用户态进程来完成分发,这一条就会使用Binder在内存使用和性能上远超D-bus。
事实上,Socket、D-bus、Binder在Android都有被用到,但前两者只是用于与传统编程环境兼容,比如BlueZ使用D-bus通信,而RIL或是Zygote则会使用Socket(在Zygote之前,Binder的通信环境还不成熟)。
Socket | Binder | Dbus | |
访问方式 | FD句柄 | PID | FD | PID |
读写接口 | Stream I/O | ioctl | 消息队列 |
网络支持 | 支持 | 不支持 | 不支持 |
性能 | 一般 | 很好 | 很差 |
统一访问接口 | 否 | 是 | 是 |
可拓展性 | 否 | 是 | 是 |
从这样的对象可以看出,Binder与Socket相比,除了不支持网络,Binder完胜。由于面向对象,使Binder在接口统一,可拓展性上更具优势,而比较讽刺的是,在这种面向对象条件下,Binder性能还比Socket要高,我们从Binder驱动里也看到过了,Binder驱动在节省内存拷贝、简化通讯处理上下足了功夫。
而D-bus则刚好相反,它提供与Binder类似的能力,但它只是用户态的一层IPC“皮”,是将IPC机制进行了用户态的面向对象封装。每次D-bus消息传输都有可能通过多次进程切换来完成分发,性能极低,在桌面系统里或许可以承受,却不是嵌入式环境里可以容忍的,基于D-bus的嵌入式Linux解决方案,一般都有性能或是用户体验不佳的通病,比如Meego。