你真的理解Binder“一次拷貝“嗎?

前言

談到到Binder相對(duì)于其他傳統(tǒng)進(jìn)程間通信方式的優(yōu)點(diǎn)的時(shí)候,我們總會(huì)說(shuō)Binder只需要做“一次拷貝”就行了,而其他傳統(tǒng)方式需要“兩次拷貝”。這確實(shí)是Binder的優(yōu)點(diǎn),但再進(jìn)一步思考就會(huì)碰到兩個(gè)問(wèn)題:

  1. 這所謂的“一次拷貝”到底是發(fā)生在什么地方?
  2. 拷貝的到底是什么東西?

而很多介紹Binder的文章會(huì)列出“一次拷貝”是其優(yōu)點(diǎn),但對(duì)上面的兩個(gè)問(wèn)題要么一筆帶過(guò),要么就是回答的并不完全正確,造成一些理解上的混亂。

本篇文章意在探索這兩個(gè)問(wèn)題的正確答案,所以需要讀者對(duì)Binder驅(qū)動(dòng)的工作過(guò)程和Binder驅(qū)動(dòng)源碼有一個(gè)大致的了解,或者至少能看懂我的上一篇文章《聊聊怎樣學(xué)習(xí)Binder》

那么接下來(lái)就讓我們帶著這兩個(gè)問(wèn)題去源碼的世界一探究竟。

源碼

在看源碼之前,我們需要先理一理一些關(guān)于Binder的預(yù)備知識(shí)。

  1. Binder的mmap發(fā)生在ProcessState的構(gòu)造函數(shù)中,也就是一個(gè)進(jìn)程就這么一塊內(nèi)存映射,大小大概是1M左右。
  2. 內(nèi)核空間讀寫(xiě)用戶(hù)空間的數(shù)據(jù)是通過(guò)以下兩個(gè)函數(shù)完成的:
  • copy_from_user() 將數(shù)據(jù)從用戶(hù)空間拷貝到內(nèi)核空間。
  • copy_to_user() 將數(shù)據(jù)從內(nèi)核空間拷貝到用戶(hù)空間。
  1. Binder驅(qū)動(dòng)源碼中有很多地方都會(huì)出現(xiàn)這兩個(gè)函數(shù)的調(diào)用。我們需要搞清楚每次調(diào)用都是在拷貝些什么東西,拷貝到哪里去了。
  2. 為了抓住本文的“一次拷貝”這個(gè)點(diǎn),下面源碼引用會(huì)盡量集中在和內(nèi)存操作相關(guān)的代碼而暫時(shí)略過(guò)其他代碼。

下面我們就先從內(nèi)存映射說(shuō)起

Binder內(nèi)存映射

ProcessState構(gòu)造函數(shù)

ProcessState::ProcessState(const char *driver)
{
    if (mDriverFD >= 0) {
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
    }
}

注意mmap上方的注釋?zhuān)呀?jīng)說(shuō)的很清楚了,這塊內(nèi)存映射只是作為接收transactions來(lái)使用的,也就是說(shuō)往驅(qū)動(dòng)寫(xiě)數(shù)據(jù)的時(shí)候是與內(nèi)存映射無(wú)關(guān)的。記住這一點(diǎn)。
下面我們看一下內(nèi)核空間相應(yīng)的調(diào)用:

binder_mmap

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    ...
    ret = binder_alloc_mmap_handler(&proc->alloc, vma);
    ...
}

真正的映射操作由函數(shù)binder_alloc_mmap_handler()完成。這里我們只需要記住這個(gè)函數(shù)的第一個(gè)入?yún)?code>&proc->alloc。通過(guò)這個(gè)結(jié)構(gòu)體我們就能找到映射好的內(nèi)存塊。

內(nèi)存映射完成之后,就讓我們看一下Binder的傳輸過(guò)程中哪里使用到了這塊特殊的內(nèi)存。

Binder傳輸過(guò)程

傳輸過(guò)程我們只關(guān)注內(nèi)存和數(shù)據(jù)。

發(fā)起方用戶(hù)空間

發(fā)起方用戶(hù)空間做的事情其實(shí)就像發(fā)快遞一樣不停的打包,注意一下都包了些啥。

IPCThreadState::writeTransactionData
// 我們要傳輸?shù)臄?shù)據(jù)在data這個(gè)入?yún)⒅?status_t IPCThreadState::writeTransactionData(... const Parcel& data...)
{
    binder_transaction_data tr;
  
   ...
        //tr.data.ptr.buffer保存了指向data的指針
        tr.data_size = data.ipcDataSize();
        tr.data.ptr.buffer = data.ipcData();
        tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
        tr.data.ptr.offsets = data.ipcObjects();
    ...
    // 將tr寫(xiě)入mOut。
    mOut.writeInt32(cmd);
    mOut.write(&tr, sizeof(tr));

    return NO_ERROR;
}

