Binder驅動之內存映射全解

Binder通過open調用打開後,需要用戶態進程需調用mmap進行內存映射。mmap系統調用,經過VFS最終會調用到binder驅動註冊的binder_mmap函數。這裏我們將揭開Binder通信高效的本質原因,:)

一 內存映射函數的實現 binder_mmap(kernel/drivers/android/binder.c)

static int binder_mmap(struct file *filp, struct vm_area_struct *vma/*用戶態虛擬地址空間描述,地址空間在0~3G*/)
{
    int ret;
    /* 一塊連續的由vmalloc分配內核虛擬地址空間描述,從 VMALLOC_START到VMALLOC_END*/
    struct vm_struct *area;
    struct binder_proc *proc = filp->private_data;
    const char *failure_string;
    struct binder_buffer *buffer;

    if (proc->tsk != current)
        return -EINVAL;

    //申請空間不能大於4M,如果大於4M就改爲4M大小。app默認是1M左右
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
          vma->vm_end = vma->vm_start + SZ_4M;

    //檢查vma是否被forbidden,vma是一塊連續的用戶態虛擬內存地址空間的描述
    if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
          ret = -EPERM;
          failure_string = "bad vm_flags";
        goto err_bad_arg;
    }

    //打開VM_DONTCOPY,關閉VM_MAYWRITE
     vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;

    //加上binder_mmap_lock互斥鎖,因爲接下來要操作proc結構體,可能發生多線程競爭
     mutex_lock(&binder_mmap_lock);

    //一個進程已經有一次mmap,如要執行新的map,需先將之前的unmap。
    if (proc->buffer) {
          ret = -EBUSY;
          failure_string = "already mapped";
          goto err_already_mapped;
    }

    /* 獲取一塊與用戶態空間大小一致的內核的連續虛擬地址空間,
     * 注意虛擬地址空間是在此一次性分配的,物理頁面卻是需要時纔去申請和映射
    */
    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
    if (area == NULL) {
          ret = -ENOMEM;
          failure_string = "get_vm_area";
        goto err_get_vm_area_failed;
    }

    //將內核虛擬地址記錄在proc的buffer中 
     proc->buffer = area->addr;

    /* 記錄用戶態虛擬地址空間與內核態虛擬地址空間的偏移量,
     * 這樣通過buffer和user_buffer_offset就可以計算出用戶態的虛擬地址。
    */
     proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;

    /*釋放互斥鎖*/
     mutex_unlock(&binder_mmap_lock);
#ifdef CONFIG_CPU_CACHE_VIPT
    /* CPU的緩存方式是否爲: VIPT(Virtual Index Physical Tag):使用虛擬地址的索引域和物理地址的標記域。
     * 這裏先不管,有興趣的可參考:https://blog.csdn.net/Q_AN1314/article/details/78980191
    */
    if (cache_is_vipt_aliasing()) {
        while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
               pr_info("binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
               vma->vm_start += PAGE_SIZE;
        }
    }
#endif

    /*分配存放物理頁地址的數組*/
    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
    if (proc->pages == NULL) {
          ret = -ENOMEM;
          failure_string = "alloc page array";
        goto err_alloc_pages_failed;
    }

    /*將虛擬地址空間的大小記錄在proc的buffer_size中*/
     proc->buffer_size = vma->vm_end - vma->vm_start;

    /* 安裝vma線性空間操作函數:open,close,fault
    * open-> binder_vma_open: 簡單的輸出日誌,pid,虛擬地址的起止、大小、標誌位(vm_flags和vm_page_prot)
    * close -> binder_vma_close: 將proc的vma,vma_vm_mm設爲NULL,並將proc加入到binder_deferred_workqueue隊列,
    *                            binder驅動有一個單獨的線程處理這個隊列。
    * fault -> binder_vam_fault: 直接返回VM_FAULT_SIGBUS, 
    */
     vma->vm_ops = &binder_vm_ops;

    /*在vma的vm_private_data字段裏存入proc的引指針*/
     vma->vm_private_data = proc;

    /* 先分配1個物理頁,並將其分別映射到內核線性地址和用戶態虛擬地址上,具體詳見2
    */
    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer **+ PAGE_SIZE**, vma)) {
          ret = -ENOMEM;
          failure_string = "alloc small buf";
        goto err_alloc_small_buf_failed;
    }

    /*成功分配了物理頁並建立好的映射關係後,內核起始虛地址做爲第一個binder_buffer的地址*/
     buffer = proc->buffer;
    /*接着將內核虛擬內存鏈入proc的buffers和free_buffers鏈表中,free標誌位設爲1
     INIT_LIST_HEAD(&proc->buffers);
     list_add(&buffer->entry, &proc->buffers);
     buffer->free = 1;
     binder_insert_free_buffer(proc, buffer);
     /*異步只能使用整個地址空間的一半*/
     proc->free_async_space = proc->buffer_size / 2;
     barrier();
     proc->files = get_files_struct(current);
     proc->vma = vma;
     proc->vma_vm_mm = vma->vm_mm;/*vma->vm_mm: vma對應的mm_struct,描述一個進程的虛擬地址空間,一個進程只有一個*/

    /*pr_info("binder_mmap: %d %lx-%lx maps %p\n",
           proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/

    return 0;

    /*出錯處理*/
