Android-Framework(八)-从驱动层来分析服务的添加过程

文章内容

视频素材:[Android Binder驱动-从驱动层来分析服务的添加过程]

Android Binder驱动:从驱动层分析服务的添加过程

一、Media 服务注册流程

一个 Media 服务调用 addService 时,用户态只是把服务名和 Binder 对象写进 Parcel,再发起一次 ioctl。驱动随后要解决三个问题:谁有资格接收“添加服务”请求,接收线程睡在哪里,以及带着 Binder 对象的数据怎样跨进程送达。

沿着 binder_ioctl → binder_get_thread → BINDER_SET_CONTEXT_MGR → binder_thread_read → binder_transaction 追下去,这三个问题会落到内核对象和队列操作上。客户端进程把事务交给 Binder 驱动,驱动定位 ServiceManager,转换对象引用,把事务挂入待办队列,最后唤醒 ServiceManager 处理 addService。

Binder驱动层服务添加全局架构图

二、全局概览

驱动为每个打开 /dev/binder 的进程创建一个 binder_proc,并在 open 阶段放进 file->private_data。它是进程在 Binder 驱动中的身份证,其中 nodes 红黑树保存本进程拥有的 Binder 实体,threads 红黑树保存参与 Binder 通信的线程。
Binder 驱动核心数据结构

binder_thread 细化到线程级,记录线程状态、todo 工作队列和 wait 等待队列。binder_node 表示服务端拥有的内部 Binder 对象,内嵌 rb_node,能直接挂入 binder_proc->nodes。binder_ref 表示另一个进程对该节点的外部引用:同一份引用既能按节点指针查找,也能按用户态使用的 handle 查找。事务本身由 binder_transaction 描述,并通过 binder_work 接入待处理队列。

binder_proc/binder_thread/binder_node数据结构关系

Binder 所说的“一次拷贝”,指事务正文只执行一次从发送方用户空间到内核缓冲区的复制。驱动分配的物理页同时映射到内核地址和接收进程的 Binder mmap 区域;写进内核映射后,接收方能从自己的用户态映射读取同一页,不必再做一次内核到用户空间的正文复制。

一次拷贝内存布局

三、入口:binder_ioctl

用户态对 Binder 驱动的控制都从 ioctl(fd, cmd, arg) 进入:fd 定位已打开的设备文件,cmd 表示命令,arg 指向命令参数。binder_ioctl 从 filp->private_data 取回 binder_proc,再按命令分发。与服务添加关系最密切的命令有三个:BINDER_SET_CONTEXT_MGR 注册上下文管理者,BINDER_WRITE_READ 收发协议命令和事务,BINDER_VERSION 查询协议版本。

c
// 代码位置 /kernel/common/drivers/android/binder.c 第 5003—5135 行// binder_ioctl: 驱动层所有操作的入口static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){    struct binder_proc *proc = filp->private_data;    // 根据 cmd 分发到不同处理函数    switch (cmd) {        case BINDER_SET_CONTEXT_MGR:            ret = binder_ioctl_set_ctx_mgr(filp, arg);            break;        case BINDER_WRITE_READ:            ret = binder_ioctl_write_read(filp, arg, ...);            break;        case BINDER_VERSION:            // 获取 binder 驱动版本号            break;    }}

四、线程管理:binder_get_thread

Linux 用 task_struct 同时描述进程和线程,Binder 驱动则按当前任务的 pid 建立 binder_thread。查找发生在 proc->threads 红黑树中。树节点嵌在业务结构里,所以源码常用 rb_entry;它建立在 container_of 之上,能由成员地址反推出外层 binder_thread 地址。

首次进入驱动而没有命中时,函数分配并初始化线程对象,建立等待队列和 todo 链表,再插入红黑树。之后同一线程进入 ioctl 就能复用它。

