Binder驅動介紹

看到一篇不錯的關於binder驅動介紹的文章,轉載一下,原文鏈接:

https://www.jianshu.com/p/b826cbf3cb8d

Binder驅動是Android專用的一個驅動程序,保持了和一般Linux驅動一樣框架。Binder驅動不涉及任何外設,本質上只操作內存,負責將數據從一個進程傳遞到另外一個進程。Binder驅動的代碼存放在如下幾個目錄:

kernel/drivers/android

kernel\include\uapi\linux\android

下面介紹Binder驅動中的幾個關鍵函數:

binder_init

static int __init binder_init(void)
{
    int ret;
    char *device_name, *device_names;
    struct binder_device *device;
    struct hlist_node *tmp;

    atomic_set(&binder_transaction_log.cur, ~0U);
    atomic_set(&binder_transaction_log_failed.cur, ~0U);
    //1. 創建*binder_deferred_workqueue*
    binder_deferred_workqueue = create_singlethread_workqueue("binder");
    if (!binder_deferred_workqueue)
        return -ENOMEM;
    //2. 在debugfs創建一些設備節點用於調試
    binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
    if (binder_debugfs_dir_entry_root)
        binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
                         binder_debugfs_dir_entry_root);

    if (binder_debugfs_dir_entry_root) {
        debugfs_create_file("state",
                    S_IRUGO,
                    binder_debugfs_dir_entry_root,
                    NULL,
                    &binder_state_fops);
        //...
    }
    
    //3. 創建設備節點
    device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
    if (!device_names) {
        ret = -ENOMEM;
        goto err_alloc_device_names_failed;
    }
    strcpy(device_names, binder_devices_param);

    while ((device_name = strsep(&device_names, ","))) {
        ret = init_binder_device(device_name);
        if (ret)
            goto err_init_binder_device_failed;
    }

    return ret;
}

Binder初始化代碼可以分爲三個部分:

  1. 創建binder_deferred_workqueue
  2. 在debugfs創建一些設備節點用於調試
  3. 創建設備節點

這裏重點分析第三部分代碼

static char *binder_devices_param = CONFIG_ANDROID_BINDER_DEVICES;

char *device_name, *device_names;
device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
if (!device_names) {
    ret = -ENOMEM;
    goto err_alloc_device_names_failed;
}
strcpy(device_names, binder_devices_param);

while ((device_name = strsep(&device_names, ","))) {
    ret = init_binder_device(device_name);
}

CONFIG_ANDROID_BINDER_DEVICES 在kernel配置如下:

CONFIG_ANDROID_BINDER_DEVICES="binder,hwbinder,vndbinder"

上述代碼即分別創建了名字爲binder,hwbinder,vndbinder的三個設備。我們繼續分析init_binder_device這個函數

static int __init init_binder_device(const char *name)
{
    int ret;
    struct binder_device *binder_device;

    binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);
    if (!binder_device)
        return -ENOMEM;

    binder_device->miscdev.fops = &binder_fops;
    binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
    binder_device->miscdev.name = name;

    binder_device->context.binder_context_mgr_uid = INVALID_UID;
    binder_device->context.name = name;
    mutex_init(&binder_device->context.context_mgr_node_lock);

    ret = misc_register(&binder_device->miscdev);
    if (ret < 0) {
        kfree(binder_device);
        return ret;
    }

    hlist_add_head(&binder_device->hlist, &binder_devices);

    return ret;
}

這個函數主要做了下面三件事:

  1. 初始化binder_device結構
  2. 使用 misc_register方法向系統註冊設備節點。該函數成功調用後即會在/device/binder (/device/vndbinder,/device/hwbinder)下生成設備節點
  3. binder_device* 加入到binder_devices鏈表中

binder_fops結構定義了操作binder設備的各個函數的指針:

static const struct file_operations binder_fops = {
    .owner = THIS_MODULE,
    .poll = binder_poll,
    .unlocked_ioctl = binder_ioctl,
    .compat_ioctl = binder_ioctl,
    .mmap = binder_mmap,
    .open = binder_open,
    .flush = binder_flush,
    .release = binder_release,
};