err_alloc_small_buf_failed:

     kfree(proc->pages);
     proc->pages = NULL;
err_alloc_pages_failed:
     mutex_lock(&binder_mmap_lock);
     vfree(proc->buffer);
     proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
     mutex_unlock(&binder_mmap_lock);

err_bad_arg:
     pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",
            proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
    return ret;
}

基本過程在上面的代碼基本已經都註釋了。幾個需要注意的地方這裏再說明一下:

  • 調用get_vm_area獲取內核態虛擬地址,地址是在32體系架構地址空間在: 3G+896M + 8M ~ 4G之間。proc中沒有直接記錄用戶態的虛地址,而是存放一個用戶態地址與內核態地址偏移量:proc->user_buffer_offset

  • vma操作函數 —— vma->vm_ops, Binder實現了open, closefault三個接口。

    • open(binder_vma_open)的實現只是簡單的輸出一條進程id及vma地址及標誌位相關信息的debug日誌。
    • close(binder_vma_close)除了輸出一條類似open的日誌信息外,還會將proc->vmaproc->vma_vm_mm置空,接着調用binder_deferred_workbinder_deferred_workqueue隊列中放入一個BINDER_DEFERRED_PUT_FILES狀態的work,在之後binder線程執行到該work時會將proc->files置空, 接着調用put_files_struct釋放進程所屬的文件資源
    • fault(binder_vm_fault):簡單的返回VM_FAULT_SIGBUS, 這個鉤子是在訪問的虛地址沒有映射的物理頁時(缺頁)時,該函數被缺頁處理程序調用,該函數負責返回物理頁描述符,但因在binder中物理頁框與虛擬地址的映射,在調用binder_alloc_buf分配binder_buffer時就已經建立好了,所以一般來說是不會發生缺頁中斷的。
  • 需要調用binder_update_page_range分配一個頁框(一頁物理內存)的原因是:用於存放第一個binder_buffer,此時整個地址空間都在這個binder_buffer中管理, 但是隨着地址空間被不斷的分配和回收,會分裂成一系列的binder_buffer節點。

binder_mmap中,僅僅是進行虛擬地址空間的分配,物理內存的分配和釋放,Binder是通過binder_update_page_range來進行的。接下來我們就來看看binder_update_page_range的實現。

二 物理內存分配/與釋放 – binder_update_page_range

  • binder_update_page_range同時負責分配和釋放物理頁框,具體是分配還是釋放通過參數allocate控制,如果該參數爲0,則表示要解除內核態和用戶態對物理頁框的地址映射,釋放物理頁框;否則就是申請物理頁框,並建立內核態和用戶態的地址映射。

  • 參數vma的類型是struct vm_area_struct是用戶態虛擬地址空間描述。該參數爲NULL表示binder_update_page_range在內核調用路徑中,此時需嘗試獲取mm_struct並增加其引用計數,以防止進程的內存描述被釋放,然後再在操作完成後減少它的引用計數(mmput)。

以下即是具體的代碼實現,整個流程相對直觀,關鍵代碼都已經註釋,理解起來應該不難。

