Android进程间通信(IPC)之实现细节篇(一) Binder驱动

一、写这系列文章的出发点

       首先跟大家介绍一下为什么要写名为“Android进程间通信(IPC)之实现细节篇”的博客, Binder通讯是Android系统框架的根本,Android系统中所有的系统服务和程序应用都必然是使用了Binder通讯的,但是对于普通开发者来说,它是透明的。而如果拿Android源码从框架层分析Binder通讯的话,又会疲于解释框架中java代码到本地代码地不断转换等问题。因此实现细节篇的目标聚焦于少量代码(而其实本篇章大部分文章连C++代码都不去涉及,实在不行就自己写示例代码)来解析通讯的具体细节实现,代码量尽可能涉及少,当然依旧会完整描述Binder通讯的实现机制。这个篇章会分为五篇文章来描述,分别是Binder驱动,ServiceManager,以及smtest(为了避免被负责代码所扰乱,自己写的一个测试程序,其功能类似于android源码框架的ServiceManager接口),服务程序gpioservicer,以及用户程序gpiouser。

二、Binder通讯模型

大家都知道Android系统中IPC是通过Binder,而非传统的管道、消息队列、socket等方式。Binder是C/S结构的,Android中Binder通讯的模型如下图所示:

 

其中,ServiceManager是服务管理程序,负责管理系统中所有的命名服务(匿名服务除外);服务程序有很多,最重要的莫过于SystemServer程序,其提供了Android系统的核心服务机制,如AMS(ActivityManagerService),WMS(WindowManagerService),IMS(InputManagerService)等;客户程序就比较好理解了,所有Android应用都是一个客户程序;Binder驱动则是Binder通讯机制的核心,负责前面三者的数据传输,Binder驱动运行在内核态,因此前面三者都需要通过系统调用才能使用Binder驱动。

Android系统Binder通讯的情况可以分为以下几类:

1、  ServiceManager程序注册实体到驱动中并阻塞在等待队列上等待被唤醒

2、  服务程序注册服务到ServiceManager中并创建服务线程等待应用程序访问

3、  客户程序向ServiceManager查询需要访问的服务

4、  客户程序通过ServiceManager返回的服务代理访问相应的服务程序

三、Binder驱动

binder通讯模型可得知,想弄清楚其实现的话,必然无法对Binder驱动视而不见了,虽然分析驱动是一件很麻烦的事情,因为会涉及到许多内核机制,但为了真正弄明白binder,也只好硬着头皮去分析驱动代码了。

这里的Binder驱动即linux内核中的驱动程序(其实Binder驱动移植到其他os都是可行的,前提是对linux内核和移植os都比较了解),生成的设备结点是/dev/binder,注册的是一个misc设备,但不涉及具体设备操作,binder驱动代码运行在内核态,调用binder驱动则是通过文件操作的系统调用接口(如:openmmapioctl等)。了解这些基本情况后,下面开始分析驱动源码。Binder驱动源码在linux内核中(貌似是2.6以后都有吧)都能找到,具体路径是drivers/staging/android/下的binder_trace.hbinder.hbinder.c文件。

       这里不得不提universus大神的文章Android Bander设计与实现 -设计篇该文章真可谓字句珠玑,每当分析代码比较困惑的时候,我总能在这篇文章中重新找到方向。当然大神能通过阅读代码把设计思想抽象出来,让阅读者能醍醐灌顶,但如果阅读者想更进一步提升能力,就需通过大神的抽象思想去重读代码,争取能从代码中印证其点点滴滴,并积累其优秀代码的设计思想。因此本文可以说是对universus大神文章某些部分的再解读,并辅以代码分析去探索其实现细节。

3.1数据结构

3.1.1 基础数据结构

这一类数据结构有个特性,就是都定义在binder.c文件中,也就是只有驱动源码会用到。虽然外部无需关注这些结构体,但是驱动代码逻辑则是以这几个结构体为核心的,因此先分析这些结构体的定义对于后面的分析也是大有益处的。这一类数据有binder_proc、binder_thread、binder_node、binder_buffer等。