這里tr只保存了指向數(shù)據(jù)的指針。然后tr被寫(xiě)入mOut這個(gè)Parcel

IPCThreadState::talkWithDriver
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
   ...

    binder_write_read bwr;
    ...
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data();
    ...
    bwr.write_consumed = 0;
    ...
    ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0
    ...    
 }

這里又包了一層,bwr。其中bwr.write_buffer保存了指向mOut.data()的指針。在這里也就是指向了tr

所以在發(fā)起方:

  • bwr含有指向tr的指針。
  • tr含有指向data的指針。

記住上面兩點(diǎn),接下來(lái)我們看一下內(nèi)核空間是怎么取包的:

發(fā)起方內(nèi)核空間

binder_ioctl_write_read
static int binder_ioctl_write_read(struct file *filp,
                unsigned int cmd, unsigned long arg,
                struct binder_thread **threadp)
{    ...
    void __user *ubuf = (void __user *)arg;
    struct binder_write_read bwr;
    ...
    if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
        ret = -EFAULT;
        goto out;
    }
    ...
    ret = binder_thread_write(proc, *threadp,
                      bwr.write_buffer,
                      bwr.write_size,
                      &bwr.write_consumed);
}

這里我們遇到了第一個(gè)copy_from_user()調(diào)用。這個(gè)調(diào)用會(huì)把用戶(hù)空間的bwr給拷貝到內(nèi)核空間。但是要注意,copy_from_user()的第一個(gè)入?yún)⑹强截惖哪繕?biāo)地址,這里給的是&bwr,函數(shù)內(nèi)部的一個(gè)結(jié)構(gòu)體。顯然此處和內(nèi)存映射沒(méi)有關(guān)系。接下來(lái)就進(jìn)入binder_thread_write。入?yún)⒂?code>bwr.write_buffer,回頭看用戶(hù)空間最底層那里,指向的是不是tr?

binder_thread_write
static int binder_thread_write(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed)
{
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
    void __user *ptr = buffer + *consumed;
    ...
    case BC_TRANSACTION:
        case BC_REPLY: {
            struct binder_transaction_data tr;

            if (copy_from_user(&tr, ptr, sizeof(tr)))
                return -EFAULT;
            ptr += sizeof(tr);
            binder_transaction(proc, thread, &tr,
                       cmd == BC_REPLY, 0);
            break;
        }
    ....
}

這里我們遇到了第二個(gè)copy_from_user()。這回會(huì)把用戶(hù)空間的那個(gè)tr,也就是IPCThreadState.mOut,給拷貝到內(nèi)核中來(lái),看它的第一個(gè)入?yún)ⅲ€是和內(nèi)存映射沒(méi)有關(guān)系。接下來(lái)就進(jìn)入關(guān)鍵的binder_transaction()了。

binder_transaction

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply,
                   binder_size_t extra_buffers_size)
{
   ... 
   struct binder_transaction *t;
   ...
   t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
        tr->offsets_size, extra_buffers_size,
        !reply && (t->flags & TF_ONE_WAY));
        ...
   copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
               tr->data.ptr.buffer, tr->data_size)
         ...
   off_start = (binder_size_t *)(t->buffer->data +
                      ALIGN(tr->data_size, sizeof(void *)));
   offp = off_start;
   ...
   copy_from_user(offp, (const void __user *)(uintptr_t)
               tr->data.ptr.offsets, tr->offsets_size);

}

首先看一下t->buffer,函數(shù)binder_alloc_new_buf()的返回值會(huì)賦值給它,這里是我們第一次見(jiàn)到這個(gè)函數(shù),從名字看是分配內(nèi)存,看它的第一個(gè)入?yún)?code>&target_proc->alloc。現(xiàn)在回想前面說(shuō)mmap的時(shí)候提到內(nèi)存映射的信息會(huì)保存到proc->alloc這個(gè)結(jié)構(gòu)體中。所以這里我們就可以確定現(xiàn)在是在接收方進(jìn)程的內(nèi)存映射中分配了一塊內(nèi)存出來(lái)。t->buffer就指向這塊有映射的內(nèi)存。

接下來(lái)就是我們遇到的第三次copy_from_user()調(diào)用了。回想在用戶(hù)空間的時(shí)候tr.data.ptr.buffer是指向我們要傳輸?shù)臄?shù)據(jù)的。所以這里可以看到這個(gè)copy_from_user()的操作就是把發(fā)起方用戶(hù)空間的數(shù)據(jù)直接拷貝到了接收方內(nèi)核的內(nèi)存映射中。 這就是所謂“一次拷貝”的關(guān)鍵點(diǎn)。

