关于进程间通讯咱们是再熟悉不过了,有时面试也常常被问到你了解 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
咱们一般所说的管道通常是指无名管道,是 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;
}
复制代码
信号 (signal) 机制是 Linux 系统中最为古老的进程间通讯机制,信号不能携带大量的数据信息,通常在知足特定场景时才会触发信号。信号啥时会产生?函数
信号出现时怎么处理?性能
#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() 的源码。学习
有关于共享内存的实现方式,你们能够参考一下这篇文章《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:映射区域的保护方式。能够为如下几种方式的组合:
参数 flags:指定映射对象的类型,映射选项和映射页是否能够共享。能够为如下几种方式的组合:
参数 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