其中binder_proc大致如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct binder_proc {  
  2.       struct hlist_node proc_node;  
  3.       struct rb_root threads;  
  4.       struct rb_root nodes;  
  5.       struct rb_root refs_by_desc;  
  6.       struct rb_root refs_by_node;  
  7.       void *buffer;  
  8.       ptrdiff_t user_buffer_offset;  
  9.    
  10.       struct rb_root free_buffers;  
  11.       struct rb_root allocated_buffers;  
  12.             size_tbuffer_size;  
  13.       struct list_head todo;  
  14.       wait_queue_head_t wait;  
  15.       int max_threads;  
  16. };  

从命名上就可得知,binder_proc用来表示调用/dev/binder文件操作接口的进程信息。结构体各主要变量包括:proc_node用于连接到全局列表中;threads表示该进程下所有的线程,用红黑树是为了快速查找;nodes代表服务的实体;refs_by_desc 和refs_by_node表示服务的引用;buffer是驱动缓存地址;user_buffer_offset则是应用缓存和驱动缓存的偏移;free_buffers表示空闲缓存块,根据缓存块大小做索引创建的红黑树;allocated_buffers表示已经分配的缓存块;buffer_size是缓存区大小,由mmap传入参数控制;todo是proc待处理任务列表;wait是等待队列;max_threads表示该进程最多可创建线程数。

      binder_proc对象在binder_open接口中创建,然后其对象指针保存在传入文件指针的private_data域,于是接下来该进程对binder的系统调用就都可以把binder_proc作为参数使用。而事实上,只有进程(主线程)才会调用binder_open接口,线程则调用binder_ioctl负责具体的通讯事宜。binder_proc、binder_thread、binder_node、binder_buffer等结构体则都以链表或者红黑树的方式连接在binder_proc对象上。

3.1.2 传输相关类结构

这一类数据结构和3.1.1相比,都定义在binder.h文件中,除了驱动会用到,用户空间程序也会包含该头文件来使用这些数据结构。可以分为两类:一类是传输协议相关,包括binder_driver_return_protocol、binder_driver_command_protocol;一类则是传输数据相关,包括binder_write_read、binder_transaction_data、flat_binder_object。

      binder_driver_return_protocol、binder_driver_command_protocol都是enum类型,前者表示返回协议字,后者表示发送协议字,协议字都由ioctl.h中的_IO系列宏生成。协议字的意思大致可通过命名知晓,具体的意义个人则认为通过代码逻辑来看更能直白的说明情况,因此这一部分说明放到后面的章节来描述。

      binder_write_read定义如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct binder_write_read {  
  2.             binder_long  write_size;  /*bytes to write */  
  3.             binder_long  write_consumed;     /* bytes consumed by driver */  
  4.             binder_ulong      write_buffer;  
  5.             binder_long  read_size;   /*bytes to read */  
  6.             binder_long  read_consumed; /*bytes consumed by driver */  
  7.             binder_ulong      read_buffer;  
  8. };  

      该结构体中字段的意思都蛮好理解,从字面上就可以看出表示的是读写buffer、buffer的size以及消耗的size。调用ioctl接口的时候,传递的参数就是该结构体。

 

      binder_transaction_data定义如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct binder_transaction_data {  
  2.       union {  
  3.            binder_size_t     handle; /* target descriptorof command trans */  
  4.            binder_ptr   ptr; /* target descriptor ofreturn trans */  
  5.       } target;  
  6.       binder_ptr   cookie;       /* target object cookie */  
  7.       unsigned int code;        /* transactioncommand */  
  8.    
  9.       /* General information about thetransaction. */  
  10.       unsigned int flags;  
  11.       pid_t          sender_pid;  
  12.       uid_t          sender_euid;  
  13.       binder_size_t     data_size;   /* number ofbytes of data */  
  14.       binder_size_t     offsets_size;     /* numberof bytes of offsets */  
  15.    
  16.       union {  
  17.            struct {  
  18.                  binder_const_ptr__user buffer;  
  19.                  binder_const_ptr __user offsets;  
  20.            } ptr;  
  21.            uint8_t  buf[8];  
  22.       } data;  
  23. };  

      该结构体表示Binder驱动接发数据包的格式,前面介绍的binder_write_read中写缓冲区就会包含一个cmd(binder_driver_command_protocol枚举值,一般常用的是BC_TRANSACTION,表示发送数据包)和该结构体对象。

