想掌握 Binder 机制?驱动核心源码详解和Binder超系统学习资源,想学不会都难!

binder 驱动的整体分层如上图,下面先来宏观的了解下 getService() 在整个 Android 系统中的调用栈,ServiceManager 本身的获取:

与 ServiceManager 进行 IPC 通信:

「本文将主要分析此过程中 binder 驱动具体承担了哪些工作」,也就是上图中 IPCThreadState 与 binder 驱动的 ioctl 调用。

binder 驱动中做的工作可以总结为以下几步:

  1. 准备数据,根据命令分发给具体的方法去处理

  2. 找到目标进程的相关信息

  3. 将数据一次拷贝到目标进程所映射的物理内存块

  4. 记录待处理的任务,唤醒目标线程

  5. 调用线程进入休眠

  6. 目标进程直接拿到数据进行处理,处理完后唤醒调用线程

  7. 调用线程返回处理结果

在源码中实际会执行到的函数主要包括:

  1. binder_ioctl()

  2. binder_get_thread()

  3. binder_ioctl_write_read()

  4. binder_thread_write()

  5. binder_transaction()

  6. binder_thread_read()

下面按照这些 binder 驱动中的函数,以工作步骤为脉络,深入分析驱动中的源码执行逻辑,彻底搞定 binder 驱动!

1.binder_ioctl()

在 IPCThreadState 中通过系统调用 ioctl 陷入系统内核,调用到 binder_ioctl() 方法:

ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)

binder_ioctl() 方法中会根据 BINDER_WRITE_READ、BINDER_SET_MAX_THREADS 等不同 cmd 转调到不同的方法去执行,这里我们只关注 BINDER_WRITE_READ,代码如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){

int ret;

//拿到调用进程在 binder_open() 中记录的 binder_proc

struct binder_proc *proc = filp->private_data;

struct binder_thread *thread;

binder_lock(func);

//获取调用线程 binder_thread

thread = binder_get_thread(proc);

switch (cmd) {

case BINDER_WRITE_READ:

//处理 binder 数据读写,binder IPC 通信的核心逻辑

ret = binder_ioctl_write_read(filp, cmd, arg, thread);

if (ret)

goto err;

break;

}

之前文章介绍过 binder_open() 方法, binder_open() 方法主要做了两个工作:

  1. 创建及初始化每个进程独有一份的、用来存放 binder 相关数据的 binder_proc 结构体

  2. 「将 binder_proc 记录起来,方便后续使用」

正是通过 file 的 private_data 来记录的:

static int binder_open(struct inode *nodp, struct file *filp){

filp->private_data = proc;

}

拿到调用进程后,进一步通过 binder_get_thread() 方法拿到调用线程,然后就交给 binder_ioctl_write_read() 方法去执行具体的 binder 数据读写了。

可见 binder_ioctl() 方法本身的逻辑非常简单,将数据 arg 透传了出去。

下面分别来看 binder_get_thread()、binder_ioctl_write_read() 这两个方法。

2.binder_get_thread()

static struct binder_thread *binder_get_thread(

struct binder_proc *proc){

struct binder_thread *thread = NULL;

struct rb_node *parent = NULL;

//从 proc 中获取红黑树根节点

struct rb_node **p = &proc->threads.rb_node;

//查找 pid 等于当前线程 id 的thread,该红黑树以 pid 大小为序存放

while (*p) {

parent = *p;

thread = rb_entry(parent, struct binder_thread, rb_node);

//current->pid 是当前调用线程的 id

if (current->pid < thread->pid)

p = &(*p)->rb_left;

else if (current->pid > thread->pid)

p = &(*p)->rb_right;

else

break;

}

if (*p == NULL) {//如果没有找到,则新创建一个

thread = kzalloc(sizeof(*thread), GFP_KERNEL);

if (thread == NULL)

return NULL;

binder_stats_created(BINDER_STAT_THREAD);

thread->proc = proc;

thread->pid = current->pid;

init_waitqueue_head(&thread->wait); //初始化等待队列

INIT_LIST_HEAD(&thread->todo); //初始化待处理队列

//加入到 proc 的 threads 红黑树中

rb_link_node(&thread->rb_node, parent, p);

rb_insert_color(&thread->rb_node, &proc->threads);

thread->looper |= BINDER_LOOPER_STATE_NEED_RETURN;

thread->return_error = BR_OK;

thread->return_error2 = BR_OK;

}

return thread;

}

binder_thread 是用来描述线程的结构体,binder_get_thread() 方法中逻辑也很简单,首先从调用进程 proc 中查找当前线程是否已被记录,如果找到就直接返回,否则新建一个返回,并记录到 proc 中。

也就是说所有调用 binder_ioctl() 的线程,都会被记录起来。

3.binder_ioctl_write_read

此方法分为两部分来看,首先是整体逻辑:

static int binder_ioctl_write_read(struct file *filp,

unsigned int cmd, unsigned long arg,

struct binder_thread *thread){

int ret = 0;

struct binder_proc *proc = filp->private_data;

unsigned int size = _IOC_SIZE(cmd);

//用户传下来的数据赋值给 ubuf

void __user *ubuf = (void __user *)arg;

struct binder_write_read bwr;

//把用户空间数据 ubuf 拷贝到 bwr

if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {

ret = -EFAULT;

goto out;

}

暂时忽略处理数据逻辑…

//将读写后的数据写回给用户空间

if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {

ret = -EFAULT;

goto out;

}

out:

return ret;

}

起初看到 copy_from_user() 方法时难以理解,因为它看起来是将我们要传输的数据拷贝到内核空间了,但目前还没有看到 server 端的任何线索,bwr 跟 server 端没有映射关系,那后续再将 bwr 传输给 server 端的时候又要拷贝,这样岂不是多次拷贝了?

其实这里的 copy_from_user() 方法并没有拷贝要传输的数据,而仅是拷贝了持有传输数据内存地址的 bwr。后续处理数据时会根据 bwr 信息真正的去拷贝要传输的数据。

处理完数据后,会将处理结果体现在 bwr 中,然后返回给用户空间处理。那是如何处理数据的呢?所谓的处理数据,就是对数据的读写而已:

if (bwr.write_size > 0) {//写数据

ret = binder_thread_write(proc,

thread,

bwr.write_buffer, bwr.write_size,

&bwr.write_consumed);

trace_binder_write_done(ret);

if (ret < 0) { //写失败

bwr.read_consumed = 0;

if (copy_to_user(ubuf, &bwr, sizeof(bwr)))

ret = -EFAULT;

goto out;

}

}

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);