static int binder_update_page_range(struct binder_proc *proc, 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;

    binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
             "%d: %s pages %p-%p\n", proc->pid,
             allocate ? "allocate" : "free", start, end);

    if (end <= start)
        return 0;

    trace_binder_update_page_range(proc, allocate, start, end);
    if (vma)
        mm = NULL; /*binder_mmap的調用走這裏*/
    else
        /* 讀取進程的內存描述符(mm_struct), 
         * 並增加內存描述符(mm_struct)中的mm_users用戶計數,防止mm_struct被釋放*/
        mm = get_task_mm(proc->tsk); 

    if (mm) {
        /*獲取寫鎖*/
        down_write(&mm->mmap_sem);
        vma = proc->vma;
        if (vma && mm != proc->vma_vm_mm) {
            pr_err("%d: vma mm and task mm mismatch\n",
                proc->pid);
            vma = NULL;
        }
    }

    /*本次調用是釋放物理頁,直接進入釋放物理頁框流程*/

    if (allocate == 0) 
        goto free_range;

    if (vma == NULL) {
        pr_err("%d: binder_alloc_buf failed to map pages in userspace, no vma\n",
            proc->pid);
        goto err_no_vma;
    }

    /* 開始循環分配物理頁,並建立映射,每次循環分配1個頁框*/
    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
        int ret;
        /* 確定頁框所存放的數組的位置,按內核虛擬地址由小到大排列*/
        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
        BUG_ON(*page);
         /*分配頁框*/
        *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
        if (*page == NULL) {
            pr_err("%d: binder_alloc_buf failed for page at %p\n",
                proc->pid, page_addr);
            goto err_alloc_page_failed;
        }

        /*將內核虛擬地址與該頁框建立映射關係*/
        ret = map_kernel_range_noflush((unsigned long)page_addr,
                    PAGE_SIZE, PAGE_KERNEL, page);

        /* 將剛剛修改的內核頁表項刷新到CPU高速緩存*/
        flush_cache_vmap((unsigned long)page_addr,
                (unsigned long)page_addr + PAGE_SIZE);

        if (ret != 1) {
            pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n",
                   proc->pid, page_addr);
            goto err_map_kernel_failed;
        }

        /*計算用戶態虛地址*/
        user_page_addr =
            (uintptr_t)page_addr + proc->user_buffer_offset;

        /*將用戶虛擬地址與該頁框建立映射關係*/
        ret = vm_insert_page(vma, user_page_addr, page[0]);
        if (ret) {
            pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",
                   proc->pid, user_page_addr);
            goto err_vm_insert_page_failed;
        }
        /* vm_insert_page does not seem to increment the refcount */
    }

    if (mm) {
        /*釋放寫鎖*/
        up_write(&mm->mmap_sem);
        mmput(mm);/*減少內存描述符(mm_struct)中的mm_users用戶計數*/
    }

    /*分配物理頁框流程到這裏結束,接下去是釋放物理頁流程*/
    return 0;

/*釋放物理頁,解除地址映射*/
free_range:/*由後往前解除用戶態及內核態的物理頁框映射,並釋放物理頁框*/
    for (page_addr = end - PAGE_SIZE; page_addr >= start;
         page_addr -= PAGE_SIZE) {
        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
        if (vma)
            /*解除用戶態虛擬地址和物理頁框的映射*/
            zap_page_range(vma, (uintptr_t)page_addr +
                proc->user_buffer_offset, PAGE_SIZE, NULL);
err_vm_insert_page_failed:
        /*解除內核態虛擬地址和物理頁框的映射*/
        unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
err_map_kernel_failed:
        __free_page(*page);/*釋放物理頁框*/
        *page = NULL;
err_alloc_page_failed:
        ;
    }

err_no_vma:
    if (mm) {
        /*釋放寫鎖*/
        up_write(&mm->mmap_sem);
        mmput(mm);/*減少內存描述符中的mm_users用戶計數*/
    }
    return -ENOMEM;
}

三 總結

  • 整個地址映射流程大致可以歸納爲以下幾個步驟:
    • 參數檢查。這裏要注意的Binder驅動中,通信的內存映射大小上限是4MB,超過4MB,會截斷到4MB。在用戶態的ProccessStat.cpp實現中,調用mmap進行地址映射的大小是BINDER_VM_SIZE,它的值爲1M - 2 * PAGE_SIZE,PAGE_SIZE一般爲4KB,所以BINDER_VM_SIZE1016KB,且同一時間只能進行一次mmap。即普通APP一次Binder通信最大的數據量是1016KB,要突破這個限制,就需要先unmap在ProcessStat映射的內存,再自行調用mmap進行內存映射,但是這樣最大也只能進行擴大到4MB。
    • 在分配內核分配一塊相同大小的虛擬地址空間。
    • 分配物理內存,並將它同時映射到內核地址空間和用戶態地址空間,即用戶態地址空間和用戶態地址空間都是指向同一塊物理內存空間。這是Binder通信只需要進行一次數據拷貝的精髓所在,當一個進程將其要進行Binder傳輸的數據從用戶態傳輸態拷貝到內核態地址空間時,此時數據所在物理內存也是通信對方用戶態虛擬地址所映射的區域,無需再從內核態再到用戶態的數據拷貝。
    • 更新相關內存管理結構,如鏈表等。
  • 異步只能使用整個映射地址空間的一半,這裏應該是爲了防止異步調用佔用完所有的地址空間,導致同步調用無地址空間可用,畢竟同步調用是阻塞是調用,它的優先級應該高於異步調用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章