Android Binder 机制浅析——iOS开发者视角

633 阅读6分钟

背景

前两天跟同事聊到了Android平台的Binder机制,之前对Binder有过一点简单的了解,但是聊的过程中忽然冒出来一个问题,为什么不直接使用mmap文件MAP_SHARED模式呢?Binder所说的只拷贝一次的效率优势mmap也同样具备。Binder与mmap相比,其主要优势在哪里呢?

查了网上一些资料,很多看得也不明就里,于是只得去翻了一下解读Android源码的书[1],现总结如下。

Binder的通信机制

进程的虚拟地址空间分为用户地址空间和内核地址区间,分别对应用户态程序可以访问的虚拟地址区域和只有内核态才能访问的虚拟地址区域。

不同进程的虚拟地址空间的用户地址空间是互相隔离的,进程A无法直接使用进程B中的用户态虚拟地址,比如说A进程malloc得到的内存地址无法给B进程直接使用。为了实现多进程通信,必然需要内核态参与,因为不同进程的内核地址空间是共享的(虚拟地址相同),而内核地址空间只有内核态才能访问。

Binder在文件系统中创建了一个Binder设备文件:/dev/binder,其驱动程序工作在内核态。各进程即通过此驱动程序进行通信。

1. Binder驱动程序初始化

Binder在其驱动程序初始化函数binder_init中向操作系统注册了一个Binder设备(misc类型),并指定了其设备文件操作方法的具体实现(binder_fops),详见以下代码片断。

//对binder设备文件/dev/binder进行文件操作,驱动程序内部会调用binder_xxx方法
static struct file_operations binder_fops = {
    .owner = THIS_MODULE,
    .poll = binder_poll,
    .unlocked_ioctl = binder_ioctl,
    .mmap = binder_mmap,
    .open = binder_open,
    .flush = binder_flush,
    .release = binder_release,
};

//将binder定义为misc设备
static struct miscdevice binder_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "binder",
    .fops = &binder_fops
};


//binder设备驱动程序的初始化函数
static init __init binder_init(void)
{
    ...
    
    //注册binder设备,具体定义见上方static变量定义
    ret = misc_register(&binder_miscdev);
    
    ...
}

由以上代码可知,当用户态应用程序对 /dev/binder 文件调用 open 方法时,工作在内核态的Binder驱动程序会调用对应的binder_open方法,同理,对其调mmap方法时,驱动程序会调用binder_mmap方法。

2. Binder的文件操作

以下讲一下这几个文件操作方法内部进行的主要工作:

  • open —— binder_open

    • 创建binder_proc结构体 proc,并进行初始化和注册
      • 每个参与Binder通信的进程都有一个对应的binder_proc结构体数据,其中存了跟进程相关的各种信息
    • 将 proc 存在跟文件句柄相关的 struct file 结构体中(filep->private_data)中,这样后续拿到filep的mmap等方法就能顺利拿到proc了
  • mmap —— binder_mmap

    • 调用完open后还需要调用mmap将Binder设备文件映射到进程虚拟地址空间才能使用Binder进程间通信机制
    • /dev/binder是个虚拟设备,mmap主要作用是为当前进程分配内核缓冲区,用来传输进程间通信数据

    :这里用户态调用mmap时传入的参数为:PROT_READ, MAP_PRIVATE,即映射的此段内存区域在用户态是只读且私有的——别的进程不能通过mmap调用对它进行操作,对它的写操作要通过binder提供的别的方式,这样binder即可对其进行权限管控。直接使用mmap的话无法做管控,另外,binder除了数据传输以外还做了很多其他周边功能(如serviceManager等),全都封装在/dev/binder这一个设备文件中,如果直接用mmap的话权限管控无法闭环,会破坏系统的健壮性。

    以下概要地列出binder_mmap中的主要工作:

    //filep对应open()返回的句柄,其中filep->private_data存了struct binder_proc指针
    //vma代表一段用户态地址空间(虚拟地址空间)
    static int binder_mmap(struct file *filep, struct vm_area_struct *vma)
    {
        1. 确保vma指定的内存大小不超过4M
        2. 检查vma中指定的内存区域是只读的(不可写,不可拷贝)
        3. 在内核地址空间中分配一块与vma大小一样的一段空间(也是虚拟地址空间),叫做area
        4. 为vma和area分配实际物理页面(存在proc->buffers中),并将vma和area中的虚拟地址映射到分配的物理页的地址上
            * 实际先只分配一个物理页,后面需要的时候再分配更多物理页
    }
    

    一个使用Binder进程间通信机制的进程只有将Binder设备文件映射到自己的地址空间,Binder驱动程序才能为它分配内核缓冲区,以便用来传输进程间通信数据。

    • Binder驱动程序为进程分配的内核缓冲区有两个地址,其中一个是用户空间地址(对应以上代码片断中的vma),一个是内核空间地址(对应代码片段中的area)
      • 真正的内核缓冲区数据存在一系列物理页面中,这些物理页面会同时被映射到进程的用户地址空间和内核地址空间中
    • 当Binder驱动程序要将一块数据传输给一个进程B时,它可以先把这块数据保存在为B分配的内核缓冲区中,然后再把这块内核缓冲区的用户空间地址告诉B,则B就可以读取缓冲区中的数据了。

上面讲了驱动程序如何把数据传给B,那么A如何把数据传给驱动程序呢?这里就会用到write方法,在驱动程序内部,write/read方法都会被转为binder_ioctl(BINDER_WROTE_READ...)方法。

  • write/read —— binder_ioctl(BINDER_WRITE_READ)
    • 首先调用 copy_from_user 将一些处于用户地址空间中的 meta data(bwr,比如待传输数据的用户区首地址、size等)拷贝到内核区(注:不是实际数据),以便后续步骤中使用
    • 若 bwr 中有要写的数据,则调用 binder_thread_write 进行写操作
      • binder_thread_write 会调用 copy_from_user 将数据从用户区拷到目标内核缓冲区
    • 若 bwr 中有要读的数据,则调用 binder_thread_read 进行读操作
      • 只操作meta data,不需要真正拷贝数据,因为数据的消费方(如进程B)可以直接用已知的用户态虚拟地址对缓冲区数据进行直接读取

写在后面

本文对Binder底层通信机制做了一些简单的梳理,受作者知识所限,很多细节没有详细展开。如果有写错或理解错的地方,欢迎读者在评论区批评指正。 另外,对于希望了解更多细节或其他“上层建筑”的读者,推荐翻阅参考资料或其他更详细的文章。

参考资料

  • [1] 《Android系统源代码情景分析》by 罗升阳