關於進程間通訊咱們是再熟悉不過了,有時面試也常常被問到你瞭解 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