另外這類需要介紹Binder驅動中的兩個數據機構.

  1. binder_device
struct binder_device {
    struct hlist_node hlist;
    struct miscdevice miscdev;
    struct binder_context context;
};

binder_device代表了一個Binder設備,參考之前的代碼 hlist_node 指向Bidner設備在binder_devices鏈表的節點,miscdevice代表了內核中的一個misc設備,binder_context即當前Binder設備的上下文。

  1. binder_context
struct binder_context {
    struct binder_node *binder_context_mgr_node;
    struct mutex context_mgr_node_lock;

    kuid_t binder_context_mgr_uid;
    const char *name;
};

binder_context用於表示一個binder設備的上下文,這個binder設備的上下文即是我們熟悉的ServiceManager在內核這中的表示。其中
binder_context_mgr_node表示想用的ServiceManager在內核中的Binder節點。其他的數據結構後續在分析後續代碼時再做介紹。

binder_open

用戶空間要使用Binder設備進行IPC,首先需要使用open系統調用打開Binder設備,此時內核會通過指針調用註冊設備時傳入的open函數,binder驅動的open函數如下:

static int binder_open(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc;
    struct binder_device *binder_dev;

    proc = kzalloc(sizeof(*proc), GFP_KERNEL);
    
    //1. 初始化鎖 
    spin_lock_init(&proc->inner_lock);
    spin_lock_init(&proc->outer_lock);
    get_task_struct(current->group_leader);
    //2. 將tsk對象賦值爲當前進程的進程描述符
    proc->tsk = current->group_leader;
    //3. 初始化todo鏈表
    INIT_LIST_HEAD(&proc->todo);
    //4. 將進程優先級信息初始化爲線程的優先級
    if (binder_supported_policy(current->policy)) {
        proc->default_priority.sched_policy = current->policy;
        proc->default_priority.prio = current->normal_prio;
    } else {
        proc->default_priority.sched_policy = SCHED_NORMAL;
        proc->default_priority.prio = NICE_TO_PRIO(0);
    }

    binder_dev = container_of(filp->private_data, struct binder_device,
                  miscdev);
    //5. 將上下文指向打開設備的上下文
    proc->context = &binder_dev->context;
    //6. 初始化alloc
    binder_alloc_init(&proc->alloc);

    binder_stats_created(BINDER_STAT_PROC);
    //7. 將PID設置爲當前進程的PID
    proc->pid = current->group_leader->pid;
    //8. 初始化delivered_death和waiting_threads鏈表
    INIT_LIST_HEAD(&proc->delivered_death);
    INIT_LIST_HEAD(&proc->waiting_threads);
    //9. 將filp->private_data指向binder_proc
    filp->private_data = proc;

    mutex_lock(&binder_procs_lock);
    //10. 添加到binder_procs雙向鏈表
    hlist_add_head(&proc->proc_node, &binder_procs);
    mutex_unlock(&binder_procs_lock);
    //...

    return 0;
}

籠統的講,這個函數僅完成了一項工作:爲打開驅動的進程創建了一個 binder_proc 結構,並將該結構添加到 binder_procs 鏈表中。binder_proc代表了一個打開了Binder驅動的進程,在binder_open函數中對binder_proc結構的部分成員做了初始化工作:

  1. 初始化鎖
  2. 將tsk對象賦值爲當前進程的進程描述符
  3. 初始化todo鏈表
  4. 將進程優先級信息初始化爲線程的優先級
  5. 將上下文指向打開設備的上下文
  6. 初始化alloc
  7. 將PID設置爲當前進程的PID
  8. 初始化delivered_death和waiting_threads鏈表

其中第6步初始化了binder_alloc對象基本內容:

void binder_alloc_init(struct binder_alloc *alloc)
{
    alloc->tsk = current->group_leader;
    alloc->pid = current->group_leader->pid;
    mutex_init(&alloc->mutex);
    INIT_LIST_HEAD(&alloc->buffers);
}

