從 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

相關文章
相關標籤/搜索