接下来分析一下该结构中的重要字段:

其中union结构的target变量表示发送的目的地,在发送方这边会使用handle来表示服务程序的引用(如默认SMgr就是0,其他服务程序根据加入SMgr的顺序依次累加),ptr在驱动找到接收方服务程序的Binder实体后会设置为服务程序Binder对象的指针,然后再根据ptr的指针转换为具体子类的指针,就可以调用onTransact函数进入到服务程序的代码处理逻辑了。

其中code表示服务程序接口函数编号,其目的就是服务程序会把它提供的接口函数都编号,从0开始,然后客户程序调用服务程序接口的时候就要指定code来告诉服务程序其想调用的接口是那个,如Smgr中添加服务的接口函数编号:

static const int ADD_SERVICE_TRANSACTION= IBinder::FIRST_CALL_TRANSACTION + 2

其中data_size,offsets_size和data需要一起解释,data_size表示发送数据的大小,offsets_size表示buffer中有多少个Binder对象,而data中buffer表示发送数据的内容,data中offsets其实是指向一个数组,该数组的大小就是offsets_size,每个数组成员存放的是Binder对象在buffer中的偏移。

 

      flat_binder_object定义如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. struct flat_binder_object {  
  2.       binder_ulong           type;  
  3.       binder_ulong           flags;  
  4.       union {  
  5.            binder_ptr   __user binder;   /* localobject */  
  6.            binder_long  handle;       /* remote object*/  
  7.       };  
  8.       binder_ptr         __usercookie;  
  9. };  

      前面介绍到binder_transaction_data中data.ptr.offsets中存放着buffer中Binder对象的偏移,而这个Binder在data.ptr.buffer存放的就是一个flat_binder_object对象。其中flags表示该Binder是一个实体还是一个引用,如果是引用,则设置handle,如果是实体,则设置binder。值得一提的是,传输数据包中的Binder实体对象,驱动都会创建对应的Node,而Binder引用,也会创建相应的数据结构(binder_ref),这样每一个通过数据包在驱动中传递的Binder对象,都会被驱动所发现,从而建立起各个引用到实体的对应关系。

      binder_write_read、binder_transaction_data、flat_binder_object这三者之间的关系,在universus大神的博文中有一个图来描述:

3.2主要函数接口

3.2.1 open

       binder_open函数的主要功能其实在之前就提到了,就是创建一个binder_proc结构体对象,且初始化各个变量,然后把binder_proc对象指针保存在调用进程中该文件指针(/dev/binder)private_data变量中。其目的就是代表一个在Binder驱动中的“进程”,且将之后创建的threadnodebuffer等由该对象串联起来,使得缓存区管理、线程池管理、数据分发等功能可以正常完成。

3.2.2 mmap

       binder_mmap函数的主要功能是指定进程为Binder驱动分配的缓冲区大小,通过该函数映射出来的地址跟一般设备驱动的功能不一样,binder_mmap映射的地址并不可以直接写操作。Binder驱动也不会在mmap的时候就分配进程指定大小的内存,而是在需要的时候分配,且用完后就会立即回收,但分配的总共内存大小不会超出指定的缓冲区大小。

3.2.3 ioctl

       binder_ioctl函数的主要功能分为两种:一种是服务程序调用ioctl来注册looper,且进入looper阻塞在其等待队列上,等待被唤醒来提供服务;一种是客户程序调用ioctl来唤醒服务程序中的某个线程,然后等待其为自己提供服务,并返回结果。(注:这段描述的代码分析会在接下来的文章中体现)

3.3 Binder与传统IPC优势的体现