trace_binder_read_done(ret);

if (!list_empty(&proc->todo))

//唤醒等待状态的线程

wake_up_interruptible(&proc->wait);

if (ret < 0) { //读失败

if (copy_to_user(ubuf, &bwr, sizeof(bwr)))

ret = -EFAULT;

goto out;

}

}

可见 binder 驱动内部依赖用户空间的 binder_write_read 决定是要读取还是写入数据:其内部变量 read_size>0 则代表要读取数据,write_size>0 代表要写入数据,若都大于 0 则先写入,后读取。

至此焦点应该集中在 binder_thread_write() 和 binder_thread_read(),下面分析这两个方法。

4.binder_thread_write

在上面的 binder_ioctl_write_read() 方法中调用 binder_thread_write() 时传入了 bwr.write_buffer、bwr.write_size 等,先搞清楚这些参数是什么。

最开始是在用户空间 IPCThreadState 的 transact() 中通过 writeTransactionData() 方法创建数据并写入 mOut 的,writeTransactionData 方法代码如下:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,

int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer){

binder_transaction_data tr; //到驱动内部后会取出此结构体进行处理

tr.target.ptr = 0;

tr.target.handle = handle; //目标 server 的 binder 的句柄

//请求码,getService() 服务对应的是 GET_SERVICE_TRANSACTION

tr.code = code;

tr.flags = binderFlags;

tr.cookie = 0;

tr.sender_pid = 0;

tr.sender_euid = 0;

const status_t err = data.errorCheck(); //验证数据合理性

if (err == NO_ERROR) {

tr.data_size = data.ipcDataSize(); //传输数据大小

tr.data.ptr.buffer = data.ipcData(); //传输数据

tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);

tr.data.ptr.offsets = data.ipcObjects();

} else {…}

mOut.writeInt32(cmd); // transact 传入的 cmd 是 BC_TRANSACTION

mOut.write(&tr, sizeof(tr)); //打包成 binder_transaction_data

return NO_ERROR;

}

然后在 IPCThreadState 的 talkWithDriver() 方法中对 write_buffer 赋值:

bwr.write_buffer = (uintptr_t)mOut.data();