c
// 代码位置 /kernel/common/drivers/android/binder.c 第 4678—4697 行// binder_get_thread: 查找或创建当前线程的 binder_threadstatic struct binder_thread *binder_get_thread(struct binder_proc *proc){    struct rb_node *parent = NULL;    struct binder_thread *thread;    // 在 threads 红黑树中查找    while (*p) {        parent = *p;        thread = rb_entry(parent, struct binder_thread, rb_node);        if (current->pid < thread->pid)            p = &(*parent)->rb_left;        else if (current->pid > thread->pid)            p = &(*parent)->rb_right;        else            return thread; // 找到已有线程    }    // 未找到则创建新线程    thread = kzalloc(sizeof(*thread), GFP_KERNEL);    thread->proc = proc;    thread->pid = current->pid;    init_waitqueue_head(&thread->wait); // 初始化等待队列    INIT_LIST_HEAD(&thread->todo);       // 初始化工作队列    rb_insert_color(&thread->rb_node, &proc->threads); // 插入红黑树    return thread;}

五、注册管理者:BINDER_SET_CONTEXT_MGR

ServiceManager 启动后发送 BINDER_SET_CONTEXT_MGR,请求成为上下文管理者。老版本实现用静态的 context_binder_node 记录这一身份;它只能成功设置一次,第二个进程注册会得到 -EBUSY。现代内核可能把它收进 binder_context,约束仍相同:一个 Binder 上下文只有一个管理者。

c
// 代码位置 /kernel/common/drivers/android/binder.c 第 4895—4941 行// binder_ioctl_set_ctx_mgr: 注册为上下文管理者static int binder_ioctl_set_ctx_mgr(struct file *filp, void __user *arg){    // 静态变量,只能设置一次    if (context_binder_node != NULL) {        // 已经有管理者,返回错误        return -EBUSY;    }    // 创建 binder_node 作为管理者的标识    context_binder_node = binder_new_node(proc, 0, 0);    // ptr 和 cookie 都设为 0,表示这是管理者的特殊节点    context_binder_node->ptr = 0;    context_binder_node->cookie = 0;    return 0;}

驱动创建 binder_node,并把内嵌的 rb_node 挂到 ServiceManager 的 nodes 红黑树。该节点的 ptr 和 cookie 都为 0,类型语义对应 BINDER_TYPE_BINDER。客户端把事务目标写成 handle 0 时,驱动据此找到上下文管理者,不需要客户端预先持有普通引用。

六、循环等待:BC_ENTER_LOOPER

完成注册后,ServiceManager 通过 BINDER_WRITE_READ 写入 BC_ENTER_LOOPER。驱动给当前 binder_thread 设置 looper 标志,表明它可以持续接收事务。此后用户态反复进入读路径,binder_thread_read 检查线程及进程的 todo 队列;没有工作时,线程在 wait_event_interruptible 上可中断睡眠。

等待队列负责“睡在哪里”,工作队列负责“醒来要做什么”。生产事务的一方先把 binder_work 接入 todo,再唤醒对应 wait queue。这个顺序保证线程被唤醒后能观察到工作;条件不满足时,等待宏还会继续睡眠,抵御无效唤醒。

七、核心:binder_transaction

Binder 对象翻译机制

服务添加完整流程

客户端经 BINDER_WRITE_READ 写入 BC_TRANSACTION,驱动最终进入 binder_transaction。对添加服务而言,事务目标是 handle 0,也就是 ServiceManager;Parcel 内还携带待注册服务的 flat_binder_object。

c
// 代码位置 /bionic/libc/kernel/uapi/linux/binder.h 第 47—56 行// flat_binder_object: 传递 binder 对象的结构体struct flat_binder_object {    __u32 type;      // 对象类型:BINDER_TYPE_BINDER 或 BINDER_TYPE_HANDLE    __u32 flags;     // 标志位    union {        binder_uintptr_t binder;  // 弱引用地址        binder_size_t handle;     // handle 值(驱动层分配)    };    binder_uintptr_t cookie;  // 强引用地址};

