binder机制 掌握 binder 机制 驱动核心源码详解

栏目:民生 2021-09-20 09:50:05
分享到:

本文设计了很多源代码和逻辑,都有尽可能多的绘图辅助。可能需要一些精力才能完全了解,建议收藏。

应用程序中getService的执行需要通过跨进程的绑定器与ServiceManager通信,绑定器将通过框架、Natve层和Linux内核驱动程序运行。

活页夹驱动程序的整体分层如上所示。我们先来了解一下getService在整个安卓系统中的调用栈以及ServiceManager本身的获取:

仪表板组合仪表与服务管理器的通信:

“本文将主要分析绑定器驱动在这个过程中承担了什么”,即上图中绑定器驱动的IPCThreadState和ioctl调用。

活页夹驱动程序中完成的工作可以总结如下:

准备数据,并根据命令将其分发到特定的方法进行处理

查找关于目标进程的信息

将数据复制到目标进程映射的物理内存块一次

记录要处理的任务并唤醒目标线程

调用线程进入睡眠状态

目标进程直接获取数据进行处理,处理后唤醒调用线程

调用线程返回处理结果

源代码中实际执行的功能主要包括:

binder_ioctl

binder_get_thread

binder_ioctl_write_read

活页夹_线程_写入

binder_transaction

binder_thread_read

在这里,根据这些绑定器驱动中的功能,以工作步骤为脉络,深入剖析驱动中的源代码执行逻辑,彻底搞定绑定器驱动!

1.binder_ioctl

在IPCThreadState中被系统调用ioctl捕获到系统内核中,并调用binder_ioctl方法:

在binder_ioctl方法中,会根据不同的cmd转移到不同的方法,如BINDER_WRITE_READ、BINDER_SET_MAX_THREADS等。这里我们只关注BINDER_WRITE_READ,代码如下:

上一篇文章介绍了binder_open方法。binder_open方法主要执行两项任务:

为每个进程创建并初始化一个唯一的binder_proc结构,以存储与binder相关的数据

“记录binder_proc以备后用”。

它由文件的私有数据记录:

获取调用进程后,通过binder_get_thread方法进一步获取调用线程,然后交给binder_ioctl_write_read方法进行具体的binder数据读写。

可以看出binder_ioctl方法本身的逻辑非常简单,数据arg传输的很彻底。

下面是两种方法:binder_get_thread和binder_ioctl_write_read。

2.binder_get_thread

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

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

3.binder_ioctl_write_read

这个方法分为两个部分,第一个是总体逻辑:

刚开始看到copy_from_user方法的时候很难理解,因为它好像把我们要传输的数据复制到了内核空,但是目前我在服务器端还没有看到任何线索,bwr和服务器端也没有映射关系,所以以后我们传输bwr到服务器端的时候,还要再复制一次,这样岂不是会被复制很多次?

实际上,这里的copy_from_user方法不复制要传输的数据,而只复制保存传输数据的内存地址的bwr。在随后的数据处理中,将根据bwr信息复制要传输的数据。

数据处理后,处理结果会反映在bwr中,然后返回给用户空进行处理。你如何处理数据?所谓处理数据只是读写数据:

可以看到,binder驱动内部依赖于用户空之间的binder_write_read来决定是读还是写数据:其内部变量read_size>0表示读数据,write_size>0表示写数据;如果两者都大于0,将首先写入,然后读取。

在这一点上,重点应该放在binder_thread_write和binder_thread_read上,下面将对此进行分析。

4.binder_thread_write

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

最初,数据是通过用户之间的IPCThreadState的transact空中的writeransactiondata方法创建并写入mOut的。writeransactiondata方法的代码如下:

然后在IPCThreadState的talkWithDriver方法中为write_buffer赋值:

了解数据的来源,再看binder_thread_write方法。在binder_thread_write方法中,处理了大量的BC_XXX命令,代码非常长。这里我们只关注当前正在处理的BC_TRANSACTION命令。简化代码如下:

在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,到目前为止只是在准备数据,并没有看到任何与目标进程相关的处理,属于“准备数据并根据命令分发到特定方法进行处理”的首要任务。

至此,第一个作业完成,下一个binder_transaction方法终于开始了后面的工作。

5.binder_transaction

binder_transaction方法中的代码比较长,所以先总结一下它做了什么:对应于开头列出的工作,这个方法做了2-4个关键步骤:

查找关于目标进程的信息

将数据复制到目标进程映射的物理内存块一次

记录要处理的任务并唤醒目标线程

以这些任务为线索,将代码分成相应的部分。首先,找到目标流程的相关信息。简化代码如下:

像binder_transaction和binder_work这样的结构在上一篇文章中已经介绍过了,它们的含义在上面的代码中也有详细说明。关键是binder_get_ref方法。它如何找到目标活页夹?这里就不展开了,后面会分析。

继续看binder_transaction方法的第二个任务“将数据复制到目标进程映射的物理内存块一次”:

