基础
ONE_WAY
binder 的调用分两种模式,one way 模式以及非 one way 模式。要注意:one_way 只在跨进程时才有效,不跨进程时无用
- ONE_WAY:异步调用。调用方不阻塞,源进程将数据发送完后就结束了。one way 意思为单程,只传输数据,不返回结果
- 非 ONE_WAY:阻塞等待目标进程返回结果
- one_way 在定义
aidl 时方法前用 oneway修饰即可
与内核交互的步骤
一个进程与 binder 驱动交互,一般有以下几步:
- 先通过 binder_open 打开 binder 通信
- 再通过 binder_mmap 执行内存映射,这里是在接收进程空间与内核空间进行内存映射
- 通过 binder_ioctl 进行数据交换。这个过程也分为四步
- 先使用 copy_from_user 从调用进程中复制信息,包括:要写入的数据量、要读的数据量等。握手过程
- 使用 binder_thread_write 从调用进程空间读
- 使用 binder_thread_read 往调用进程空间写
- 再将第一步的握手信息 copy 回调用进程空间
- 用户进程与内核通过 binder 通信时,它们的数据格式是约定好的。即第几个字节传什么值,都是固定的。这样内核才能解析出具体的值
常用结构体
总结几个比较重要的:
| 名 | 内容 | 初始化时机 |
|---|---|---|
| vm_area_struct | 调用进程虚拟地址的描述,它是一段连接的虚拟内存空间 | binder_mmap 时会用作参数 |
| binder_write_read | 记录读写数据 | 第一次 copy_from_user 时填充 |
| binder_transaction_data | binder 通信的事务数据 | 记录有调用者的 pid/uid,目标进程等信息 |
| binder_proc | 应用进程在 binder 驱动中的结构体,存储该进程相关信息 | binder_open 时初始化 |
| binder_thread | 应用进程的线程在驱动中的映射 | |
| binder_alloc | binder_proc 中用于操作 binder_buffer 的结构体 | binder_open 时初始化 |
| binder_buffer | 描述一次交互过程中使用到的内存 | mmap()创建用于 Binder 传输数据的缓存区 |
| binder_node | IBinder 对象在驱动中的表示,一般称之为 binder 实体 | 在 IBinder 对象第一次进入驱动时创建 |
| binder_ref | 一般称之为 binder 引用。它是将 IBinder 对象的句柄与 Binder 实体进行关联的类 | 在 IBinder 对象第一次进入驱动时创建 |
binder_transaction | 一次进程通信的抽象 | 含有属性 binder_work,所以可以添加到 binder_thread/binder_proc 的 todo 列表中 |
binder_work | binder_thread 的工作抽象 |
对于 binder_node,简单理解在 aidl 中,每一个 Service#onBind 返回的对象在驱动中都有一个 binder_node 与之对应。常用的 ServiceManager 它在 binder 常用中有一个对应的 binder_node:binder_context_mgr_node。
binder_node 最通俗的理解为:驱动使用它存储了 IBinder 对象的一些信息。binder_node::ptr 指向了 IBinder 对象在其宿主进程中的地址。binder_ref 不但存储有句柄,还有句柄对应的 binder_node 对象
binder_proc 通过红黑树组织管理 binder_thread。查看 binder_proc 内容,可以发现它里面有一些跟 thread 相关的属性,如 max_threads 等,binder 驱动会根据这些属性决定 binder 线程的使用。另外,binder_thread 中也有 todo 属性用于记录该线程要处理的工作。
binder 驱动需要与服务进程建立内存映射,也需要内存缓存去存储客户端发送的数据。这些内存缓存在驱动中使用 binder_buffer 表示。
在旧版本中 binder_buffer 是直接使用 binder_proc 管理,在新版本中将这部分功能移交给了 binder_alloc,由 binder_alloc 管理。binder_buffer 使用后并不会被回收,而是由 binder_alloc 按红黑树进行管理。下次使用时,会从树中查找到最合适的一块直接使用。在 binder_alloc 有几个跟 buffer 相关的属性:
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
有几个引用关系:
vm_area_struct->vm_private_data = binder_proc
binder_proc->binder_alloc->buffer = vm_area_struct->vm_start
// 最大不超过 4M
binder_alloc->buffer_size = vm_area_struct->vm_end - vm_area_struct->vm_start
binder_thread
应用端线程在内核中的映射
主要结构如下:
struct binder_proc *proc; // 其所关联的 binder_proc
// 关联 binder_proc 中的 binder_proc::threads
struct rb_node rb_node;
// 用于链接到 binder_proc::waiting_threads
// 一旦一个线程出现在这,就表明它现在很闲
struct list_head waiting_thread_node;
int pid;
int looper; /* only modified by this thread */
bool looper_need_return; /* can be written by other thread */
// 通过后面一个线程可能递归的处理多个请求,每一个请求有不同的参数
// 所有这里叫 stack,记录下不同请求的参数
struct binder_transaction *transaction_stack;
// 当前线程要处理的 binder_work
struct list_head todo;
bool process_todo;
// 当 binder_thread 没事可干时就会被阻塞,这个属性用于完成该功能
wait_queue_head_t wait;
这里面涉及到红黑树处理。linux 内核中某个对象如果想用存储于红黑树,它得执有 rb_node 实例,然后用该实例在红黑树中 crud。同时 c++ 可以通过 container_of 拿到 rb_node 所属的对象,所以卑微的只会 java 语言初看时一脸懵逼。
container_of 的主要原理是:已知当前对象的地址,它在所属的结构体中的偏移也是固定的,两者进行运算就可以知道所属对象的地址。linux 中通过 inode 对象拿到实际文件系统中的 node 也是这么玩的。
上面说过一个线程可能递归地处理多个 ipc,这里解释下原因。假设 A 请求 B,在 B 返回前 A 应该就会被阻塞。如果此时 B 再请求 A,内核就会将任务交给 A 执行(此时内核是知道 A 处于等待状态)。因为与其让 A 空等,不如让它处理,免得另启线程消耗资源。因此可以得出一结论:大部分 ipc 都是由 binder 线程处理,但递归调用时会由发起线程处理
关于 binder_thread 的处理主要在 binder_ioctl() 中。该方法调用 binder_get_thread()
static struct binder_thread *binder_get_thread(struct binder_proc *proc)
{
struct binder_thread *thread;
struct binder_thread *new_thread;
binder_inner_proc_lock(proc);
// 先从 binder_proc 中已有的 thread 选择。通过 pid 选择
thread = binder_get_thread_ilocked(proc, NULL);
binder_inner_proc_unlock(proc);
if (!thread) {
// 如果没有,就新建
new_thread = kzalloc(sizeof(*thread), GFP_KERNEL);
if (new_thread == NULL)
return NULL;
binder_inner_proc_lock(proc);
// 再选择。此时已有新的 thread,所以如果还没有就存储到 binder_proc 中
thread = binder_get_thread_ilocked(proc, new_thread);
binder_inner_proc_unlock(proc);
// 如果第二次选择到,就把新建的 thread 给释放
if (thread != new_thread)
kfree(new_thread);
}
return thread;
}
binder_get_thread_ilocked() 如下:
static struct binder_thread *binder_get_thread_ilocked(
struct binder_proc *proc, struct binder_thread *new_thread)
{
struct binder_thread *thread = NULL;
struct rb_node *parent = NULL;
// 先拿到红黑树的根节点
struct rb_node **p = &proc->threads.rb_node;
while (*p) {
parent = *p;
// 通过 rb_node 拿到它所属的 binder_thread 对象
thread = rb_entry(parent, struct binder_thread, rb_node);
if (current->pid < thread->pid)
p = &(*p)->rb_left;
else if (current->pid > thread->pid)
p = &(*p)->rb_right;
else
return thread;
}
// 到这里说明没有找到。如果也没有 new_thread,就可以直接返回了
// 对应了上面的第一次查找
if (!new_thread)
return NULL;
thread = new_thread;
thread->proc = proc;
thread->pid = current->pid;
thread->task = current;
init_waitqueue_head(&thread->wait);
INIT_LIST_HEAD(&thread->todo);
// 将新 thread 插入到红黑树中
rb_link_node(&thread->rb_node, parent, p);
rb_insert_color(&thread->rb_node, &proc->threads);
// 省略剩余各种属性赋值
return thread;
}
关于 binder_thread 还有一个地方处理,在 binder_thread_read 中,在该方法的最后会判断是不是 binder_thread 不足,如果不足就会让进程再启一个 binder_thread。主要逻辑如下:
// 如果当前没有正在请求新建 binder 线程
// 同时没有在等待 binder 线程,而且没有达到最大线程个数
if (proc->requested_threads == 0 &&
list_empty(&thread->proc->waiting_threads) &&
proc->requested_threads_started < proc->max_threads &&
(thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
BINDER_LOOPER_STATE_ENTERED))) {
// +1 表示正在请求新建
proc->requested_threads++;
binder_inner_proc_unlock(proc);
// 给进程发一个 BR_SPAWN_LOOPER,用于创建 binder 线程
if (put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))
return -EFAULT;
} else
binder_inner_proc_unlock(proc);
binder_proc
// 存储 binder_thread 的红黑树的根节点
struct rb_root threads;
// 每一个 binder 对象在驱动中都有一个对应的 binder_node
// nodes 属性就是用于存储这些 binder_node
struct rb_root nodes;
// 同时每一个进程也会通过 binder_ref 引用别的进程中的 binder_node
// 下面的两个属性用于存储这些 binder_ref
// 之所以用两个,是按不同的排序规则进行排序
struct rb_root refs_by_desc;
struct rb_root refs_by_node;
// 存储正在等任务的 binder_thread,当这里的 thread 都很闲
struct list_head waiting_threads;
int pid;
struct task_struct *tsk;
// 用于挂载进程中要处理的任务。对应的是 binder_work 结构体
struct list_head todo;
// 根据注释,这个应该是用来处理死亡通知的
struct list_head delivered_death;
// 当前进程允许的最大 binder 线程个数
int max_threads;
// 从上面可知:内核在线程不足时会主动要求服务端新建 binder 线程,用于表示正在申请的个数
// 根据注释,当前版本该属性只能是 0 或 1
// 但这个线程数量有限,就是 max_threads 指定的个数
int requested_threads;
// 已经启动的 binder_thread 线程个数
int requested_threads_started;
struct binder_alloc alloc;
binder_transaction
对一次跨进程通信的抽象
主要属性如下:
// 含有 binder_work 属性,因此可以添加到 todo 列表中
struct binder_work work;
// 跨进程调用的源
struct binder_thread *from;
struct binder_transaction *from_parent;
// 目标进程、线程
struct binder_proc *to_proc;
struct binder_thread *to_thread;
struct binder_transaction *to_parent;
// 是否需要返回结果
unsigned need_reply:1;
// 内存区。用于存储跨进程传递的数据
struct binder_buffer *buffer;
// 真正要请求的方法的 code 值
unsigned int code;
// 目标线程的最低优先级
struct binder_priority priority;
// 目标线程的原有优先级。目标线程在处理需求时,优先级会被临时提升到与
// 源线程一样。处理完成后会恢复到原有优先级
struct binder_priority saved_priority;
// 源进程的 uid,目标线程可使用该值鉴权
kuid_t sender_euid;
// binder 传输 fd 时会对 fd 进行一次转换,使两个进程的不同 fd 指向同一个系统级 item
// 这个过程叫 fix-up。该字段存储了所有要转换的 fd
struct list_head fd_fixups;
主要说一下 from_parent 与 to_parent。一次 ipc 涉及到两个 binder_thread,每一个 binder_thread 中都有一个 binder_transaction 栈,所以一个 binder_transaction 会同时存在于两个 binder_thread 中。
又由于 binder_thread 可以递归处理请求(即原请求未处理完又得进行别的 ipc),所以在新的 binder_transaction 入栈前就必须记录已有的栈顶,这样才能在处理完本次 ipc 后继续执行原有逻辑。因为两个 binder_thread 就需要两个属性记录,即 from_parent 与 to_parent。具体例子可见最上面的链接三
binder_work
binder_thread 与 binder_proc 中 todo 列表中存储的对象
我们知道当一个 binder_thread 没事干时会阻塞,当需要它干活时就往其 todo 列表中添加一个 binder_work,然后唤醒它。
binder_thread 清醒后会检查自己的 todo 列表,拿到 binder_work,根据 binder_work 的 type 执行不同的处理流程。同时通过 container_of() 函数拿到 binder_work 所属的对象。
这一部分逻辑在后面的 binder_thread_read 中体现。
// 用于存储在 todo 列表中
struct list_head entry;
enum binder_work_type {
// 下面可以看到,一次通信时最常用的是就最开始两个
BINDER_WORK_TRANSACTION = 1,
BINDER_WORK_TRANSACTION_COMPLETE,
BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT,
BINDER_WORK_RETURN_ERROR,
BINDER_WORK_NODE,
BINDER_WORK_DEAD_BINDER,
BINDER_WORK_DEAD_BINDER_AND_CLEAR,
BINDER_WORK_CLEAR_DEATH_NOTIFICATION,
} type; // 当前 work 的类型
binder_buffer
目标进程与内核建立的内存映射区。binder_proc 会使用 binder_alloc 管理 binder_buffer
// 用于在 binder_alloc 中存储
struct list_head entry;
struct rb_node rb_node;
// 关联的 binder_transaction
struct binder_transaction *transaction;
// 关联的 binder_node
struct binder_node *target_node;
// 数据大小
size_t data_size;
// 除基本数据类型外,还会存储一些对象(binder,fd 等),
size_t offsets_size;
// 内存区的起始位置
void __user *user_data;
int pid;
请求码
进程与驱动交互时,都通过请求码决定要进行的操作,因此请求码有两个流向:
- 从进程传递到驱动,以 BC_ 开头
- 从驱动传递到进程,以 BR_ 开头。binder return 的缩写。
比如一次请求,大体上涉及到以下几个请求码。来自理解Android Binder机制(1/3):驱动篇 (paul.pub)
其中第一步 BC_ENTER_LOOPER 是 server 进程被 fork 出来后主动向 binder 驱动注册的,用于告知驱动自己的 binder 主线程已就绪。
- BR_TRANSACTION_COMPLETE:数据已复制到目标进程。如果此时是 one way 模式,源线程就可以继续执行 ipc 后面的代码了
- BR_REPLY:目标线程已处理完请求,需要源线程到驱动中读取处理数据
- BR_TRANSACTION:有请求发送至当前线程,需要进行处理。也就是说此时当前线程是 ipc 中的服务端。
Binder 线程池
这里的线程池指服务端自己的线程池,不是 binder 驱动的。每一个进程被 fork 出后,会自动创建一个 binder 主线程,该线程不会退出
除 binder 主线程外,后续线程的创建由 binder 驱动根据需要通知服务端进行创建。这里涉及到服务端与驱动的几次交互,有不同的请求码:
- BC_ENTER_LOOPER:服务端发往驱动,告知驱动服务端 binder 主线程已就绪
- BR_SPAWN_LOOPER:驱动端发往服务端,告知服务端需要创建一个线程
- BC_REGISTER_LOOPER:服务端发往驱动,告诉驱动当前非主线程已就绪。当驱动先发 BR_SPAWN_LOOPER 到用户进程,用户进程创建完线程后传递驱动的请求码就是 BC_REGISTER_LOOPER
每一个进程被 zygote fork 出来以后会执行到 ZygoteInit#nativeZygoteInit,它会进入 native 层,然后启动自己的 binder 主线程。
说一点前置的。每一个进程在 native 层都有一个 ProcessState 对象,它是全局单例的。它构造时通过 mmap 与 binder 驱动建立联系,拿到驱动对应的文件描述符,后面跟 binder 驱动通信时都是通过该描述符进行的。binder 线程的启动就在 ProcessState#startThreadPool() 中。进入 native 层后,随后调用 ProcessState#startThreadPool:该方法会告诉 binder 驱动主线程已就绪——主线程是不会退出的
用户进程与 binder 驱动关于线程的交互主要有 ProcessState::self()->startThreadPool()与IPCThreadState::self()->joinThreadPool() 两个方法,前者在创建了一个线程以后在线程中调用了后者。joinThreadPool:默认参数为 true,因此默认时往驱动发送的请求码是 BC_ENTER_LOOPER
void IPCThreadState::joinThreadPool(bool isMain) // 默认为 true
{
mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
status_t result;
do {
// 该方法会与驱动交互,驱动拿到 BC_ENTER_LOOPER 或 BC_REGISTER_LOOPER
result = getAndExecuteCommand();
// 非主线程才可退出
if(result == TIMED_OUT && !isMain) {
break;
}
} while (result != -ECONNREFUSED && result != -EBADF);
// 线程退出时会发送 BC_EXIT_LOOPER 到驱动
mOut.writeInt32(BC_EXIT_LOOPER);
talkWithDriver(false);
}
每一个进程都有一个最大的 binder 线程,默认为 15,由驱动管理,但驱动只统计请求码为 BC_REGISTER_LOOPER 的线程。因此,可以简单理解 默认时驱动只能通知用户进程创建 15 个线程
binder_init
- binder 的初始化函数是 binder_init,该方法中调用了 misc_register() 向内核注册 binder 驱动
- 注册时传入了 miscdevice 结构体对象,对象中又定义了 binder 驱动的操作。因此看 binder 原理就看 file_operations 中定义的方法即可
// binder 相应的操作 static const struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = binder_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, }; // misc_register() 中的参数 static struct miscdevice binder_miscdev = { .minor = MISC_DYNAMIC_MINOR, .name = "binder", .fops = &binder_fops };
binder_open
为源进程创建 binder_proc 对象,同时将该对象添加到 binder_procs 中
- binder_procs 是一个哈希表(java 低版本的 HashMap 实现方式)
- 创建 binder_proc 对象,用于存储调用进程信息,然后使用 binder_procs 统一管理所有的 binder_proc 对象
binder_mmap
完成内存映射功能。或者说进程预留一块虚拟内存,用于后续接收数据。在 ioctl 的时候才会为这块虚拟内存对应物理内存
限制大小源进程与内核地址映射的大小最多为 4M- 这一步并没有在内核空间申请虚拟内存与源进程进行对应,只是生成 binder_buffer 对象,由它和 binder_alloc 共同存储源进程要映射的内存的起始位置以及大小
- 旧版本上这里会分配一页的物理内存,但新版本上没有了
上面主要方法 binder_alloc_mmap_handler()
通过 kcalloc() 分配了一个数组,这个数组大概是与实现物理地址相关的?不懂。
然后使用 binder_alloc 记录源进程用于映射的内存起始位置(vma->vm_start) 以及长度(vm_end - vm_start),到这里内核虚拟内存中并没有申请任何一块内存与源进程进行对应
最后生成 binder_buffer,再调用 binder_insert_free_buffer() 将 binder_buffer 与 binder_alloc 关联:将 binder_buffer 插入到 binder_proc->free_buffers 中,free_buffers 是通过红黑树管理所有的 binder_buffer
上面就是 binder_mmap 的整个流程,简单讲:生成 binder_buffer 对象,然后由 binder_alloc 管理。同时将源进程用于映射的内存起始位置以及大小,记录下来。大小分同步、异步:异步的大小是同步的 1/2
旧版上这个方法还会分配一页的物理内存,然后映射到源进程以及内核进程,但新版本上已经无了。
binder_ioctl
根据传入的不同命令执行不同的操作,两个进程间的收发数据的过程都由该方法完成
- 首先通过 binder_get_thread() 获取一个 binder_thread 对象,是源进程中的线程,不是目标进程中的
- binder_ioctl 会 switch(cmd),根据不同的 cmd 执行不同的操作。如 ServiceManger 会向 binder 进行注册,将自己设置为 manager,里面的 cmd 就是 BINDER_SET_CONTEXT_MGR
- 最常用的 cmd 是 BINDER_WRITE_READ,它会执行 binder_ioctl_write_read。下面分析
// 这个 arg 指的是源进程中 biner_write_read 结构体对象的地址
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct binder_proc *proc = filp->private_data;
struct binder_thread *thread;
// 这一步就是为 proc->threads 中查找 binder_thread 或新建一个,并初始化
// 上面说过
thread = binder_get_thread(proc);
switch (cmd) {
case BINDER_WRITE_READ: // 最常用的
ret = binder_ioctl_write_read(filp, cmd, arg, thread);
break;
。。。
}
binder_ioctl_write_read
这个方法是真正的将数据从用户空间复制到内容空间的,整个方法中使用了三次 copy_from_user
我们知道每一个 binder_thread 都有自己的 todo 列表,
这里是对上面总结的具体分析:
static int binder_ioctl_write_read(struct file *filp,
unsigned int cmd, unsigned long arg,
struct binder_thread *thread)
{
int ret = 0;
// 在 binder_open 中创建的 binder_proc 对象
struct binder_proc *proc = filp->private_data;
// 将 arg 强转成 void* 指针,忽略期中的 __user
void __user *ubuf = (void __user *)arg;
// 初始化一个结构体,这个 bwr 属性内核空间
// 虽然它在内核空间中创建的,但 bwr 中的缓冲区的地址却指向了用户进程
struct binder_write_read bwr;
// 这里只 copy 了 binder_write_read 的大小,并没有将所有数据全部 copy 到内核空间,也就是先进行了握手
// 这句执行完后 bwr 就会有值
if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
}
if (bwr.write_size > 0) {
// 源进程需要往目标进程中写数据,那么内核会就使用 copy_from_user 把数据复制过来
ret = binder_thread_write(proc, thread,
bwr.write_buffer,
bwr.write_size,
&bwr.write_consumed);
}
if (bwr.read_size > 0) {
// 如果源进程需要读取返回结果
ret = binder_thread_read(proc, thread, bwr.read_buffer,
bwr.read_size,
&bwr.read_consumed,
filp->f_flags & O_NONBLOCK);
// 如果当前是进程 todo 队列不为空,就唤醒一个线程去处理
if (!binder_worklist_empty_ilocked(&proc->todo))
binder_wakeup_proc_ilocked(proc);
}
// 再将 bwr 复制到源进程,ubuf 指的是源进程中 biner_write_read 结构体对象的地址
// Binder读/写完后, 将内核空间数据 bwr 拷贝到传输进来的用户空间 struct binder_write_read
if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
}
return ret;
}
从上面可以看出,驱动会根据是否要目标进程是否要读写分别调用 binder_thread_write 与 binder_thread_read。
binder_thread_write
先看方法声明与整体结构
// proc 与 thread 指源进程与源线程
// binder_buffer:指源进程的缓冲区地址,这里面存储了要发送给目标进程的数据
// size 指缓存区中数据大小
// consumed:指缓存区中的数据已被读取了多少,正常情况下应该是 0
static int binder_thread_write(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
uint32_t cmd;
struct binder_context *context = proc->context;
// 前面的注释中说过,这里拿到的是源进程的缓冲区的地址
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;
while (ptr < end && thread->return_error.cmd == BR_OK) {
// 先读一个命令,常用的就是 BC_TRANSACTION、BC_REPLY
// 这一块结合着 ipcthreadState 的 transact() 就比较明白
if (get_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
// 命令读完了
ptr += sizeof(uint32_t);
// 省略各种 case 处理,在下面分析
}
return 0;
}
它根据请求码执行不同的操作。请求码由调用进程传递。常用的有 BC_TRANSACTION 与 BC_REPLY。在这两种情况下,代码如下:
// buffer 是 bwr.write_buffer
// consumed 是 bwr.write_consumed
void __user *ptr = buffer + *consumed;
// get_user 跟 copy_from_user 一样,也是从源进程中读数据
get_user(cmd, (uint32_t __user *)ptr)
// 根据上面读的 cmd,通过 switch-cash 执行不同逻辑
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
// 再次从调用进程空间中 copy 一份数据,填充 binder_transaction_data
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
// 然后将数据交给 binder_transaction 处理
binder_transaction(proc, thread, &tr,
cmd == BC_REPLY, 0);
break;
}
到目前为止,又一次调用了 copy_from_user,但复制的还不是具体的数据,而是 binder_transaction_data 结构体对象。下一步就该是分发到目标进程。这一步在 binder_transaction 中。
binder_transaction
上面已经拿到了 binder_transaction_data 对象,该对象里属性指向了用户实际要发送的数据所在的位置,下一步就是建立目标进程与内核的映射,并复制数据。从常理推测,这一步需要处理的任务有三个:
- 寻找到目标进程
- 将数据复制到目标进程,伴随着对物理内存的分配
- 唤醒目标线程干活
分析该方法最主要一点是 发送和回复时,thread 指向的不同:前者指向客户端线程,后者指向服务端线程
先看第一步
// 参数 thread 指源进程的 thread
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){
struct binder_transaction *t; // 描述 server 端操作需要的一些数据。它通过 from_parent 属性将所有 binder_transaction 串成一个链表
struct binder_work *tcomplete; // 交给源线程的一个任务。
// binder 将数据复制到目标进程后,会向源进程回一条消息 BR_TRANSACTION_COMPLETE
// 源进程收到后,如果是 oneway 模式就直接返回。binder_work 就是做向源进程回消息任务的
struct binder_proc *target_proc = NULL; // 目标进程
struct binder_thread *target_thread = NULL; // 目标线程
struct binder_node *target_node = NULL; // 目标节点。每一个 server 在驱动中都有一个 binder_node 与之对应
if (reply) {
// thread 代表源进程
in_reply_to = thread->transaction_stack;
thread->transaction_stack = in_reply_to->to_parent;
// 为 target_thread 赋值
// 因为是回复情况,所以 target_thread 就是发送请求的线程,这里可以拿到 target_thread
target_thread = binder_get_txn_from_and_acq_inner(in_reply_to);
// 根据删除的判断部分,可得出下面结论:
// in_reply_to->to_thread == thread
// target_thread->transaction_stack == in_reply_to
target_proc = target_thread->proc;
} else {
// 发送时。客户进程无法指定服务进程的处理线程
// 所以整个 else 分支中不会为 target_thread 赋值,除了可以有复用的情况
// 根据 handle 寻找目标进程。
先找到 target_node
if (tr->target.handle) {
// 如果 handle 不是 0。 handle == 0 表示的是 ServiceManager
// 通过 handle 拿到 binder_ref,再通过 binder_ref 拿到 binder_node
struct binder_ref *ref;
// 同时句柄在 binder_proc 中找到相应的 binder_proc
ref = binder_get_ref_olocked(proc, tr->target.handle,
true);
// 可以直接理解为将 ref->node 赋值给 target_node
// 同时通过 binder_node::proc 为 target_proc 赋值
// 该方法执行完后,就找到目标进程对应的 binder_proc
target_node = binder_get_node_refs_for_txn(
ref->node, &target_proc,
&return_error);
} else {// handle 为 0,指向 service manager
target_node = context->binder_context_mgr_node;
// 跟上面一样
target_node = binder_get_node_refs_for_txn(
target_node, &target_proc,
&return_error);
}
// 下面的代码主要是想利用已有的 binder_thread,适用于递归 ipc
if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) {
struct binder_transaction *tmp;
tmp = thread->transaction_stack;
// 拿到当前线程的所有 binder_transaction,比对是否有正在与当前进程通信的进程
// 上面说过每一个 binder_transaction 同时会记录它的源、目标
while (tmp) {
struct binder_thread *from;
from = tmp->from;
// 如果有正在通信的线程,那么就将该线程做为处理本次请求的目标线程
if (from && from->proc == target_proc) {
target_thread = from;
break;
}
// 上面解释 binder_thread 时说过
// 此句就相当于从顶往下遍历栈中所有元素
tmp = tmp->from_parent;
}
}
}
// 新建 t 与 tcomplete,随后为它们属性赋值
// t 就是本次 ipc 的抽象,是 binder_transaction 对象
t = kzalloc(sizeof(*t), GFP_KERNEL);
// 用于 BR_TRANSACTION_COMPLETE 时。这个任务会交到源线程的 todo 列表
// 表示数据已经从源进程复制到目标进程
// 如果本次操作是 oneway 模式,整个调用过程就结束了,源线程可以继承执行 ipc 后的逻辑
// 否则,源线程会忽略此次 br,继承等待 br_reply 拿到目标进程返回的结果
tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);
// t 中属性的一部分赋值,其余省略
// 发送的时候且处于非 oneway 模式,t->from 记录了源线程
// 便于目标进程在处理完事务后, 找到源线程, 并将结果返回给源线程
if (!reply && !(tr->flags & TF_ONE_WAY))
t->from = thread;
else
t->from = NULL;
// 下面为 t 的各种属性赋值,上面 binder_transaction 中说过
t->sender_euid = task_euid(proc->tsk);
t->to_proc = target_proc;
// 只有在回复时,target_thread 才有值,发送数据时值为 null
t->to_thread = target_thread;
t->code = tr->code;
t->flags = tr->flags;
//为了上面 if 判断描述方便,这里将一部分代码提前
if(!(t->flags & TF_ONE_WAY)){
// 将 from_parent 设置成原来的 transaction_stack
t->from_parent = thread->transaction_stack;
// 然后用 thread->transaction_stack 记录当前的
// 因此整个 transaction_stack 即 binder_transaction 就会串成一个链表
thread->transaction_stack = t;
}
}
上面是第一步,已经寻找到了目标进程,同时也拿到了 binder_thread。在找 target_thread 的时候,有一个复用过程,这一块可参考 Service注册过程--Java服务中的 5.7.3 节
还有一点要注意:回复时 target_node 为 null,只有调用时才不为 null。这一块后面在 binder_thread_read() 分析时会用到
第二步就该是将数据复制到目标进程中
// 这里会分配实际的物理内存
t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
tr->offsets_size, extra_buffers_size,
!reply && (t->flags & TF_ONE_WAY), current->tgid);
t->buffer->transaction = t;
t->buffer->target_node = target_node;
// 用户进程传入到 binder 驱动的数据分两种:一种是普通的 int string 等,一种是 IBinder 对象
// 两者的处理逻辑不同
// 先复制普通的数据。这一次就是复制的真实数据,会复制到内存映射区,即 t->buffer
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer, 0,
(const void __user *)
(uintptr_t)tr->data.ptr.buffer,
tr->data_size)) {
}
// 再复制 IBinder 对象
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer,
ALIGN(tr->data_size, sizeof(void *)),
(const void __user *)
(uintptr_t)tr->data.ptr.offsets,
tr->offsets_size)) {
}
// 复制完 IBinder 对象后,还需要进行一些处理,剩下的一块就是处理 IBinder 对象的
off_start_offset = ALIGN(tr->data_size, sizeof(void *));
buffer_offset = off_start_offset;
off_end_offset = off_start_offset + tr->offsets_size;
for (buffer_offset = off_start_offset; buffer_offset < off_end_offset;
buffer_offset += sizeof(binder_size_t)) {
//...
}
上面的方法我们留了一个 IBinder 对象的处理,这一块可看下一步中关于 IBinder 传输部分 Binder入门(三) 数据传输
数据已复制完成,下一步就开始唤醒目标线程干活了。
tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
t->work.type = BINDER_WORK_TRANSACTION;
if (reply) {
// tcomplete 添加到 thread 的 todo 列表中。在回复时,thread 指服务端线程
binder_enqueue_thread_work(thread, tcomplete);
// 删除 target_thread->transaction_stack 中的第一个元素
// 我们上面说过 target_thread->transaction_stack 是一个链表,这里就相当于删除表头
binder_pop_transaction_ilocked(target_thread, in_reply_to);
// 将 t->work 添加到 target_thread 的 todo 列表中
// 注意 work 的 type = BINDER_WORK_TRANSACTION
binder_enqueue_thread_work_ilocked(target_thread, &t->work);
// 唤醒 target_thread。这里是回复,也就相当于唤醒源线程
wake_up_interruptible_sync(&target_thread->wait);
} else if (!(t->flags & TF_ONE_WAY)) {
// 非单程,需要返回数据
// 将 tcomplete 添加到 thread->todo 中
binder_enqueue_deferred_thread_work_ilocked(thread, tcomplete);
// 因为需要返回数据,所以用 thread 记录一下传递给目标里程的 binder_transaction
// 将 t 添加到 thread->transaction_stack 中
// thread->transaction_stack 是一单链表,将 t 添加到表头
t->need_reply = 1;
t->from_parent = thread->transaction_stack;
thread->transaction_stack = t;
// 传递给目标里程,并唤醒目标线程
// 注意这里的也会将 t 添加到 target_thread 的 todo 列表中
binder_proc_transaction(t,
target_proc, target_thread);
} else {
// 单程,不需要返回结果
// 将 tcomplete 添加到 thread->todo 中,同时将 thread->process_todo = true
binder_enqueue_thread_work(thread, tcomplete);
// 传递给目标里程,并唤醒目标进程
binder_proc_transaction(t, target_proc, NULL);
}
结合上面的代码,总结一下现在源线程与目标线程各自要进行的操作
-
发送时,源线程的 todo 列表中会收到一个 binder_work,即上面的 tcomplete,其 type = BINDER_WORK_TRANSACTION_COMPLETE
-
发送时,目标线程的 todo 列表也会收到一个 binder_work,即代码中的 t->work,它的 type = BINDER_WORK_TRANSACTION。这一步的逻辑在 binder_proc_transaction() 中,没有分析
-
回复时,目标线程(这里为了统一,指的是服务端线程)的 todo 列表会收到 tcomplete
-
回复时,源线程(这里为了统一,指的是客户端线程)的 todo 列表会收到 t->work
至此,binder_thread_write 已经结束,数据复制到目标进程,且已唤醒相应的线程去执行。而且源线程与目标线程都有一个 work 需要执行
binder_proc_transaction
static int binder_proc_transaction(struct binder_transaction *t,
struct binder_proc *proc,
struct binder_thread *thread)
{
struct binder_node *node = t->buffer->target_node;
binder_node_lock(node);
// 从 Proc 中选取一个 thread 处理当前任务
// 其实就是选择 proc::waiting_threads 中的第一个元素
if (!thread && !pending_async)
thread = binder_select_thread_ilocked(proc);
if (thread) {
// 找到线程,加到线程的 todo 中
binder_enqueue_thread_work_ilocked(thread, &t->work);
} else if (!pending_async) {
// 加到进程的 todo 中
binder_enqueue_work_ilocked(&t->work, &proc->todo);
} else {
binder_enqueue_work_ilocked(&t->work, &node->async_todo);
}
if (!pending_async)
// 唤醒线程开始工作
binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */);
return 0;
}
binder_thread_read
回到 binder_ioctl_write_read 中,它调用完 binder_thread_write 后,如果源进程需要读数据(即 bwr.read_size > 0),就会调用 binder_thread_read。
这一部分可以参考 想掌握 Binder 机制?驱动核心源码详解和Binder超系统学习资源,想学不会都难 与 Binder系列2—Binder Driver再探。前一篇讲了整体逻辑,后一篇讲的是关于线程池部分
// thread 指源线程,proc 指源进程
// 这个方法客户端线程与服务端线程都会执行,所以逻辑有点绕
static int binder_thread_read(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed, int non_block)
{
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;
int ret = 0;
int wait_for_proc_work;
if (*consumed == 0) {
// 先回一个 BR_NOOP
if (put_user(BR_NOOP, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
}
retry:
binder_inner_proc_lock(proc);
// thread->todo 为空并且 thread->transaction_stack 为空时,wait_for_proc_work 才为 true
// 从 binder_thread_write() 过来时,wait_for_proc_work 为 false
wait_for_proc_work = binder_available_for_proc_work_ilocked(thread);
// 设置当前线程等待
thread->looper |= BINDER_LOOPER_STATE_WAITING;
trace_binder_wait_for_work(wait_for_proc_work,
!!thread->transaction_stack,
!binder_worklist_empty(proc, &thread->todo));
if (wait_for_proc_work) {
// 需要等待,但没有设置 binder 线程时这个代码块才有意义
// 这属性异常情况,不考虑
}
// non_block 为 false
if (non_block) {
} else {
// 当 wait_for_proc_work 为 true, 则进入休眠等待状态
// 这里很明显不成立
// 如果目标进程在某一时刻没有要处理的任务,就会在这里进入休眠,等到 binder_thread_write() 时唤醒才继续工作
// 休眠是通过 schedule() 方法实现的,这跟旧版本不同
ret = binder_wait_for_work(thread, wait_for_proc_work);
}
// 清理掉线程的等待状态
thread->looper &= ~BINDER_LOOPER_STATE_WAITING;
while (1) {
uint32_t cmd;
struct binder_transaction_data_secctx tr;
// binder_transaction_data 对象
struct binder_transaction_data * trd = &tr.transaction_data;
struct binder_work *w = NULL;
struct list_head *list = NULL;
struct binder_transaction *t = NULL;
struct binder_thread *t_from;
size_t trsize = sizeof(*trd);
// 先取 thread 自己的 todo 列表,没有的话取其 proc 的 todo 列表
if (!binder_worklist_empty_ilocked(&thread->todo))
list = &thread->todo;
else if (!binder_worklist_empty_ilocked(&proc->todo) &&
wait_for_proc_work)
list = &proc->todo;
else {
}
// 取列表的第一项,拿到 binder_work
w = binder_dequeue_work_head_ilocked(list);
if (binder_worklist_empty_ilocked(&thread->todo))
thread->process_todo = false;
// 根据 work 的 type 执行不同逻辑
switch (w->type) {
// 这个 case 对应两种情况:第一种目标线程返回数据唤醒源线程,第二种目标线程被唤醒
//
// 具体逻辑在上面分析过
case BINDER_WORK_TRANSACTION: {
binder_inner_proc_unlock(proc);
// 通过 w 找到它所属的 binder_transaction 对象
// binder_transaction 在 binder_thread_write() 中创建并用于传递给目标进程的对象
t = container_of(w, struct binder_transaction, work);
} break;
//省略一些 case
case BINDER_WORK_TRANSACTION_COMPLETE: {
binder_inner_proc_unlock(proc);
cmd = BR_TRANSACTION_COMPLETE;
// 向源线程发送一个 BR_TRANSACTION_COMPLETE 命令
// 还 binder_thread_write 中说过当驱动将数据复制完成后会将一个 tcomplete 添加到源线程的 todo 列表
// 到这里,就是驱动向源线程发送一个 BR_TRANSACTION_COMPLETE 命名
if (put_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
binder_stat_br(proc, thread, cmd);
} break;
//... 还是省略一些 case
}
if (!t)// 这样的判断只有 t== null 时才成立
continue;
// 执行到这里,说明第一个 case 成立
if (t->buffer->target_node) {
// 判断成立,说明是目标线程的逻辑
// target_node 的逻辑在上面分析 binder_transaction() 第一步时说过
struct binder_node *target_node = t->buffer->target_node;
struct binder_priority node_prio;
trd->target.ptr = target_node->ptr;
trd->cookie = target_node->cookie;
// 命令就是 BR_TRANSACTION
cmd = BR_TRANSACTION;
} else {
// 否则就是回复逻辑
trd->target.ptr = 0;
trd->cookie = 0;
cmd = BR_REPLY;
}
trd->code = t->code;
trd->flags = t->flags;
trd->sender_euid = from_kuid(current_user_ns(), t->sender_euid);
trd->data_size = t->buffer->data_size;
trd->offsets_size = t->buffer->offsets_size;
trd->data.ptr.buffer = (uintptr_t)t->buffer->user_data;
trd->data.ptr.offsets = trd->data.ptr.buffer +
ALIGN(t->buffer->data_size,
sizeof(void *));
// 将命令从驱动中写入源进程
if (put_user(cmd, (uint32_t __user *)ptr)) {
}
ptr += sizeof(uint32_t);
// 将 tr 复制到源进程中,tr 简单理解就是 trd,即 binder_transaction_data 对象
// ptr 是源进程中 Parcel::mIn 后的某一个地址
// 这一步就是将 tr 复制到 Parcel::mIn 中
if (copy_to_user(ptr, &tr, trsize)) {
}
ptr += trsize;
// 后面一堆清理工作,不看
}
done:
*consumed = ptr - buffer;
binder_inner_proc_lock(proc);
if (proc->requested_threads == 0 &&
list_empty(&thread->proc->waiting_threads) &&
proc->requested_threads_started < proc->max_threads &&
(thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
BINDER_LOOPER_STATE_ENTERED)) /* the user-space code fails to */
/*spawn a new thread if we leave this out */) {
proc->requested_threads++;
binder_inner_proc_unlock(proc);
// 发送 BR_SPAWN_LOOPER 命令,让源进程再准备一个 binder 线程
if (put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))
return -EFAULT;
binder_stat_br(proc, thread, BR_SPAWN_LOOPER);
} else
binder_inner_proc_unlock(proc);
return 0;
}
总结一下
客户端调用服务端时
- 由用户进程进入内核,然后由内核与服务进程建立内存映射、分配物理内存等,并将客户端发送的数据复制到服务端
- 如果
客户端需要等待返回数据:客户端线程会被内核暂停,等服务端返回后再唤醒 - 如果
客户端不需要返回数据,待内核将数据复制到服务进程后,客户端会由内核态返回至用户态,用户线程可以接着执行
物理内存分配
在分析 binder_transaction 时说过该方法会分配物理内存,同时为目标进程与内核建立映射。这里具体看一下,
-
调用 binder_alloc_new_buf:
// tr 为 binder_transaction_data 对象 // data_size 指源进程往内核中传递的 data 的数据量 // offsets_size 指源进程往内核中传递的 object 的数据量 t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size, tr->offsets_size, extra_buffers_size, !reply && (t->flags & TF_ONE_WAY), current->tgid); -
上一个方法直接调用 binder_alloc_new_buf_locked,后者有如下代码:
// 注意这里的传参,是将全部范围都传进去了 // 旧版本的时候只传了一页 ret = binder_update_page_range(alloc, 1, (void __user *) PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr); -
binder_update_page_range 基本上就和旧版本一样了:利用
alloc_page一次分配一页物理内存。只不过传入的参数不再是一页,而是全部内存// binder_update_page_range 节选 // 从头遍历到尾,一次一页为每一块虚拟内存分配物理内存 for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) { index = (page_addr - alloc->buffer) / PAGE_SIZE; // binder_alloc 的 pages,在 binder_mmap 中创建 page = &alloc->pages[index]; // 调用 alloc_page 开始分配物理内存 page->page_ptr = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); page->alloc = alloc; INIT_LIST_HEAD(&page->lru); user_page_addr = (uintptr_t)page_addr; // 将分配的物理内存映射到目标 // 这里的 vma 指的是 target_proc 中记录的 vma,也就是目标进程的信息 ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr); }重要结论
大部分 ipc 都是由 binder 线程处理,但递归调用时会由发起线程处理。具体原因在 binder_thread 中说过。