从 Linux 内核的角度来看 Binder 驱动

关于进程间通讯咱们是再熟悉不过了,有时面试也常常被问到你了解 IPC 吗?咱们通常都会答 AIDL ,Binder 驱动,共享内存?若是要咱们再说详细点呢?或者说说共享内存的具体实现?这里推荐一篇罗升阳的博客 《Android进程间通讯(IPC)机制Binder简要介绍和学习计划》。本文是基于 linux 进程间通讯来写的,咱们都知道 Android 是基于 linux 内核,所以了解了 linux 进程间通讯也就基本了解了 Android 底层进程间通讯。去年初来深圳去腾讯面试,被问到了知道进程间通讯吗?我说 binder 驱动,还有吗?我说 Socket ,还有吗?我说其余的就不了解了。最后面试的结果也是不出所料,GG。linux

首先来了解一下进程间通讯的本质是什么。在 Android 开发者须要知道的 Linux 知识 一文中提到,一个完整的进程在 32 位系统上的虚拟内存分布为: 0-3G 是用户空间,3-4G 是内核空间。操做系统在映射开辟物理内存时,每一个进程的用户空间会映射到不一样区域,每一个进程的内核空间会映射到同一区域(能够简单的这么理解)。所以若是两个进程间须要传递数据是不能直接访问的,要交换数据必须经过内核,在内核中开辟一块缓冲区,进程 A 把数据拷贝到内存缓冲区,进程 B 再从内核缓冲区把数据读走,这种机制称为进程间通讯(IPC,InterProcess Communication),所以进程间通讯得要借助内核空间。android

在 linux 中常见的进程间通讯方式有:文件,管道,信号,信号量,共享内存,消息队列,套接字,命名管道,随着 linux 的发展到目前最最多见的有:面试

  • 管道(使用最简单)
  • 信号(开销最小)
  • 共享映射区(无血缘关系)
  • 本地套接字(低速稳定)

对于一个 Android 开发者来讲,最最最多见的就只剩共享映射区了,像咱们最熟悉的 Binder 驱动,腾讯开源的 MMKV, 本身实现高性能的日志库等等,都是基于共享映射区也就是咱们所说的共享内存。所以本文咱们着重来分析共享映射区,其余的内容就一笔带过了,若是你们实在感兴趣,能够自行查阅资料。bash

1. 管道

咱们一般所说的管道通常是指无名管道,是 IPC 中最古老的一种形式。1. 数据不能本身写,本身读;2. 管道中数据不可反复读,一旦读走,管道中再也不存在;3. 采用半双工通讯方式,数据只能单方向上流动;4. 只能在带有血缘关系的进程间通讯;5. 管道能够当作是一种特殊的文件,对于它的读写也可使用普通的 read、write 等函数,可是它不是普通的文件,并不属于其余任何文件系统,而且只存在于内存中。ionic

#include<stdio.h>
#include<unistd.h>
  
int main()
 {
     int fd[2];  // 两个文件描述符
     pid_t pid;
     char buff[20];
 
     if(pipe(fd) < 0)  // 建立管道
         printf("Create Pipe Error!\n");
 
     if((pid = fork()) < 0)  // 建立子进程
         printf("Fork Error!\n");
     else if(pid > 0)  // 父进程
     {
         close(fd[0]); // 关闭读端
         write(fd[1], "hello pipe\n", 11);
     }
     else
     {
         close(fd[1]); // 关闭写端
         read(fd[0], buff, 20);
         printf("%s", buff);
     }
     return 0;
 }
复制代码

2. 信号

信号 (signal) 机制是 Linux 系统中最为古老的进程间通讯机制,信号不能携带大量的数据信息,通常在知足特定场景时才会触发信号。信号啥时会产生?函数

  1. 按键产生,ctrl+c,ctrl+z
  2. 系统调用产生,kill,raise,abort
  3. 软件条件产生,alarm
  4. 硬件异常产生,非法访问内存,除0,内存对齐出错
  5. 命令产生,kill

信号出现时怎么处理?性能

  1. 忽略此信号,但有两种信号决不能被忽略,它们是: SIGKILL\SIGSTOP。 这是由于这两种信号向超级用户提供了一种终止或中止进程的方法。
  2. 执行系统默认动做,对大多数信号的系统默认动做是终止该进程。
  3. 执行用户但愿的动做,通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户但愿的处理。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char* argv[]){
        pid_t pid = fork();
        if(pid < 0){
                printf("fork error!\n");
        }else if(pid > 0){
                while(1){
                        printf("I am parent!\n");
                        sleep(1);
                }
        }else if(pid == 0){
                sleep(5);
                kill(getppid(), SIGKILL);
        }
        return 0;
}
复制代码

上面是一个很是简单的小例子,你们不妨看一下 Process.killProcess() 的源码。学习

3. 共享映射区

有关于共享内存的实现方式,你们能够参考一下这篇文章《JNI 基础 - Android 共享内存的序列化过程》 ,这里咱们主要来说讲 mmap 这个函数做用与实现原理,在 Android 的 binder 驱动中,在腾讯开源的 MMKV库中,在一些高性能的日志库中,凡是关于共享映射区的地方都会有它的存在。先来看下函数的原型:ui

void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
复制代码

参数 start:指向欲映射的内存起始地址,一般设为 NULL,表明让系统自动选定地址,映射成功后返回该地址。spa

参数 length:表明将文件中多大的部分映射到内存。