搞清楚了数据的来源,再来看 binder_thread_write() 方法,binder_thread_write() 方法中处理了大量的 BC_XXX 命令,代码很长,这里我们只关注当前正在处理的 BC_TRANSACTION 命令,简化后代码如下:

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;

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 == BR_OK) {

if (get_user(cmd, (uint32_t __user *)ptr)) // 读取一个 cmd

return -EFAULT;

//跳过 cmd 所占的空间,指向要处理的数据

ptr += sizeof(uint32_t);

switch (cmd) {

case BC_TRANSACTION:

case BC_REPLY: {

//与 writeTransactionData 中准备的数据结构体对应

struct binder_transaction_data tr;

//拷贝到内核空间 tr 中

if (copy_from_user(&tr, ptr, sizeof(tr)))

return -EFAULT;

//跳过数据所占空间,指向下一个 cmd

ptr += sizeof(tr);

//处理数据

binder_transaction(proc, thread, &tr, cmd == BC_REPLY);

break;

}

处理其他 BC_XX 命令…

}

//被写入处理消耗的数据量,对应于用户空间的 bwr.write_consumed

*consumed = ptr - buffer;

binder_thread_write() 中从 bwr.write_buffer 中取出了 cmd 和 cmd 对应的数据,进一步交给 binder_transaction() 处理,需要注意的是,BC_TRANSACTION、BC_REPLY 这两个命令都是由 binder_transaction() 处理的。

简单梳理一下,由 binder_ioctl -> binder_ioctl_write_read -> binder_thread_write ,到目前为止还只是在准备数据,没有看到跟目标进程相关的任何处理,都属于 “准备数据,根据命令分发给具体的方法去处理” 第 1 个工作。

而到此为止,第 1 个工作便结束,下一步的 binder_transaction() 方法终于要开始后面的工作了。

5.binder_transaction

binder_transaction() 方法中代码较长,先总结它干了哪些事:对应开头列出的工作,此方法中做了非常关键的 2-4 步:

  1. 找到目标进程的相关信息

  2. 将数据一次拷贝到目标进程所映射的物理内存块

  3. 记录待处理的任务,唤醒目标线程

以这些工作为线索,将代码分为对应的部分来看,首先是**「找到目标进程的相关信息」**,简化后代码如下:

static void binder_transaction(struct binder_proc *proc,

struct binder_thread *thread,

struct binder_transaction_data *tr, int reply){

struct binder_transaction *t; //用于描述本次 server 端要进行的 transaction

struct binder_work *tcomplete; //用于描述当前调用线程未完成的 transaction

binder_size_t *offp, *off_end;

struct binder_proc *target_proc; //目标进程

struct binder_thread *target_thread = NULL; //目标线程

struct binder_node *target_node = NULL; //目标 binder 节点

struct list_head *target_list; //目标 TODO 队列

wait_queue_head_t *target_wait; //目标等待队列

if(reply){

in_reply_to = thread->transaction_stack;

…处理 BC_REPLY,暂不关注

}else{

//处理 BC_TRANSACTION

if (tr->target.handle) { //handle 不为 0

struct binder_ref *ref;

//根据 handle 找到目标 binder 实体节点的引用

ref = binder_get_ref(proc, tr->target.handle);

target_node = ref->node; //拿到目标 binder 节点

} else {

// handle 为 0 则代表目标 binder 是 service manager

// 对于本次调用来说目标就是 service manager

target_node = binder_context_mgr_node;

}

}

target_proc = target_node->proc; //拿到目标进程

if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) {

struct binder_transaction *tmp;

tmp = thread->transaction_stack;

while (tmp) {

if (tmp->from && tmp->from->proc == target_proc)

target_thread = tmp->from; //拿到目标线程

tmp = tmp->from_parent;

}

}

target_list = &target_thread->todo; //拿到目标 TODO 队列

target_wait = &target_thread->wait; //拿到目标等待队列

binder_transaction、binder_work 等结构体在上一篇中有介绍,上面代码中也详细注释了它们的含义。比较关键的是 binder_get_ref() 方法,它是如何找到目标 binder 的呢?这里暂不延伸,下文再做分析。

继续看 binder_transaction() 方法的第 2 个工作,「将数据一次拷贝到目标进程所映射的物理内存块」

t = kzalloc(sizeof(*t), GFP_KERNEL); //创建用于描述本次 server 端要进行的 transaction

tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL); //创建用于描述当前调用线程未完成的 transaction

if (!reply && !(tr->flags & TF_ONE_WAY)) //将信息记录到 t 中:

t->from = thread; //记录调用线程

else

t->from = NULL;

t->sender_euid = task_euid(proc->tsk);

t->to_proc = target_proc; //记录目标进程

t->to_thread = target_thread; //记录目标线程

