本文设计了很多源代码和逻辑,都有尽可能多的绘图辅助。可能需要一些精力才能完全了解,建议收藏。
应用程序中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调用中绑定器驱动程序内部的特定执行逻辑。这部分也是活页夹机制中最难的部分,掌握了最难的部分可以大大提高信心。