Binder入门(二)驱动

1,704 阅读26分钟

基础

  1. 下文为描述方便,将执行系统调用的进程称为源进程或当前进程(在 binder 中通过 current 获取),要调用到的称为目标进程
  2. binder 线上源码
  3. 非常通俗易懂的 binder 教程

ONE_WAY

binder 的调用分两种模式,one way 模式以及非 one way 模式。要注意:one_way 只在跨进程时才有效,不跨进程时无用

  1. ONE_WAY:异步调用。调用方不阻塞,源进程将数据发送完后就结束了。one way 意思为单程,只传输数据,不返回结果
  2. 非 ONE_WAY:阻塞等待目标进程返回结果
  3. one_way 在定义 aidl 时方法前用 oneway 修饰即可

与内核交互的步骤

一个进程与 binder 驱动交互,一般有以下几步:

  1. 先通过 binder_open 打开 binder 通信
  2. 再通过 binder_mmap 执行内存映射,这里是在接收进程空间与内核空间进行内存映射
  3. 通过 binder_ioctl 进行数据交换。这个过程也分为四步
    • 先使用 copy_from_user 从调用进程中复制信息,包括:要写入的数据量、要读的数据量等。握手过程
    • 使用 binder_thread_write 从调用进程空间读
    • 使用 binder_thread_read 往调用进程空间写
    • 再将第一步的握手信息 copy 回调用进程空间
  4. 用户进程与内核通过 binder 通信时,它们的数据格式是约定好的。即第几个字节传什么值,都是固定的。这样内核才能解析出具体的值

常用结构体

链接

总结几个比较重要的:

内容初始化时机
vm_area_struct调用进程虚拟地址的描述,它是一段连接的虚拟内存空间binder_mmap 时会用作参数
binder_write_read记录读写数据第一次 copy_from_user 时填充
binder_transaction_databinder 通信的事务数据记录有调用者的 pid/uid,目标进程等信息
binder_proc应用进程在 binder 驱动中的结构体,存储该进程相关信息binder_open 时初始化
binder_thread应用进程的线程在驱动中的映射
binder_allocbinder_proc 中用于操作 binder_buffer 的结构体binder_open 时初始化
binder_buffer描述一次交互过程中使用到的内存mmap()创建用于 Binder 传输数据的缓存区
binder_nodeIBinder 对象在驱动中的表示,一般称之为 binder 实体在 IBinder 对象第一次进入驱动时创建
binder_ref一般称之为 binder 引用。它是将 IBinder 对象的句柄与 Binder 实体进行关联的类在 IBinder 对象第一次进入驱动时创建
binder_transaction一次进程通信的抽象含有属性 binder_work,所以可以添加到 binder_thread/binder_proc 的 todo 列表中
binder_workbinder_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;

请求码

进程与驱动交互时,都通过请求码决定要进行的操作,因此请求码有两个流向:

  1. 从进程传递到驱动,以 BC_ 开头
  2. 从驱动传递到进程,以 BR_ 开头。binder return 的缩写。

比如一次请求,大体上涉及到以下几个请求码。来自理解Android Binder机制(1/3):驱动篇 (paul.pub)

image.png

其中第一步 BC_ENTER_LOOPER 是 server 进程被 fork 出来后主动向 binder 驱动注册的,用于告知驱动自己的 binder 主线程已就绪。

  1. BR_TRANSACTION_COMPLETE:数据已复制到目标进程。如果此时是 one way 模式,源线程就可以继续执行 ipc 后面的代码了
  2. BR_REPLY:目标线程已处理完请求,需要源线程到驱动中读取处理数据
  3. 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

  1. binder 的初始化函数是 binder_init,该方法中调用了 misc_register() 向内核注册 binder 驱动
  2. 注册时传入了 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 中

  1. binder_procs 是一个哈希表(java 低版本的 HashMap 实现方式)
  2. 创建 binder_proc 对象,用于存储调用进程信息,然后使用 binder_procs 统一管理所有的 binder_proc 对象

image.png

binder_mmap

完成内存映射功能。或者说进程预留一块虚拟内存,用于后续接收数据。在 ioctl 的时候才会为这块虚拟内存对应物理内存

  1. 限制大小源进程与内核地址映射的大小最多为 4M
  2. 这一步并没有在内核空间申请虚拟内存与源进程进行对应,只是生成 binder_buffer 对象,由它和 binder_alloc 共同存储源进程要映射的内存的起始位置以及大小
  3. 旧版本上这里会分配一页的物理内存,但新版本上没有了

image.png

上面主要方法 binder_alloc_mmap_handler()

image.png

通过 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

iShot2021-09-09 19.54.11.png

上面就是 binder_mmap 的整个流程,简单讲:生成 binder_buffer 对象,然后由 binder_alloc 管理。同时将源进程用于映射的内存起始位置以及大小,记录下来。大小分同步、异步:异步的大小是同步的 1/2

旧版上这个方法还会分配一页的物理内存,然后映射到源进程以及内核进程,但新版本上已经无了。

binder_ioctl

根据传入的不同命令执行不同的操作,两个进程间的收发数据的过程都由该方法完成

  1. 首先通过 binder_get_thread() 获取一个 binder_thread 对象,是源进程中的线程,不是目标进程中的
  2. 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 对象,该对象里属性指向了用户实际要发送的数据所在的位置,下一步就是建立目标进程与内核的映射,并复制数据。从常理推测,这一步需要处理的任务有三个:

  1. 寻找到目标进程
  2. 将数据复制到目标进程,伴随着对物理内存的分配
  3. 唤醒目标线程干活

分析该方法最主要一点是 发送和回复时,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);
   }

结合上面的代码,总结一下现在源线程与目标线程各自要进行的操作

  1. 发送时,源线程的 todo 列表中会收到一个 binder_work,即上面的 tcomplete,其 type = BINDER_WORK_TRANSACTION_COMPLETE

  2. 发送时,目标线程的 todo 列表也会收到一个 binder_work,即代码中的 t->work,它的 type = BINDER_WORK_TRANSACTION。这一步的逻辑在 binder_proc_transaction() 中,没有分析

  3. 回复时,目标线程(这里为了统一,指的是服务端线程)的 todo 列表会收到 tcomplete

  4. 回复时,源线程(这里为了统一,指的是客户端线程)的 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;
}

总结一下

客户端调用服务端时

  1. 由用户进程进入内核,然后由内核与服务进程建立内存映射、分配物理内存等,并将客户端发送的数据复制到服务端
  2. 如果客户端需要等待返回数据:客户端线程会被内核暂停,等服务端返回后再唤醒
  3. 如果客户端不需要返回数据,待内核将数据复制到服务进程后,客户端会由内核态返回至用户态,用户线程可以接着执行

物理内存分配

在分析 binder_transaction 时说过该方法会分配物理内存,同时为目标进程与内核建立映射。这里具体看一下,

  1. 调用 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);
    
  2. 上一个方法直接调用 binder_alloc_new_buf_locked,后者有如下代码:

    // 注意这里的传参,是将全部范围都传进去了
    // 旧版本的时候只传了一页
    ret = binder_update_page_range(alloc, 1, (void __user *)
       PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr);
    
  3. 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);
    }
    

    重要结论

    1. 大部分 ipc 都是由 binder 线程处理,但递归调用时会由发起线程处理。具体原因在 binder_thread 中说过。