Binder是Android中主要的IPC / RPC(进程间通信)系统。 它允许应用程序彼此通信,并且它是Android环境中几种重要机制的基础。 例如,Android服务是建立在Binder之上的。 与Binder交换的消息称为binder事务 ,它们可以传输简单数据(例如整数),但也可以处理更复杂的结构,例如文件描述符,内存缓冲区或对象的弱/强引用。 Internet上有很多有趣的Binder文档,但是关于如何将消息从一个进程转换到另一个进程的细节很少。 本文试图描述Binder如何处理消息以及如何在不同进程之间执行复杂对象(文件描述符,指针)的转换。 为此,采用的最佳实践是从用户态到binder内核态的binder事务。
用户态的Binder
在探索Binder内核模块如何工作之前,让我们以Android Service调用为例,来看下binder事务是如何在用户态中准备的。
Android服务概述
服务是在后台运行并为其他应用程序提供功能的Android组件。 其中一些服务是Android框架的一部分,但是已安装的应用程序也可以通过服务发布自己功能,供他人使用。 当应用程序要发布新服务时,它首先注册到“服务管理器Service Manager” (1) ,其中包含并更新所有正在运行的服务的列表。 稍后,客户端向ServiceManager 请求一个handler(2)来与该服务进行通信,该handler能够调用已发布的函数(3) 。
从Android 8.0开始,存在三个不同的Binder域。 每个域都有其自己的服务管理器,并且可以通过/dev/
的相应设备进行访问。 一个binder域对应一个设备,如下表描述:
为了使用binder系统,一个进程需要打开这些设备中的一个,并执行一些初始化步骤,然后再发送或接收binder事务。
准备binder事务
Android框架在binder设备上包含多个抽象层。 通常,当开发人员实现新服务时,他们会以高级语言描述要公开的接口。 以平台应用程序为例,是以AIDL语言编写的,而由供应商开发的硬件服务则具有以HIDL语言编写的接口描述。 这些描述被编译到Java / C ++文件中,在其中使用Parcel组件对参数进行序列化或者反序列化。 生成的代码包含两个类,一个Binder Proxy和一个Binder Stub 。 Proxy代理类用于请求远程服务,而Stub则用于接收传入呼叫,如下图所示。
binder层
在最底层,通过域对应的设备将应用程序连接到Binder内核模块。 使用ioctl
syscall来发送和接收绑定程序消息。
序列化步骤使用Parcel类完成,该类提供了在Binder消息中读取和写入数据的功能。 有两种不同的类:
/dev/binder
和/dev/vndbinder
域基于AIDL描述语言,并使用在frameworks/native/include/binder/Parcel.h定义的Parcel。 该类型Parcel允许发送基本类型和文件描述符 。 例如,以下代码摘自命令SHELL_COMMAND_TRANSACTION
的默认代理实现。 该命令准备并写入远程服务使用的标准输入,输出和错误流的文件描述符。
// Extract from frameworks/base/core/java/Android/os/Binder.java public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeFileDescriptor(in); data.writeFileDescriptor(out); data.writeFileDescriptor(err); data.writeStringArray(args); ShellCallback.writeToParcel(callback, data); resultReceiver.writeToParcel(data, 0); try { transact(SHELL_COMMAND_TRANSACTION, data, reply, 0); reply.readException(); } finally { data.recycle(); reply.recycle(); } }
/dev/hwbinder
域使用的是基于之前实现基础上的另一个Parcel实现,system/libhwbinder/include/hwbinder/Parcel.h- 。 这种Parcel实现允许发送数据缓冲区,例如C结构。 数据缓冲区可以嵌套,并包含指向其他结构的指针。 在以下示例中,结构
hild_memory
结构包含一个嵌入式结构(hild_string
)和一个内存指针(mHandle
):
// Extract from 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); // [...] // Extract from system/libhidl/base/include/hidl/HidlSupport.h struct hidl_memory { // ... private: hidl_handle mHandle __attribute__ ((aligned(8))); uint64_t mSize __attribute__ ((aligned(8))); hidl_string mName __attribute__ ((aligned(8))); }; 这两种Parcel能够发送文件描述符和带有内存地址的复杂数据结构。 因为这些元素包含特定于调用者进程的数据,所以Parcel组件将绑定对象写入事务消息中。
Binder对象
除了简单类型(字符串,整数等)之外,还可以发送绑定对象。 Binder对象是一种类型值为以下之一的结构:
/ Extract from : drivers/staging/Android/uapi/binder.h
enum {
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对象的struct binder_object_header {
__u32 type;
};
struct binder_buffer_object {
struct binder_object_header hdr;
__u32 flags;
binder_uintptr_t buffer;
binder_size_t length;
binder_size_t parent;
binder_size_t parent_offset;
};
hdr
下方的属性是属于特定类型的。
不同的binder对象描述如下:
- BINDER_TYPE_BINDER和BINDER_TYPE_WEAK_BINDER :这些类型是对本地对象的强引用和弱引用。
- BINDER_TYPE_HANDLER和BINDER_TYPE_WEAK_HANDLE :这些类型是对远程对象的强引用和弱引用。
- BINDER_TYPE_FD :此类型用于发送文件描述符号。 这通常用于发送ashmem共享内存以传输大量数据。 实际上,binder s事务消息被限制为1 MB。 但是,可以使用任何文件描述符类型(文件,套接字,标准输入等)。
- BINDER_TYPE_FDA :描述文件描述符数组的对象。
- BINDER_TYPE_PTR :用于使用内存地址及其大小发送缓冲区的对象。
当Parcel类编写缓冲区或文件描述符时,它将在数据缓冲区中添加binder对象(图上为蓝色)。 活页夹对象和简单类型混合在数据缓冲区中。 每次写入对象时,其相对位置都会插入到偏移缓冲区中(紫色)。
binder消息缓冲区和偏移量
一旦data
和offets
缓冲区已满,就准备将binder_transaction_data
传递给内核。 我们可以注意到它包含上述指针,数据缓冲区和偏移量数组的大小。 字段handler
用于设置目标进程,该进程是先前由服务管理器检索的。 另一个有趣的属性是code
,其中包含要执行的远程服务的方法ID。
// file : development/ndk/platforms/android-9/include/linux/binder.h
struct binder_transaction_data {
union {
size_t handle;
void *ptr;
} target;
void *cookie;
unsigned int code;
unsigned int flags;
pid_t sender_pid;
uid_t sender_euid;
size_t data_size;
size_t offsets_size;
union {
struct {
const void *buffer;
const void *offsets;
} ptr;
uint8_t buf[8];
} data;
};
在调用ioctl之前,必须填充最后一个结构( binder_write_read
)。 它包含读写命令缓冲区,并指向上一个缓冲区:
// file : development/ndk/platforms/android-9/include/linux/binder.h struct binder_write_read { signed long write_size; signed long write_consumed; unsigned long write_buffer; signed long read_size; signed long read_consumed; unsigned long read_buffer; };
发送binder事务所需的数据结构可以用下面的图总结:
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
。
现在所有都准备好执行绑定程序事务,调用者需要使用ioctl执行
命令BINDER_WRITE_READ
,内核模块将处理该消息并转换目标进程的所有绑定程序对象:强/弱处理程序,文件描述符和缓冲区。
在下一部分中,让我们继续在内核方面进行分析!
Binder内核模块
现在,调用者进程已准备好其数据并执行了一个ioctl来发送事务。 所有Binder对象都将被转换,并且消息将被复制到目标内存中。
用于ioctl
的命令由binder_ioctl_write_read
函数处理,该函数执行数据参数的安全复制。
// file : drivers/android/binder.c static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { // [...] switch (cmd) { case BINDER_WRITE_READ: ret = binder_ioctl_write_read(filp, cmd, arg, thread); if (ret) goto err; break; // file : drivers/android/binder.c static int binder_ioctl_write_read(struct file *filp, unsigned int cmd, unsigned long arg, struct binder_thread *thread) { // [...] if (copy_from_user(&bwr, ubuf, sizeof(bwr))) { ret = -EFAULT; goto out; } // [...] if (bwr.write_size > 0) { ret = binder_thread_write(proc, thread, bwr.write_buffer, bwr.write_size, &bwr.write_consumed);
在写事务的情况下,将调用函数binder_thread_write
,然后将与事务关联的命令调度到相应的处理程序。
// file : drivers/android/binder.c switch (cmd) { case BC_INCREFS: case BC_ACQUIRE: case BC_RELEASE: case BC_DECREFS: // [...] case BC_TRANSACTION_SG: case BC_REPLY_SG: { struct binder_transaction_data_sg tr; if (copy_from_user(&tr, ptr, sizeof(tr))) return -EFAULT; ptr += sizeof(tr); binder_transaction(proc, thread, &tr.transaction_data, cmd == BC_REPLY_SG, tr.buffers_size); break; } // [...]
对于命令BC_TRANSACTION_SG
,在userland中准备的binder_transaction_data缓冲区由binder_transaction
函数处理。
binder事务
binder_transaction
函数位于文件drivers/staging/Android/binder.c
。
这个重要的功能执行以下任务:在目标进程中(在binder保留的内存中)分配一个缓冲区,验证所有数据对象并执行转换,在目标内存进程中复制数据和偏移缓冲区。
为了验证binder象,内核查看包含所有对象相对位置的offsets
缓冲区。 取决于对象类型,内核执行不同的转换。
// file : drivers/android/binder.c static void binder_transaction(struct binder_proc *proc, struct binder_thread *thread, struct binder_transaction_data *tr, int reply, binder_size_t extra_buffers_size){ // [...] // Object validation in binder_transaction function. // offp is a pointer to the offsets buffer for (; offp < off_end; offp++) { struct binder_object_header *hdr; size_t object_size = binder_validate_object(t->buffer, *offp); if (object_size == 0 || *offp < off_min) { binder_user_error("%d:%d got transaction with invalid offset (%lld, min %lld max %lld) or object.\n", proc->pid, thread->pid, (u64)*offp, (u64)off_min, (u64)t->buffer->data_size); return_error = BR_FAILED_REPLY; return_error_param = -EINVAL; return_error_line = __LINE__; goto err_bad_offset; } hdr = (struct binder_object_header *)(t->buffer->data + *offp); off_min = *offp + object_size; switch (hdr->type) { case BINDER_TYPE_BINDER: case BINDER_TYPE_WEAK_BINDER: { // [..] Validation and Translation case BINDER_TYPE_HANDLE: case BINDER_TYPE_WEAK_HANDLE: { // [..] Validation and Translation } case BINDER_TYPE_FD:{ // [..] Validation and Translation } case BINDER_TYPE_FDA:{ // [..] Validation and Translation } case BINDER_TYPE_PTR: { // [..] Validation and Translation }
Weak/Strong Binder/Handler
binder对象引用可以是指向本地对象的虚拟内存地址(binder引用),也可以是标识另一个进程的远程对象的处理程序(处理程序引用)。
当内核获取对象引用(本地或远程)时,它将更新内部表,该表包含每个进程的真实虚拟内存地址和处理程序(binder <=>处理程序)之间的映射。
有两种翻译:
- 将虚拟内存地址转换为处理程序:
binder_translate_binder
- 将处理程序转换为虚拟内存地址:
binder_translate_handle
Binder内核模块保留共享对象的引用计数。 与新进程共享引用时,其计数器值将增加。 当不再使用参考时,将通知所有者并可以释放它。
binder->handler转换
// file : drivers/android/binder.c
static int binder_translate_binder(struct flat_binder_object *fp,
struct binder_transaction *t,
struct binder_thread *thread)
{
// [...]
node = binder_get_node(proc, fp->binder);
if (!node) {
node = binder_new_node(proc, fp);
if (!node)
return -ENOMEM;
}
if (fp->cookie != node->cookie) {
// [...] ERROR
}
// SELinux check
if (security_binder_transfer_binder(proc->tsk, target_proc->tsk)) {
// [...] ERROR
}
ret = binder_inc_ref_for_node(target_proc, node,
fp->hdr.type == BINDER_TYPE_BINDER,
&thread->todo, &rdata);
if (ret)
goto done;
if (fp->hdr.type == BINDER_TYPE_BINDER)
fp->hdr.type = BINDER_TYPE_HANDLE;
else
fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
fp->binder = 0;
fp->handle = rdata.desc;
fp->cookie = 0;
// [..]
}
该函数获取与绑定器值(虚拟地址)对应的节点,或者如果不存在该节点,则创建一个新节点。 此节点在本地对象和远程对象( rdata.desc
)之间具有关联。 在SELinux安全检查之后,引用计数器将增加,并且绑定程序对象中的引用值将更改,并由引用处理程序替换。
handler->binder转换
// file : drivers/android/binder.c static int binder_translate_handle(struct flat_binder_object *fp, struct binder_transaction *t, struct binder_thread *thread) { // [...] node = binder_get_node_from_ref(proc, fp->handle, fp->hdr.type == BINDER_TYPE_HANDLE, &src_rdata); if (!node) { // [...] Error } // SELinux security check if (security_binder_transfer_binder(proc->tsk, target_proc->tsk)) { ret = -EPERM; goto done; } binder_node_lock(node); if (node->proc == target_proc) { if (fp->hdr.type == BINDER_TYPE_HANDLE) fp->hdr.type = BINDER_TYPE_BINDER; else fp->hdr.type = BINDER_TYPE_WEAK_BINDER; fp->binder = node->ptr; fp->cookie = node->cookie; // [...] binder_inc_node_nilocked(node, fp->hdr.type == BINDER_TYPE_BINDER, 0, NULL); // [...] } else { struct binder_ref_data dest_rdata; ret = binder_inc_ref_for_node(target_proc, node, fp->hdr.type == BINDER_TYPE_HANDLE, NULL, &dest_rdata); // [...] fp->binder = 0; fp->handle = dest_rdata.desc; fp->cookie = 0; } done: binder_put_node(node); return ret; }
此转换功能与上一个功能非常相似。 但是,我们可以注意到,处理程序引用可以在不同的进程之间共享。 如果目标进程与节点匹配,则仅在绑定程序引用中转换处理程序引用。
文件描述符
当binder对象类型为BINDER_TYPE_FD或BINDER_TYPE_FDA时,内核需要检查文件描述符是否正确(与打开的struct文件相关联)并在目标进程中将其复制。 转换是由binder_translate_fd
函数完成的。 详情如下:
// file : drivers/android/binder.c static int binder_translate_fd(int fd, struct binder_transaction *t, struct binder_thread *thread, struct binder_transaction *in_reply_to) { // [...] // 1 : Check if the target allows file descriptors if (in_reply_to) target_allows_fd = !!(in_reply_to->flags & TF_ACCEPT_FDS); else target_allows_fd = t->buffer->target_node->accept_fds; if (!target_allows_fd) { binder_user_error("%d:%d got %s with fd, %d, but target does not allow fds\n", proc->pid, thread->pid, in_reply_to ? "reply" : "transaction", fd); ret = -EPERM; goto err_fd_not_accepted; } // 2 : Get file struct corresponding to the filedescriptor number file = fget(fd); if (!file) { binder_user_error("%d:%d got transaction with invalid fd, %d\n", proc->pid, thread->pid, fd); ret = -EBADF; goto err_fget; } // 3 : SELinux check ret = security_binder_transfer_file(proc->tsk, target_proc->tsk, file); if (ret < 0) { ret = -EPERM; goto err_security; } // 4 : Get a 'free' filedescriptor number in the target process. target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); if (target_fd < 0) { ret = -ENOMEM; goto err_get_unused_fd; } // 5 : This inserts the 'file' into the target process with the target_fd filedescriptor number. task_fd_install(target_proc, target_fd, file); return target_fd; // [...] }
经过一些验证之后,对task_fd_install
的最后一次调用将在目标进程中添加与调用方文件描述符关联的文件。 在内部,它使用内核API函数__fd_install
在进程fd数组中安装文件指针。
缓冲对象
缓冲对象是最有趣的。 它们由硬件服务的Parcel类使用,并允许传输内存缓冲区。 缓冲区对象具有一种层次结构机制,可用于打包父对象的偏移量。 这对于发送包含指针的结构非常有用。 binder缓冲区对象由以下结构定义:
// file : include/uapi/linux/android/binder.h struct binder_buffer_object { struct binder_object_header hdr; __u32 flags; binder_uintptr_t buffer; binder_size_t length; binder_size_t parent; binder_size_t parent_offset; }; 让我们看一个例子:我们有以下代码,我们想使用Binder发送hidl_string结构的实例。
struct hidl_string { // copy from a C-style string. nullptr will create an empty string hidl_string(const char *); // ... private: details::hidl_pointer<const char> mBuffer; // Pointer to the real char string uint32_t mSize; // NOT including the terminating '\0'. bool mOwnsBuffer; // if true then mBuffer is a mutable char * }; hidl_string my_obj("My demo string");
创建my_obj时,将执行堆分配以存储给定的字符串,并设置属性mBuffer
。 要将这个对象发送到另一个进程,需要两个BINDER_TYPE_PTR
对象:
- 第一个
binder_buffer_offset
,其缓冲区字段指向my_obj
结构 - 第二个指向堆中的字符串。 该对象必须是先前对象的子对象,并将parent_offset属性设置为
char * str
在结构中的位置
下图详细说明了所需的两个绑定程序对象的配置:
binder消息缓冲区
当内核转换这些对象时,它将打包子缓冲区中描述的偏移量,并将不同的缓冲区([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)。