事务处理可以拆成五个动作。

  1. 在发送方落实 node。 驱动根据对象偏移扫描 Parcel。遇到 BINDER_TYPE_BINDER,就用 binder 与 cookie 在发送方 proc->nodes 中查找;首次传递时创建 binder_node。这里的两个值是用户态地址标识,驱动不会把它们当作可直接解引用的内核指针。
  2. 在接收方建立 ref。 驱动为 ServiceManager 创建指向该 node 的 binder_ref。引用进入按 node 指针检索和按 handle(源码常称 desc)检索的索引,使驱动能双向定位。
  3. 改写对象表示。 接收方不能使用发送方地址,因此驱动把 BINDER_TYPE_BINDER 改成 BINDER_TYPE_HANDLE,并写入 ref->desc。普通 handle 通常从 1 分配;0 留给上下文管理者。是否恰好连续,还要服从具体版本的分配与复用策略。
  4. 复制事务数据。 copy_from_user 把发送方的 Parcel 数据及 offsets 信息写入驱动为目标进程分配的 buffer。对象转换发生在驱动可访问的目标缓冲区中。
  5. 排队并唤醒。 事务工作项进入目标线程或目标进程的 todo,驱动唤醒合适的等待线程;发送线程的队列还会收到 BINDER_WORK_TRANSACTION_COMPLETE,稍后由读路径返回 BR_TRANSACTION_COMPLETE。

handle分配和type替换流程

c
// 代码位置 /kernel/common/drivers/android/binder.c 第 3000—3675 行// binder_transaction: 核心事务处理函数static void binder_transaction(struct binder_proc *proc,                               struct binder_thread *thread,                               struct binder_transaction_data *tr, int reply) {    // 1. 在发送方创建 binder_node(首次传递时)    if (tr->target.handle == 0) {        // 传的是内部对象,需要创建 node        struct binder_node *node = binder_new_node(proc, ptr, cookie);        // 挂载到 proc->nodes 红黑树    }     // 2. 在接收方创建 binder_ref(首次传递时)    struct binder_ref *ref = binder_get_ref_for_node(target_proc, node);    // 以 PTR 为 key 和以 handle 为 key 分别挂载     // 3. 替换 type 和 handle    // type 从 BINDER_TYPE_BINDER 变为 BINDER_TYPE_HANDLE    // handle 值从 1 开始累加    t->type = BINDER_TYPE_HANDLE;    t->handle = ref->desc; // desc 从 1 递增     // 4. 数据拷贝:将数据写入目标进程的 buffer    copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size);     // 5. 唤醒目标进程    list_add_tail(&w.entry, &target_thread->todo); // 加入目标 todo 队列    wake_up_interruptible(&target_thread->wait);    // 唤醒目标等待队列     // 6. 向本进程添加 TRANSACTION_COMPLETE    struct binder_work *work = kzalloc(sizeof(*work), GFP_KERNEL);    work->type = BINDER_WORK_TRANSACTION_COMPLETE;    list_add_tail(&work->entry, &thread->todo);}

binder_transaction完整流程

八、一次拷贝的数据路径

把路径连起来看,发送方先在 write buffer 中组织 binder_transaction_data、Parcel 正文和对象偏移表,随后通过 ioctl 交给驱动。驱动校验用户地址和长度,分配属于目标 binder_proc 的 buffer,再用 copy_from_user 完成正文复制。该 buffer 背后的物理页已经映射进 ServiceManager 的 Binder 地址区,驱动返回 BR_TRANSACTION 时只需给出接收方可访问的 buffer 地址和大小。

ServiceManager 被唤醒后,binder_thread_read 从 todo 取出 BINDER_WORK_TRANSACTION,把必要的命令元数据写到读缓冲区。用户态随后直接解析 mmap 区域中的 Parcel。所谓一次拷贝只计算大块事务数据;ioctl 控制结构、返回命令等小数据仍会经过用户与内核边界,也不能把 mmap 理解成两个进程直接共享彼此的任意内存。

九、总结

  • ServiceManager 用 BINDER_SET_CONTEXT_MGR 建立唯一的特殊 node,取得 handle 0 对应的管理者身份;
  • 它用 BC_ENTER_LOOPER 声明接收能力,并在 binder_thread_read 的等待队列上休眠;
  • 客户端事务进入 binder_transaction 后,驱动创建或复用 node/ref、把地址形式改成 handle、复制数据、挂入 todo 并唤醒接收线程。

阅读源码时,可以抓住这条主链:binder_ioctl 找到进程上下文,binder_get_thread 找到线程上下文,设置 context manager 后由 binder_thread_read 保持接收循环,最终由 binder_transaction 完成对象翻译和跨进程投递。不同 Android 与 Linux 版本的函数签名、锁和数据结构会有些变化。


本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android-Framework(八)-从驱动层来分析服务的添加过程 · Yinchen 的博客