3.3.1 C-S模式

       传统IPC中只有socketCS模式的,但socket主要用于复杂网络环境中不同主机的通讯,而用在同一主机中进程间通讯,却难免显得过于复杂,也无形中增加了开销。那Binder通讯CS模式是如何形成的呢?先抛却实现不说,也许你可以尝试着去提出几个问题,以表达你心中的疑惑:

       1、一个程序如何提供服务?

       2、一个程序如何访问服务?

       然后你可以尝试着这样去想,从使用者的角度来说:我想访问一个服务,我希望以最简单的方式能查询到服务程序(如通过查询window就能找到窗口管理器服务)且返回我一个接口,然后通过接口很方便就访问相应的服务,最好不要让我等。

如果一个客户能这样表达需求,我想大部分的程序员能想到如下的模型:有一个服务管理器来管理所有的服务程序,且应用程序可以通过服务管理器来获得相应服务的接口,服务启动后一直等待着被唤醒来提供服务。

而事实上,Binder也是如此。这个服务管理器就是SMgr,而服务和服务的接口就是:Binder实体和Binder引用。

       Binder实体说直白点就是提供服务的程序在驱动中的“表示”,Binder引用则是这些服务程序在各个客户程序中的“代理”,“代理”在上层代码中是一个封装的类对象,但对于驱动而言,它就是一个从0开始递增的整型值,在驱动中称之为“引用号”更为直观,而0就被设定为SMgr实体的引用号。用universus大神形象的描述来说,SMgr就是第一只会下蛋的鸡,接下来所有的服务程序都会注册到SMgr(该步骤也是通过驱动的),且SMgr会根据服务的名字和引用号建立一个表(引用号是驱动分配的),然后客户程序通过名字向SMgr查询服务得到的就是一个引用号,在访问相应服务时,驱动就会根据引用号找到相应服务的Binder实体,进而访问到服务。(注:这段描述的代码分析会在接下来的文章中体现)

3.3.2内存只拷贝一次

       通过驱动为两个进程传递数据,作为程序员,我们能想到的最简单的方法就是:驱动通过copy_from_user()把发送方缓存区中的数据拷贝到内核内存,然后再通过copy_to_user()拷贝到接收方缓存区中,而这也是一些常用IPC(管道、消息队列等)的方式。Binder驱动只存在一次内存拷贝,这无疑会减少开销并提高通讯效率。

       而只拷贝一次的秘密在前面就已经跟大家提及了,原因就在是binder_mmap会把内核内存(前面也提到,这片内存区不是一次性申请的,驱动会自己管理这片内存)映射到接收方缓存区中,即只要通过copy_from_user()把发送方缓存区中的数据拷贝到内核内存,接收方就可以访问到数据了。

3.3.3 安全性

       Binder优秀的安全性能其实是始于Android的安全机制的,Android系统会为某一个安装的应用分配唯一的UID,且严格控制各个应用的权限。Binder驱动与传统IPC相比,在于能通过内核获取到进程可靠的UIDPID

3.3.4 缓存区管理

       前面已经提及到,Binder驱动中传递的数据包格式是binder_transaction_data,该结构体的分析前面也说到了,可以得知的是该结构的内存空间是由接收方提供,也就是说这部分内存其实是会拷贝两次的,就跟传统IPC方式一样,但这只是包头。传递的数据其实是存放在该结构中data.ptr.buffer中的,驱动会根据该buffer的大小从缓存区中找到最合适的空间,如果没找到就会申请内存且同时映射到接收方地址和内核地址上,接收方处理完数据后,就会发送BC_FREE_BUFFER来告知驱动释放该空间。

3.3.5 线程池管理

       当请求太多,服务程序处理不来的时候,驱动就会告知服务程序新建线程来提供服务,这就涉及到线程池管理了。但驱动不能无休止地要求服务进程创建线程来提供服务,进程可以通过ioctl传递BINDER_SET_MAX_THREADS命令来告知驱动最多支持的线程数目。如果所有的线程都处在服务中的时候,那新的请求就就只能挂在todo列表上了,相反服务线程没有事情做的时候,就会阻塞在等待队列上,等待被唤醒。

版权声明:本文为博主原创文章,未经博主允许不得转载。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值