t->code = tr->code; //记录请求码,getService() 对应的是 GET_SERVICE_TRANSACTION

t->flags = tr->flags;

//实际申请目标进程所映射的物理内存,准备接收要传输的数据

t->buffer = binder_alloc_buf(target_proc, tr->data_size,

tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));

//申请到 t->buffer 后,从用户空间将数据拷贝进来,这里就是一次拷贝数据的地方!!

if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)

tr->data.ptr.buffer, tr->data_size)) {

return_error = BR_FAILED_REPLY;

goto err_copy_data_failed;

}

为什么在拷贝之前要先申请物理内存呢?之前介绍 binder_mmap() 方法时详细分析过,虽然 binder_mmap() 直接映射了 (1M-8K) 的虚拟内存,但却只申请了 1 页的物理页面,等到实际使用时再动态申请。也就是说,在 binder_ioctl() 实际传输数据的时候,再通过 binder_alloc_buf() 方法去申请物理内存。

至此已经将要传输的数据拷贝到目标进程,目标进程可以直接读取到了,接下来要做的就是将目标进程要处理的任务记录起来,然后唤醒目标进程,这样在目标进程被唤醒后,才能知道要处理什么任务。

最后来看 binder_transaction() 方法的第 3 个工作,「记录待处理的任务,唤醒目标线程」

if (reply) { //如果是处理 BC_REPLY,pop 出来栈顶记录的 transaction(实际上是删除链表头元素)

binder_pop_transaction(target_thread, in_reply_to);

} else if (!(t->flags & TF_ONE_WAY)) {

//如果不是 oneway,将 server 端要处理的 transaction 记录到当前调用线程

t->need_reply = 1;

t->from_parent = thread->transaction_stack;

thread->transaction_stack = t;

} else {

…暂不关注 oneway 的情况

}

t->work.type = BINDER_WORK_TRANSACTION;

list_add_tail(&t->work.entry, target_list); //加入目标的处理队列中

tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; //设置调用线程待处理的任务类型

list_add_tail(&tcomplete->entry, &thread->todo); //记录调用线程待处理的任务

if (target_wait)

wake_up_interruptible(target_wait); //唤醒目标线程

再次梳理一下,至此已经完成了前四个工作:

  1. 准备数据,根据命令分发给具体的方法去处理

  2. 找到目标进程的相关信息

  3. 将数据一次拷贝到目标进程所映射的物理内存块
    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

【Android思维脑图(技能树)】

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【Android高级架构视频学习资源】

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

img-nKadeIA7-1713614663695)]

[外链图片转存中…(img-dNaoYwVU-1713614663696)]

[外链图片转存中…(img-wx9fwwZE-1713614663697)]

[外链图片转存中…(img-4lgRsAPU-1713614663699)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

[外链图片转存中…(img-SIaF6JeO-1713614663700)]

【Android思维脑图(技能树)】

[外链图片转存中…(img-QiFSZQdK-1713614663701)]

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-SgwC48f1-1713614663702)]

【Android高级架构视频学习资源】

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Binder机制是Android系统中一种进程间通信(IPC)的机制,用于在不同进程之间进行数据交换和通信。通过Binder机制,Android应用程序可以实现进程间的数据共享和相互调用。 Binder机制基于C/S架构,主要由服务端、客户端和Binder驱动组成。服务端提供一个或多个服务,将其注册到Binder驱动中,并通过Binder对象发送和接收数据;客户端通过获取服务端的Binder对象,与其进行通信和交互;而Binder驱动负责管理Binder对象的创建、销毁和通信。 在Binder机制中,Binder对象充当了交互的桥梁。每个Binder对象都有一个唯一的标识符(具体是一个32位的整数),用于识别和查找对应的服务端。通过Binder对象,客户端和服务端可以进行方法调用、数据传输等操作。服务端通过Binder对象将数据发送给客户端,客户端通过Binder对象将数据传递给服务端。 Binder机制设计了多种数据结构来实现进程间通信,如BpBinder、BpRefBase、Parcel等。BpBinder负责处理进程间的通信,并通过Binder Proxy将方法调用转发给服务端;BpRefBase用于引用计数,确保对象在不再使用时能够正确释放;Parcel用于在进程间传递数据,可以实现序列化和反序列化。 总结来说,Android Binder机制是Android系统中一种进程间通信的机制,通过Binder对象实现不同进程之间的数据交换和通信。通过服务端、客户端和Binder驱动的协作,应用程序可以实现进程间的数据共享和相互调用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值