活页夹是Android中主要的IPC / RPC(进程间通信)系统。 它允许应用程序彼此通信,并且它是Android环境中几种重要机制的基础。 例如,Android服务是建立在Binder之上的。 与Binder交换的消息称为活页夹事务 ,它们可以传输简单数据(例如整数),但也可以处理更复杂的结构,例如文件描述符,内存缓冲区或对象上的弱/强引用。 Internet上有很多有趣的Binder文档,但是关于如何将消息从一个进程转换到另一个进程的细节很少。 本文试图描述Binder如何处理消息以及如何在不同进程之间执行复杂对象(文件描述符,指针)的转换。 为此,将从用户区到绑定程序内核进行绑定程序事务。
USERLAND中的活页夹
在探索Binder内核模块如何工作之前,让我们看一下在调用Android Service的情况下如何在用户区中准备事务。
Android服务概述
服务是在后台运行并为其他应用程序提供功能的Android组件。 其中一些是Android框架的一部分,但已安装的应用程序也可以公开其自身的功能。 当应用程序要公开新服务时,它首先注册到“服务管理器” (1) ,其中包含并更新所有正在运行的服务的列表。 稍后,客户端向处理程序请求ServiceManager (2)与该服务进行通信,并能够调用公开的函数(3) 。
服务互动
从Android 8.0开始,存在三个不同的Binder域。 每个域都有其自己的服务管理器,并且可以通过/dev/
的相应设备进行访问。 下表描述了每个Binder域有一个设备:
绑定器域,摘自Android文档网站
为了使用活页夹系统,一个过程需要打开这些设备之一并执行一些初始化步骤,然后再发送或接收活页夹交易。
准备活页夹交易
Android框架在活页夹设备顶部包含多个抽象层。 通常,当开发人员实施新服务时,他们会描述以高级语言公开的接口。 在框架应用程序的情况下,描述是使用AIDL语言编写的,而由供应商开发的硬件服务则具有以HIDL语言编写的接口描述。 这些描述被编译到Java / C ++文件中,在其中使用Parcel组件对参数进行反序列化。 生成的代码包含两个类,一个Binder Proxy和一个Binder Stub 。 代理类用于请求远程服务,而存根则用于接收传入呼叫,如下图所示。
黏合剂层
在最低级别,使用域对应的设备将应用程序连接到Binder内核模块。 他们使用ioctl
syscall来发送和接收绑定程序消息。
序列化步骤使用Parcel类完成,该类提供了在Binder消息中读取和写入数据的功能。 有两种不同的宗地类:
/dev/binder
和/dev/vndbinder
域基于AIDL描述语言,并使用在frameworks/native/include/binder/Parcel.h
定义的Parcel。 这个包裹允许发送基本类型和文件描述符 。 例如,以下代码摘自命令SHELL_COMMAND_TRANSACTION
的默认代理实现。 该命令准备并写入远程服务使用的标准输入,输出和错误流的文件描述符。
//从frameworks / base / core / java / Android / os / Binder.java中提取 public void shellCommand (输入 FileDescriptor ,输出 FileDescriptor , FileDescriptor err , 字符串 [] args , ShellCallback 回调 , ResultReceiver resultReceiver ) 引发 RemoteException { 宗地 数据 =宗地 。 获得 (); 包裹 回复 = 包裹 。 获得 (); 数据 。 writeFileDescriptor ( in ); 数据 。 writeFileDescriptor ( out ); 数据 。 writeFileDescriptor ( err ); 数据 。 writeStringArray ( args ); ShellCallback 。 writeToParcel ( callback , data ); resultReceiver 。 writeToParcel ( data , 0 ); 尝试 { 交易 ( SHELL_COMMAND_TRANSACTION , data , 回复 , 0 ); 回覆 。 readException (); } 最后 { 数据 。 回收 (); 回覆 。 回收 (); } }
/dev/hwbinder
域使用在先前的基础上在system/libhwbinder/include/hwbinder/Parcel.h
实现的另一个Parcel。 这种Parcel实现允许发送数据缓冲区,例如C结构。 数据缓冲区可以嵌套,并包含指向其他结构的指针。 在以下示例中,结构hild_memory
结构包含一个嵌入式结构(hild_string
)和一个内存指针(mHandle
):
//从system / libhidl / transport / include / hidl / HidlBinderSupport.h中提取 // ---------------------- hidl_memory status_t readEmbeddedFromParcel ( const hidl_memory & memory, const Parcel & parcel, size_t parentHandle, size_t parentOffset); status_t writeEmbeddedToParcel ( const hidl_memory & memory, Parcel * parcel, size_t parentHandle, size_t parentOffset); // [...] //从system / libhidl / base / include / hidl / HidlSupport.h中提取 struct hidl_memory { // ... 私人的 : hidl_handle mHandle __attribute__ ((aligned( 8 ))); uint64_t mSize __attribute__ ((aligned( 8 ))); hidl_string mName __attribute__ ((aligned( 8 ))); };
这两种Parcel能够发送文件描述符和带有内存地址的复杂数据结构。 因为这些元素包含特定于调用者进程的数据,所以Parcel组件将绑定对象写入事务消息中。
活页夹对象
除了简单类型(字符串,整数等)之外,还可以发送绑定对象。 活页夹对象是一种类型值为以下之一的结构:
//摘自:drivers / staging / Android / uapi / binder.h 枚举 { BINDER_TYPE_BINDER = B_PACK_CHARS( 's' , 'b' , '*' , B_TYPE_LARGE), BINDER_TYPE_WEAK_BINDER = B_PACK_CHARS( 'w' , 'b' , '*' , B_TYPE_LARGE), BINDER_TYPE_HANDLE = B_PACK_CHARS( 's' , 'h' , '*' , B_TYPE_LARGE), BINDER_TYPE_WEAK_HANDLE = B_PACK_CHARS( 'w' , 'h' , '*' , B_TYPE_LARGE), BINDER_TYPE_FD = B_PACK_CHARS( 'f' , 'd' , '*' , B_TYPE_LARGE), BINDER_TYPE_FDA = B_PACK_CHARS( 'f' , 'd' , 'a' , B_TYPE_LARGE), BINDER_TYPE_PTR = B_PACK_CHARS( 'p' , 't' , '*' , B_TYPE_LARGE), };
下面是一个类型为BINDER_TYPE_PTR
的活页夹对象的BINDER_TYPE_PTR
:
structinder_object_header { __u32 类型; }; structinder_buffer_object { 结构 binder_object_header hdr; __u32 标志; binding_uintptr_t 缓冲区; binding_size_t 长度; binding_size_t 父对象; 活页夹大小 };
hdr
下方的属性是特定于类型的。
不同的活页夹对象可以描述如下:
- BINDER_TYPE_BINDER和BINDER_TYPE_WEAK_BINDER :这些类型是对本地对象的强引用和弱引用。
- BINDER_TYPE_HANDLER和BINDER_TYPE_WEAK_HANDLE :这些类型是对远程对象的强引用和弱引用。
- BINDER_TYPE_FD :此类型用于发送文件描述符号。 这通常用于发送ashmem共享内存以传输大量数据。 实际上,活页夹交易消息被限制为1 MB。 但是,可以使用任何文件描述符类型(文件,套接字,标准输入等)。
- BINDER_TYPE_FDA :描述文件描述符数组的对象。
- BINDER_TYPE_PTR :用于使用内存地址及其大小发送缓冲区的对象。
当Parcel类编写缓冲区或文件描述符时,它将在数据缓冲区中添加活页夹对象(图上为蓝色)。 活页夹对象和简单类型混合在数据缓冲区中。 每次写入对象时,其相对位置都会插入到偏移缓冲区中(紫色)。
活页夹消息缓冲区和偏移量
一旦data
和offets
缓冲区已满,就准备将binder_transaction_data
传递给内核。 我们可以注意到它包含上述指针,数据缓冲区和偏移量数组的大小。 字段handler
用于设置目标过程,该过程是先前由服务管理器检索的。 另一个有趣的属性是code
,其中包含要执行的远程服务的方法ID。
//文件:development / ndk / platforms / android-9 / include / linux / binder.h structinder_transaction_data { 工会 { size_t 句柄; 无效 * ptr; } 目标; 无效 * cookie; 未签名的 int 代码; unsigned int 标志; pid_t sender_pid; uid_t sender_euid; size_t data_size; size_t offsets_size; 工会 { 结构 { const void * buffer; const void * 偏移量; } ptr; uint8_t buf [ 8 ]; } 数据; };
在调用ioctl之前,必须填充最后一个结构( binder_write_read
)。 它包含读写命令缓冲区,并指向上一个缓冲区:
//文件:development / ndk / platforms / android-9 / include / linux / binder.h structinder_write_read { 签名 长 write_size; 签名 长 write_consumed; 无符号 长 write_buffer; 签名 长 read_size; 签名 长 read_consumed; 无符号 长 read_buffer; };
发送活页夹事务所需的数据结构可以用下面的图总结:
binding_write_read结构
我们可以注意到, write_buffer
并不直接指向binder_transaction_data
结构。 它以命令标识符为前缀。 如果是交易,则值为BC_TRANSACTION_SG
。
请注意,除了BC_TRANSACTION_SG
以外, BC_TRANSACTION_SG
存在许多命令,例如BC_ACQUIRE
和BC_RELEASE
以增加或减少强处理程序,或者在停止远程服务时会注意到BC_REQUEST_DEATH_NOTIFICATION
。
现在所有人都准备好执行绑定程序事务,调用者需要使用命令BINDER_WRITE_READ
调用ioctl
,内核模块将处理该消息并转换目标进程的所有绑定程序对象:强/弱处理程序,文件描述符和缓冲区。
在下一部分中,让我们继续在内核方面进行分析!
活页夹内核模块
现在,调用者进程已准备好其数据并执行了一个ioctl来发送事务。 所有活页夹对象都将被翻译,并且消息将被复制到目标内存中。
用于ioctl
的命令由binder_ioctl_write_read
函数处理,该函数执行数据参数的安全复制。
//文件:drivers / android / binder.c 静态 长 binder_ioctl ( 结构 文件 * filp, 无符号 int cmd, 无符号 长 arg) { // [...] 开关 (cmd) { 情况 BINDER_WRITE_READ: ret = 活页夹_ioctl_write_read(filp, cmd, arg, thread ); 如果 (ret) 转到 错误; 休息 ;
//文件:drivers / android / binder.c 静态 整数 binder_ioctl_write_read ( 结构 文件 * filp, unsigned int cmd, unsigned long arg, struct binder_thread * 线程 ) { // [...] 如果 (copy_from_user( & bwr, ubuf, sizeof (bwr))) { ret = -EFAULT; 出去 } // [...] 如果 (bwr.write_size > 0 ) { ret = binder_thread_write(proc, thread , bwr.write_buffer, bwr.write_size, & bwr.write_consumed);
在写事务的情况下,将调用函数binder_thread_write
,然后将与事务关联的命令调度到相应的处理程序。
//文件:drivers / android / binder.c 开关 (cmd) { 情况 BC_INCREFS: 情况 BC_ACQUIRE: 情况 BC_RELEASE: 情况 BC_DECREFS: // [...] 情况 BC_TRANSACTION_SG: 案例 BC_REPLY_SG: { struct binder_transaction_data_sg tr; 如果 (copy_from_user( & tr, ptr, sizeof (tr))) 返回 -EFAULT; ptr + = sizeof (tr); binding_transaction(proc, thread 和 tr.transaction_data, cmd == BC_REPLY_SG, tr.buffers_size); 休息 ; } // [...]
对于命令BC_TRANSACTION_SG
,在userland中准备的binder_transaction_data缓冲区由binder_transaction
函数处理。
活页夹交易
binder_transaction
函数位于文件drivers/staging/Android/binder.c
。
这个重要的功能执行以下任务:在目标进程中(在活页夹保留的内存中)分配一个缓冲区,验证所有数据对象并执行转换,在目标内存进程中复制数据和偏移缓冲区。
为了验证活页夹对象,内核查看包含所有对象相对位置的offsets
缓冲区。 取决于对象类型,内核执行不同的转换。
//文件:drivers / android / binder.c 静态 void binder_transaction ( struct binder_proc * proc, struct binder_thread * 线程 , struct binder_transaction_data * tr, int 回复, binding_size_t extra_buffers_size){ // [...] // bind_transaction函数中的对象验证。 // offp是指向偏移量缓冲区的指针 for (; offp < off_end; offp ++ ) { struct binder_object_header * hdr; size_t object_size = 活页夹验证对象(t- > buffer, * offp); 如果 (object_size == 0 || * offp < off_min) { bindingr_user_error( “%d:%d获得了具有无效偏移量(%lld,最小%lld最大%lld)或对象的事务。 \ n ” , proc- > pid, 线程 -> pid, (u64) * offp, (u64)off_min, (u64)t- > 缓冲区 -> data_size); return_error = BR_FAILED_REPLY; return_error_param = -EINVAL; return_error_line = __LINE__; 转到 err_bad_offset; } hdr = ( struct binder_object_header * )(t- > 缓冲区 -> 数据 + * offp); off_min = * offp + object_size; 开关 (hdr- > type) { 情况 BINDER_TYPE_BINDER: 案例 BINDER_TYPE_WEAK_BINDER: { // [..]验证和翻译 案例 BINDER_TYPE_HANDLE: 案例 BINDER_TYPE_WEAK_HANDLE: { // [..]验证和翻译 } 案例 BINDER_TYPE_FD:{ // [..]验证和翻译 } 案例 BINDER_TYPE_FDA:{ // [..]验证和翻译 } 案例 BINDER_TYPE_PTR: { // [..]验证和翻译 }
弱/强粘结剂/处理机
活页夹对象引用可以是指向本地对象的虚拟内存地址(活页夹引用),也可以是标识另一个进程的远程对象的处理程序(处理程序引用)。
当内核获取对象引用(本地或远程)时,它将更新内部表,该表包含每个进程的真实虚拟内存地址和处理程序(binder <=>处理程序)之间的映射。
有两种翻译:
- 将虚拟内存地址转换为处理程序:
binder_translate_binder
- 将处理程序转换为虚拟内存地址:
binder_translate_handle
Binder内核模块保留共享对象的引用计数。 与新进程共享引用时,其计数器值将增加。 当不再使用参考时,将通知所有者并可以释放它。
活页夹->处理程序翻译
//文件:drivers / android / binder.c 静态 int binding_translate_binder ( struct flat_binder_object * fp, struct binder_transaction * t, struct binder_thread * 线程 ) { // [...] 节点 = binder_get_node(proc,fp- > 粘合剂); if ( ! 节点) { 节点 = binder_new_node(proc, fp); 如果 ( ! 节点) 返回 -ENOMEM; } 如果 (fp- > cookie != 节点 -> cookie) { // [...]错误 } // SELinux检查 如果 (security_binder_transfer_binder(proc- > tsk, target_proc- > tsk)) { // [...]错误 } ret = binder_inc_ref_for_node(target_proc, 节点, fp- > hdr.type == BINDER_TYPE_BINDER, & 线程 -> todo 和& rdata); 如果 (ret) 完成 如果 ( fp- > hdr.type == BINDER_TYPE_BINDER) fp- > hdr.type = BINDER_TYPE_HANDLE; 其他 fp- > hdr.type = BINDER_TYPE_WEAK_HANDLE; fp- > 粘结剂 = 0 ; fp- > handle = rdata.desc; fp- > cookie = 0 ; // [..] }
该函数获取与绑定器值(虚拟地址)对应的节点,或者如果不存在该节点,则创建一个新节点。 此节点在本地对象和远程对象( rdata.desc
)之间具有关联。 在SELinux安全检查之后,引用计数器将增加,并且绑定程序对象中的引用值将更改,并由引用处理程序替换。
处理程序->活页夹翻译
//文件:drivers / android / binder.c 静态 整数 binder_translate_handle ( 结构 flat_binder_object * fp, struct binder_transaction * t, struct binder_thread * 线程 ) { // [...] 节点 = binder_get_node_from_ref(proc,fp- > 句柄, fp- > hdr.type == BINDER_TYPE_HANDLE 和 src_rdata); if ( ! 节点) { // [...]错误 } // SELinux安全检查 如果 (security_binder_transfer_binder(proc- > tsk, target_proc- > tsk)) { ret = -EPERM; 完成 } binding_node_lock(node); if ( node- > proc == target_proc) { 如果 ( fp- > hdr.type == BINDER_TYPE_HANDLE) fp- > hdr.type = BINDER_TYPE_BINDER; 其他 fp- > hdr.type = BINDER_TYPE_WEAK_BINDER; fp- > 粘合剂 = 节点 -> ptr; fp- > cookie = 节点 -> cookie; // [...] binding_inc_node_nilocked(node, fp- > hdr.type == BINDER_TYPE_BINDER, 0 , NULL); // [...] } 其他 { struct binder_ref_data dest_rdata; ret = binder_inc_ref_for_node(target_proc, 节点, fp- > hdr.type == BINDER_TYPE_HANDLE, NULL & dest_rdata); // [...] fp- > 粘结剂 = 0 ; fp- > handle = dest_rdata.desc; fp- > cookie = 0 ; } 完成: binding_put_node(node); 返回 ret }
此翻译功能与上一个功能非常相似。 但是,我们可以注意到,处理程序引用可以在不同的过程之间共享。 如果目标进程与节点匹配,则仅在绑定程序引用中转换处理程序引用。
文件描述符
当联编程序对象类型为BINDER_TYPE_FD或BINDER_TYPE_FDA时,内核需要检查文件描述符是否正确(与打开的struct文件相关联)并在目标进程中将其复制。 翻译是由binder_translate_fd
函数完成的。 详情如下:
//文件:drivers / android / binder.c 静态 整数 bind_translate_fd ( 整数 fd, struct binder_transaction * t, struct binder_thread * 线程 , struct binder_transaction * in_reply_to) { // [...] // 1:检查目标是否允许文件描述符 如果 (in_reply_to) target_allows_fd = !! (in_reply_to- > 标志 和 TF_ACCEPT_FDS); 其他 target_allows_fd = t- > 缓冲区 -> target_node- > accept_fds; 如果 ( ! target_allows_fd) { binder_user_error( “%d:%d使用fd获得了%s,%d,但是目标不允许fds \ n ” , proc- > pid, 线程 -> pid, in_reply_to ? “ reply” : “交易” , fd); ret = -EPERM; 转到 err_fd_not_accepted; } // 2:获取与文件描述符号相对应的文件结构 文件 = fget(fd); 如果 ( ! 文件) { binding_user_error( “%d:%d获得了无效fd的交易,%d \ n ” , proc- > pid, 线程 -> pid, fd); ret = -EBADF; 转到 err_fget; } // 3:SELinux检查 ret = security_binder_transfer_file(proc- > tsk, target_proc- > tsk, file); 如果 (ret < 0 ) { ret = -EPERM; 转到 err_security; } // 4:在目标进程中获取一个“免费”文件描述符号。 target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); 如果 (target_fd < 0 ) { ret = -ENOMEM; 转到 err_get_unused_fd; } // 5:将“ file”插入到具有target_fd文件描述符编号的目标进程中。 task_fd_install(target_proc, target_fd, 文件); 返回 target_fd; // [...] }
经过一些验证之后,对task_fd_install
的最后一次调用将在目标进程中添加与调用方文件描述符关联的文件。 在内部,它使用内核API函数__fd_install
在进程fd数组中安装文件指针。
缓冲对象
缓冲对象是最有趣的。 它们由硬件服务的Parcel类使用,并允许传输内存缓冲区。 缓冲区对象具有一种层次结构机制,可用于修补父对象的偏移量。 这对于发送包含指针的结构非常有用。 活页夹缓冲区对象由以下结构定义:
//文件:include / uapi / linux / android / binder.h structinder_buffer_object { 结构 binder_object_header hdr; __u32 标志; binding_uintptr_t 缓冲区; binding_size_t 长度; binding_size_t 父对象; 活页夹大小 };
让我们看一个例子:我们有以下代码,我们想使用Binder发送hidl_string
结构的实例。
struct hidl_string { //从C样式的字符串复制。 nullptr将创建一个空字符串 hidl_string( const char * ); // ... 私人的 : 详细信息 :: hidl_pointer < const char > mBuffer; //指向真正的char字符串的指针 uint32_t mSize; //不包括结尾的'\ 0'。 bool mOwnsBuffer; //如果为true,则mBuffer为可变字符* }; hidl_string my_obj ( “我的演示字符串” );
创建my_obj时,将执行堆分配以存储给定的字符串,并设置属性mBuffer
。 要将这个对象发送到另一个进程,需要两个BINDER_TYPE_PTR
对象:
- 第一个
binder_buffer_offset
,其缓冲区字段指向my_obj
结构 - 第二个指向堆中的字符串。 该对象必须是先前对象的子对象,并将parent_offset属性设置为
char * str
在结构中的位置
下图详细说明了所需的两个绑定程序对象的配置:
活页夹消息缓冲区
当内核转换这些对象时,它将修补子缓冲区中描述的偏移量,并将不同的缓冲区([object.buffer,object.buffer + object.length])复制到目标内存进程中。 在我们的例子中,对应于属性mBuffer
的偏移量是用指针修补的,该指针将字符串存储在目标存储过程中。
活页夹消息缓冲区
为了解析my_obj
数据,目标进程读取第一个缓冲区以获取hidl_struct
(3),而下一个缓冲区的预期大小为mSize
以确保结构( mSize
)中描述的大小与包含该大小的缓冲区的大小相同。字符串(4) 。
结论
Binder是一个复杂而强大的IPC / RPC系统,它可以使整个Android生态系统正常工作。 即使内核组件很旧,也很少有有关其工作原理的文档。 此外,最近在Android内核( https://lore.kernel.org/patchwork/patch/757477/ )中添加了有趣的对象类型BINDER_TYPE_FDA
和BINDER_TYPE_PTR
。 这些新类型是Android 8.0中通过Treble
项目引入的新HAL架构中的通信基础(HIDL)。