binder_mmap

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。在用戶空間使用mmap系統調用,內核會通過設備指針調用相應驅動的mmap函數。而Binder驅動中的mmap實現,實際上是將用戶空間的一段虛擬地址和kernel空間的一段虛擬地址映射到同一塊物理地址上,以減少IPC過程中數據從用戶空間向內核空間的拷貝次數。具體代碼如下:

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    struct binder_proc *proc = filp->private_data;
    //...
    vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
    vma->vm_ops = &binder_vm_ops;
    vma->vm_private_data = proc;
    //...
    ret = binder_alloc_mmap_handler(&proc->alloc, vma);
    //...
    proc->files = get_files_struct(current);
    return 0;
}

上述代碼主要調用了 binder_alloc_mmap_handler,該函數傳入的參數爲proc->alloc以及vma,類型分別爲binder_alloc以及vm_area_struct。在具體分析這個函數之前,先回憶一下之前介紹的基礎知識

  • 在打開MMU之後,用戶空間和內核空間訪問內存使用的是虛擬地址,需要通過MMU轉換成物理地址後訪問
  • 通常Kernel將其虛擬地址空間的前896M(3G-3G+896M)映射到物理內存前896M(0-896M)
  • Kernel虛擬地址空間後128M不做固定映射,需要時將虛擬地址(以頁爲單位)映射到任意物理地址

另外有幾個內核中的結構體和方法需要介紹一下:

  1. vm_area_struct
struct vm_area_struct
{
     struct mm_struct *vm_mm;           

     unsigned  long vm_start;       /*虛擬內存區域起始地址*/    
     unsigned  long vm_end;         /*虛擬內存區域結束地址*/

     //....
} ;

vm_area_struct表示用戶空間的一段虛擬內存區域,其中vm_mm域指向了進程的mm_struct結構體,vm_startvm_end分別指向這段虛擬地址區域的起始地址和結束地址,其中vm_start即使mmap系統調用所返回的地址。

  1. vm_struct
struct vm_struct {  
    struct vm_struct    *next;       /*指向下一個vm區域*/  
    void                *addr;       /*指向第一個內存單元(線性地址)*/  
    unsigned long        size;       /*該塊內存區的大小*/  
    //... 
};

vm_structvm_area_struct類似,表示的是內核空間的一段連續的虛擬內存區域,用於將內核空間後120M地址映射到物理地址,其中addr表示這段虛擬內存區域的起始地址,size表示這段內虛擬存區域的大小。

  1. get_vm_area
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)

get_vm_area用於向kernel申請一段虛擬的內存區域,其中size爲要申請虛擬區域大小,返回值爲vm_struct指針。

  1. alloc_page
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)

alloc_page(gfp_mask) 實際調用alloc_pages函數,分配20個頁面,並返回一個struct page指針。struct page用於表示一個內存物理頁。

  1. map_kernel_range_noflush
int map_kernel_range_noflush(unsigned long addr, unsigned long size, pgprot_t prot, struct page **pages)

map_kernel_range_noflush用於將內核地址空間的一段地址和一些特定的內存物理頁面映射。其中addr參數爲內核虛地址的起始地址,size表示這段虛擬地址的大小,port爲物理頁面的保護標誌位,pages指向物理頁面的(數組的)指針。

  1. flush_cache_vmap

flush_cache_vmap和具體的架構相關,map_kernel_range_noflush並不會刷新緩存,flush_cache_vmap通常和其配合使用用於刷新緩存。

  1. vm_insert_page
int vm_insert_page(struct vm_area_struct *vma, unsigned long addr, struct page *page)

vm_insert_page用於將一個page指針指向的物理頁插入到用戶虛擬地址空間。其中vma用於描述這段虛擬地址控件,addr用於描述該物理頁需要插入的用戶空間的虛擬地址。

接下來具體分析binder_alloc_mmap_handler函數的實現:

int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma)
{
    int ret;
    struct vm_struct *area;
    const char *failure_string;
    struct binder_buffer *buffer;

    mutex_lock(&binder_alloc_mmap_lock);
    /*1*/
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
    
    /*2*/
    alloc->buffer = area->addr;
    /*3*/
    alloc->user_buffer_offset =
        vma->vm_start - (uintptr_t)alloc->buffer;
    mutex_unlock(&binder_alloc_mmap_lock);
    /*4*/
    alloc->pages = kzalloc(sizeof(alloc->pages[0]) *
                   ((vma->vm_end - vma->vm_start) / PAGE_SIZE),
                   GFP_KERNEL);
    alloc->buffer_size = vma->vm_end - vma->vm_start;
    /*5*/
    buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);

    /*6*/ 
    if (__binder_update_page_range(alloc, 1, alloc->buffer,alloc->buffer BINDER_MIN_ALLOC, vma)) {
    }
    /*7*/ 
    buffer->data = alloc->buffer;
    list_add(&buffer->entry, &alloc->buffers);
    buffer->free = 1;
    /*7*/ 
    binder_insert_free_buffer(alloc, buffer);
    alloc->free_async_space = alloc->buffer_size / 2;
    barrier();
    alloc->vma = vma;
    alloc->vma_vm_mm = vma->vm_mm;

    return 0;
}

這個函數中需要注意的地方,已經增加標記,解釋如下:

  1. 申請一段內核空間的虛擬地址
  2. 將這段虛擬地址的首地址複製給alloc->buffer,alloc結構是在binder_open中初始化的
  3. 計算alloc->user_buffer_offset,vma->vm_start是用戶空間的虛擬地址的首地址,alloc->buffer是Kernel空間的首地址,user_buffer_offset爲兩者的差。計算完成後,vma 和 area這兩段虛擬地址空間的任意地址,都可以通過user_buffer_offset進行換算。
  4. 分配page數組,並計算buffer_size
  5. 爲一個binder_buffer結構體分配內存
  6. 調用__binder_update_page_range函數,這個函數後續會繼續深入介紹
  7. 初始化這個binder_buffer,data指向申請的內核虛擬地址段的首地址,並添加大量alloc->buffers數組,free置爲1
  8. 調用binder_insert_free_buffer函數,將buffer結構插入到alloc->free_buffers這顆紅黑樹種,便於

繼續分析__binder_update_page_range函數,主要代碼如下:

static int __binder_update_page_range(struct binder_alloc *alloc, int allocate,
                    void *start, void *end,
                    struct vm_area_struct *vma)
{
    void *page_addr;
    unsigned long user_page_addr;
    struct page **page;
    struct mm_struct *mm;

    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        int ret;

        page = &alloc->pages[(page_addr - alloc->buffer) / PAGE_SIZE];
        /*1*/
        *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
        /*2*/
        ret = map_kernel_range_noflush((unsigned long)page_addr,
                    PAGE_SIZE, PAGE_KERNEL, page);
        flush_cache_vmap((unsigned long)page_addr,
                (unsigned long)page_addr + PAGE_SIZE);
        /*3*/
        user_page_addr =
            (uintptr_t)page_addr + alloc->user_buffer_offset;
        ret = vm_insert_page(vma, user_page_addr, page[0]);
    }

    return 0;
}

這段代碼主要邏輯位於for循環中,循環中需要注意的地方已通過註釋標註,解釋如下:

  1. 申請一個物理頁面,並將返回的struct page結構體指針,存入到alloc->pages數組中
  2. 映射page_addr指向的內核虛擬地址控件和申請的物理頁面
  3. 將申請的物理頁面插入到對應的用戶空間虛擬地址

for循環首先將page_addr初始化爲start,每次增加一個PAGE_SIZE,page_addr >=end結束。實際的效果就是將start - end這段內核虛地址空間以及對應的用戶虛地址控件,映射到同一組物理頁面上。這樣在用戶空間和內核空間都可以通過自己的虛地址訪問相同的物理內存。映射的結果入下圖所示:

Binder 內存映射



作者:ObadiObada
鏈接:https://www.jianshu.com/p/b826cbf3cb8d
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章