参数 prot:映射区域的保护方式。能够为如下几种方式的组合:

  • PROT_EXEC 页内容能够被执行
  • PROT_READ 页内容能够被读取
  • PROT_WRITE 页能够被写入
  • PROT_NONE 页不可访问

参数 flags:指定映射对象的类型,映射选项和映射页是否能够共享。能够为如下几种方式的组合:

  • AP_FIXED 使用指定的映射起始地址,若是由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。若是指定的起始地址不可用,操做将会失败。而且起始地址必须落在页的边界上。
  • MAP_SHARED 与其它全部映射这个对象的进程共享映射空间。对共享区的写入,至关于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
  • MAP_PRIVATE 创建一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
  • MAP_NORESERVE 不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会获得保证。当交换空间不被保留,同时内存不足,对映射区的修改会引发段违例信号。
  • MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联。
  • 等等不经常使用的

参数 fd:文件句柄 fd。若是 MAP_ANONYMOUS 被设定,为了兼容问题,其值应为-1。

参数 offset:被映射对象内容的偏移位置(起点)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>

struct person{
        char name[24];
        int age;
};

void sys_err(const char *str){
        perror(str);
        exit(0);
}

int main(int argc, char *argv[]){
        int fd;
        struct person stu = {"Darren", 25};
        struct person *p;
        fd = open("test_map", O_RDWR|O_CREAT|O_TRUNC, 0644);
        if(fd == -1)
                sys_err("open error");

        ftruncate(fd, sizeof(stu));

        p = (person*)mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        if(p == MAP_FAILED)
                sys_err("mmap error");
        while(1){
                memcpy(p, &stu, sizeof(stu));
                stu.age++;
                sleep(1);
        }

        munmap(p, sizeof(stu));
        close(fd);
        return 0;
}
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>

struct person{
        char name[24];
        int age;
};

void sys_err(const char *str){
        perror(str);
        exit(0);
}

int main(int argc, char *argv[]){
        int fd;
        struct person *p;
        fd = open("test_map", O_RDONLY);
        if(fd == -1)
                sys_err("open error");

        p = (person*)mmap(NULL, sizeof(person), PROT_READ, MAP_SHARED, fd, 0);
        if(p == MAP_FAILED)
                sys_err("mmap error");
        while(1){
                printf("name = %s, age = %d\n", p->name, p->age);
                sleep(2);
        }

        munmap(p, sizeof(person));
        close(fd);
        return 0;
}
复制代码

关于其实现的原理,最好的方式天然是看源码,但这里咱们主要来聊聊 Android binder 中 mmap 的做用及原理(一次内存拷贝),关于 mmap 的源码你们能够自行阅读(不难的),具体的位置在

android/platform/bionic/libc/bionic/mmap.cpp 
复制代码

Android 应用在进程启动之初会建立一个单例的 ProcessState 对象,其构造函数执行时会同时完成 binder 的 mmap,为进程分配一块内存,专门用于 Binder 通讯,以下。

ProcessState::ProcessState(const char *driver)
    : mDriverName(String8(driver))
    , mDriverFD(open_driver(driver))
    ...
 {
    if (mDriverFD >= 0) {
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        ...
    }
}
复制代码

第一个参数是分配地址,为0意味着让系统自动分配,先在用户空间找到一块合适的虚拟内存,以后,在内核空间也找到一块合适的虚拟内存,修改两个控件的页表,使得二者映射到同一块物理内存。

Linux 的内存分用户空间跟内核空间,同时页表也分两类,用户空间页表跟内核空间页表,每一个进程有一个用户空间页表,可是系统只有一个内核空间页表。而 Binder mmap 的关键是:也更新用户空间对应的页表的同时也同步映射内核页表,让两个页表都指向同一块地址,这样一来,数据只须要从 A 进程的用户空间,直接拷贝拷贝到 B 所对应的内核空间,而 B 多对应的内核空间在 B 进程的用户空间也有相应的映射,这样就无需从内核拷贝到用户空间了。

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    ...
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;
    ...
    // 在内核空间找合适的虚拟内存块
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
    proc->buffer = area->addr;
    // 记录用户空间虚拟地址跟内核空间虚拟地址的差值 
    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
    ...
    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
    // 分配page,并更新用户空间及内核空间对应的页表 
    ret = binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma);
    ...
    return ret;
}

static int binder_update_page_range(struct binder_proc *proc, int allocate,
            void *start, void *end,
            struct vm_area_struct *vma)
{
  ...
  // 一页页分配
  for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
    int ret;
    struct page **page_array_ptr;
    // 分配一页
    page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
    *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
    ...
    // 修改页表,让物理空间映射到内核空间 
    ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
    ..
    // 根据以前记录过差值,计算用户空间对应的虚拟地址 
    user_page_addr =
        (uintptr_t)page_addr + proc->user_buffer_offset;
    // 修改页表,让物理空间映射到用户空间 
    ret = vm_insert_page(vma, user_page_addr, page[0]);
  }
  ...
  return -ENOMEM;
}
复制代码

上面的代码能够看到,binder 一次拷贝的关键是,完成内存的时候,同时完成了内核空间跟用户空间的映射,也就是说,同一份物理内存,既能够在用户空间用虚拟地址访问,也能够在内核空间用虚拟地址访问。

视频连接:pan.baidu.com/s/1_4GFw8AK… 视频密码:eke1

相关文章
相关标签/搜索