为什么要在复制前申请物理内存?之前在介绍binder_mmap方法时已经详细分析过了。虽然binder_mmap直接映射了虚拟内存,但它只应用于一个物理页面,然后在实际使用时动态应用。也就是说,当binder_ioctl实际传输数据时,它通过binder_alloc_buf方法申请物理内存。

此时,要传输的数据已经复制到目标进程,目标进程可以直接读取这些数据。下一步是记录目标进程需要处理的任务,然后唤醒目标进程,这样就知道目标进程被唤醒后需要处理哪些任务。

最后,我们来看看binder_transaction方法的第三个任务“记录要处理的任务并唤醒目标线程”:

前四项任务再次完成:

准备数据,并根据命令将其分发到特定的方法进行处理

查找关于目标进程的信息

将数据复制到目标进程映射的物理内存块一次

记录要处理的任务并唤醒目标线程

第一个作业涉及的方法是binder _ ioctl-> binder _ get _ thread-> binder _ ioctl _ write _ read-> binder _ thread _ write,主要涉及一些数据准备和方法跳转,但没有做什么实质性的工作。而binder_transaction方法做了非常重要的2-4个工作。

剩下的工作是:

调用线程进入睡眠状态

目标进程直接获取数据进行处理,处理后唤醒调用线程

调用线程返回处理结果

可以想象,5和6是并行处理的,而不是时间上的限制。让我们看看第五个作业:调用线程如何进入睡眠状态并等待服务器执行结果。

6.binder_thread_read

唤醒目标线程后,调用线程执行binder_thread_write并写入数据,然后返回binder_ioctl_write_read方法,然后执行binder_thread_read方法。

而调用线程的休眠是由这个方法触发的,下面将binder_thread_read分成两部分,第一部分是是否阻塞当前线程的判断逻辑:

Consumed表示用户空之间的bwr.read_consumed,其中为0,因此会在ptr中添加一个BR_NOOP。

如何理解wait_for_proc_work条件?在binder_transaction方法中,服务器端要处理的事务记录在当前调用线程thread->transaction_stack中;在thread->todo中记录当前调用线程的挂起任务。

因此,thread->transaction_stack和thread->todo在这里都不是空,wait_for_proc_work为false,这意味着当前线程还没有准备好阻塞。

然而,wait_for_proc_work并不是决定是否睡眠的最终条件。如果non_block始终为false,是否休眠当前线程取决于binder_has_thread_work的返回值。活页夹的工作方法如下:

Thread->todo不是空,所以binder_has_thread_work返回true,当前调用线程不进入睡眠状态,继续执行。你可能会想,调用线程的休眠不是由binder_thread_read方法触发的吗?这是真的,但这次不是。首先,分析binder_thread_read继续执行的逻辑:

在上面binder_transaction方法的末尾,将binder_work _ transaction _ complete类型的binder _ work添加到线程->todo。在这种情况下,binder_work被处理,一个BR_TRANSACTION_COMPLETE命令被添加到ptr。

结合当前逻辑,已经依次执行了binder_thread_write和binder_thread_read方法,并在binder_thread_read中向用户空传输了两个命令:BR_NOOP和BR_TRANSACTION_COMPLETE。

binder_ioctl的这个调用就完成了,然后它会回到IPCThreadState,掌握binder机制。先了解这些重点课!在详细分析了IPCThreadState中的代码后,这里就不展开了,简单总结一下后续执行的逻辑:

mIn中有两个命令BR_NOOP和BR_TRANSACTION_COMPLETE。首先,BR_NOOP命令被处理,这个命令什么也不做。因为talkwitdriver处于while循环中,所以它会再次进入talkwitdriver,但是此时,因为mIn中还有数据没有被读取,所以它不会调用binder_ioctl。

然后处理BR_TRANSACTION_COMPLETE命令。如果是单向,直接结束本次IPC调用;否则,再次输入talkWithDriver。第二次进入talkWithDriver时,bwr.write_size = 0,bwr.read_size > 0,因此将第二次调用binder_ioctl方法。在binder_ioctl的第二次执行中,bwr.write_size = 0,bwr.read_size > 0,因此不会执行binder_thread_write方法,而只会执行binder_thread_read方法。

第二次执行binder_thread_read时,thread->todo已经处理为空,但是thread->transaction_stack还没有空,wait_for_proc_work仍然为false。但是,决定是否睡眠的最终条件已经建立:binder_has_thread_work返回false,因此当前调用线程通过wait _ event _ freezable进入睡眠状态。

最后的

还剩下两个工作:

目标进程直接获取数据进行处理,处理后唤醒调用线程

调用线程返回处理结果

但是没有必要再看代码了,因为上面的方法已经覆盖了剩下的工作。对于getService,目标流程是ServiceManager,并详细分析了Service Manager中与绑定器驱动交互相关的代码的工作原理。

最后,上图总结了活页夹驱动所承担的工作。呼叫流程逻辑:

服务管理器端逻辑:

本节完整地分析了IPC调用中绑定器驱动程序内部的特定执行逻辑。这部分也是活页夹机制中最难的部分,掌握了最难的部分可以大大提高信心。