緊接著還有一個(gè)copy_from_user()調(diào)用,這里拷貝的是和數(shù)據(jù)相關(guān)的一些跨境程對(duì)象的偏移量,和前面拷貝bwrtr在體量上來(lái)講與數(shù)據(jù)的體量相比不是主要矛盾,所以說(shuō)“一次拷貝”指的就是上面對(duì)數(shù)據(jù)的拷貝。

至此關(guān)于“一次拷貝”這個(gè)問(wèn)題我們應(yīng)該是已經(jīng)有了初步的答案了,但為了讓整個(gè)過(guò)程形成個(gè)閉環(huán),接下來(lái)我們?cè)賮?lái)看一下Binder傳輸過(guò)程的后半段。

接收方內(nèi)核空間

binder_thread_read
   static int binder_thread_read(struct binder_proc *proc,
                  struct binder_thread **threadp,
                  binder_uintptr_t binder_buffer, size_t size,
                  binder_size_t *consumed, int non_block)
{ 
    ....
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
    void __user *ptr = buffer + *consumed;
    ....
    tr.data_size = t->buffer->data_size;
    tr.offsets_size = t->buffer->offsets_size;
    tr.data.ptr.buffer = (binder_uintptr_t)
            ((uintptr_t)t->buffer->data +
            binder_alloc_get_user_buffer_offset(&proc->alloc));
    tr.data.ptr.offsets = tr.data.ptr.buffer +
                    ALIGN(t->buffer->data_size,
                        sizeof(void *));
    ...
    copy_to_user(ptr, &tr, sizeof(tr));

}

這里我們遇到了第一個(gè)copy_to_user()調(diào)用,這是把tr給拷貝到接收方的用戶(hù)空間的IPCThreadState.mIn。在此之前把內(nèi)核映射的數(shù)據(jù)地址指針轉(zhuǎn)換為用戶(hù)空間的指針賦值給tr.data.ptr.buffer

binder_ioctl_write_read
static int binder_ioctl_write_read(struct file *filp,
                unsigned int cmd, unsigned long arg,
                struct binder_thread **threadp)
{
    ...
    copy_to_user(ubuf, &bwr, sizeof(bwr));
    ...
}

最后我們遇到了第二個(gè)copy_to_user()。把bwr又拷貝回用戶(hù)空間了,注意此時(shí)bwr內(nèi)包含指向tr的指針。也就是bwr.read_buffer是指向這個(gè)tr,或者說(shuō)IPCThreadState.mIn

接收方用戶(hù)空間

接下來(lái)就回到接收方的用戶(hù)空間了:

IPCThreadState::executeCommand
status_t IPCThreadState::executeCommand(int32_t cmd)
{
...
case BR_TRANSACTION:
        {
            binder_transaction_data tr;
            result = mIn.read(&tr, sizeof(tr));
            ...
            Parcel buffer;
            buffer.ipcSetDataReference(
                reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
                tr.data_size,
                reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
                tr.offsets_size/sizeof(binder_size_t), freeBuffer, this);

            ...        
          
          error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer, &reply, tr.flags);
       }                    
...
}

這里首先把trmIn里面讀出來(lái)。然后就直接就把內(nèi)存映射過(guò)來(lái)的指針tr.data.ptr.buffer,也就是那“一次拷貝”過(guò)來(lái)的地址,設(shè)置給buffer這個(gè)Parcel。這樣下面的實(shí)體Binder就可以調(diào)用transact來(lái)處理發(fā)起方傳過(guò)來(lái)的數(shù)據(jù)了。到這里應(yīng)該明白最前面做mmap的那個(gè)注釋了吧,內(nèi)存映射確實(shí)只是用來(lái)接收Binder傳輸過(guò)來(lái)的數(shù)據(jù)的。

總結(jié)

對(duì)Binder“一次拷貝”的兩個(gè)問(wèn)題(什么時(shí)候拷貝和拷貝的是什么東西),相信大家已經(jīng)有了一個(gè)初步的了解。這里我用一張圖來(lái)總結(jié)一下上面介紹的內(nèi)容:


總結(jié)

圖中表示了文中所講的關(guān)鍵的copy_from_usercopy_to_user。斜著的那個(gè)綠色箭頭就是“一次拷貝”所在之處。右側(cè)接收方的兩個(gè)綠色塊代表內(nèi)存映射。

關(guān)于對(duì)“一次拷貝”的理解以及內(nèi)存映射在Binder通信中的作用如果不仔細(xì)去研究的話(huà)很容易被Binder驅(qū)動(dòng)源碼里那么多的copy_from_usercopy_to_user調(diào)用給搞混了。但是研究透了以后這個(gè)機(jī)制其實(shí)并不復(fù)雜。希望這篇文章能幫到大家。

我的博客即將同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=ioy2yvgosgha

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。