紅茶一杯話Binder(傳輸機制篇_中)

紅茶一杯話Bindercss

(傳輸機制篇_中)html

 

侯 亮node

 

1 談談底層IPC機制吧

        在上一篇文章的最後,咱們說到BpBinder將數據發到了Binder驅動。然而在驅動層,這部分數據又是如何傳遞到BBinder一側的呢?這裏面到底藏着什麼貓膩?另外,上一篇文章雖然闡述了4棵紅黑樹,可是並未說明紅黑樹的節點究竟是怎麼產生的。如今,咱們試着回答這些問題。數組

1.1 概述

        在Binder驅動層,和ioctl()相對的動做是binder_ioctl()函數。在這個函數裏,會先調用相似copy_from_user()這樣的函數,來讀取用戶態的數據。而後,再調用binder_thread_write()和binder_thread_read()進行進一步的處理。咱們先畫一張調用關係圖:cookie

image001

         binder_ioctl()調用binder_thread_write()的代碼是這樣的:函數

if (bwr.write_size > 0) 
{
    ret = binder_thread_write(proc, thread, (void __user *)bwr.write_buffer,
                              bwr.write_size, &bwr.write_consumed);
    if (ret < 0) 
    {
        bwr.read_consumed = 0;
        if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
            ret = -EFAULT;
        goto err;
    }
}

注意binder_thread_write()的前兩個參數,一個是binder_proc指針,另外一個是binder_thread指針,表示發起傳輸動做的進程和線程。binder_proc沒必要多說了,那個binder_thread是怎麼回事?你們應該還記得前文提到的binder_proc裏的4棵樹吧,此處的binder_thread就是從threads樹中查到的節點。oop

thread = binder_get_thread(proc);

binder_get_thread()的代碼以下:ui

static struct binder_thread *binder_get_thread(struct binder_proc *proc)
{
    struct binder_thread *thread = NULL;
    struct rb_node *parent = NULL;
    struct rb_node **p = &proc->threads.rb_node;
 
    // 儘可能從threads樹中查找和current線程匹配的binder_thread節點
    while (*p)
    {
        parent = *p;
        thread = rb_entry(parent, struct binder_thread, rb_node);
        if (current->pid < thread->pid)
            p = &(*p)->rb_left;
        else if (current->pid > thread->pid)
            p = &(*p)->rb_right;
        else
            break;
    }
   
    // 「找不到就建立」一個binder_thread節點
    if (*p == NULL)
    {
        thread = kzalloc(sizeof(*thread), GFP_KERNEL);
        if (thread == NULL)
            return NULL;
        binder_stats_created(BINDER_STAT_THREAD);
        thread->proc = proc;
        thread->pid = current->pid;
        init_waitqueue_head(&thread->wait);
        INIT_LIST_HEAD(&thread->todo);
       
         // 新binder_thread節點插入紅黑樹
        rb_link_node(&thread->rb_node, parent, p);
        rb_insert_color(&thread->rb_node, &proc->threads);
        thread->looper |= BINDER_LOOPER_STATE_NEED_RETURN;
        thread->return_error = BR_OK;
        thread->return_error2 = BR_OK;
    }
    return thread;
}

binder_get_thread()會盡可能從threads樹中查找和current線程匹配的binder_thread節點,若是找不到,就會建立一個新的節點並插入樹中。這種「找不到就建立」的作法,在後文還會看到,咱們暫時先很少說。this

        在調用binder_thread_write()以後,binder_ioctl()接着調用到binder_thread_read(),此時每每須要等待遠端的回覆,因此binder_thread_read()會讓線程睡眠,把控制權讓出來。在將來的某個時刻,遠端處理完此處發去的語義,就會着手發回回覆。當回覆到達後,線程會從之前binder_thread_read()睡眠的地方醒來,並進一步解析收到的回覆。spa

        以上所說,都只是概要性的闡述,下面咱們要深刻一些細節了。

 

1.2 要進行跨進程調用,須要考慮什麼?

        咱們能夠先考慮一下,要設計跨進程調用機制,大概須要考慮什麼東西呢?咱們列一下:

1) 發起端:確定包括髮起端所從屬的進程,以及實際執行傳輸動做的線程。固然,發起端的BpBinder更是重中之重。

2) 接收端:包括與發起端對應的BBinder,以及目標進程、線程。

3) 待傳輸的數據:其實就是前文IPCThreadState::writeTransactionData()代碼中的binder_transaction_data了,須要注意的是,這份數據中除了包含簡單數據,還可能包含其餘binder對象噢,這些對象或許對應binder代理對象,或許對應binder實體對象,視具體狀況而定。

4) 若是咱們的IPC動做須要接收應答(reply),該如何保證應答能準確無誤地傳回來?

5) 如何讓系統中的多個傳輸動做有條不紊地進行。

        咱們能夠先畫一張示意圖:

image002

然而這張圖彷佛仍是串接不起整個傳輸過程,圖中的「傳輸的數據」究竟是怎麼發到目標端的呢?要回答這個問題,咱們還得繼續研究Binder IPC機制的實現機理。

 

1.3 傳輸機制的大致運做

         Binder IPC機制的大致思路是這樣的,它將每次「傳輸並執行特定語義的」工做理解爲一個小事務,既然所傳輸的數據是binder_transaction_data類型的,那麼這種事務的類名能夠相應地定爲binder_transaction。系統中固然會有不少事務啦,那麼發向同一個進程或線程的若干事務就必須串行化起來,所以binder驅動爲進程節點(binder_proc)和線程節點(binder_thread)都設計了個todo隊列。todo隊列的職責就是「串行化地組織待處理的事務」。

        下圖繪製了一個進程節點,以及一個從屬於該進程的線程節點,它們各帶了兩個待處理的事務(binder_transaction):

image003

        這樣看來,傳輸動做的基本目標就很明確了,就是想辦法把發起端的一個binder_transaction節點,插入到目標端進程或其合適子線程的todo隊列去。

        但是,該怎麼找目標進程和目標線程呢?基本作法是先從發起端的BpBinder開始,找到與其對應的binder_node節點,這個在前文闡述binder_proc的4棵紅黑樹時已經說過了,這裏再也不贅述。總之拿到目標binder_node以後,咱們就能夠經過其proc域,拿到目標進程對應的binder_proc了。若是偷懶的話,咱們直接把binder_transaction節點插到這個binder_proc的todo鏈表去,就算完成傳輸動做了。固然,binder驅動作了一些更精細的調整。

        binder驅動但願能把binder_transaction節點儘可能放到目標進程裏的某個線程去,這樣能夠充分利用這個進程中的binder工做線程。好比一個binder線程目前正睡着,它在等待其餘某個線程作完某個事情後纔會醒來,而那個工做又恰恰須要在當前這個binder_transaction事務處理結束後才能完成,那麼咱們就可讓那個睡着的線程先去作當前的binder_transaction事務,這就達到充分利用線程的目的了。反正無論怎麼說,若是binder驅動能夠找到一個合適的線程,它就會把binder_transaction節點插到它的todo隊列去。而若是找不到合適的線程,還能夠把節點插入目標binder_proc的todo隊列。

 

1.4 紅黑樹節點的產生過程

        另外一個要考慮的東西就是binder_proc裏的那4棵樹啦。前文在闡述binder_get_thread()時,已經看到過向threads樹中添加節點的動做。那麼其餘3棵樹的節點該如何添加呢?其實,祕密都在傳輸動做中。要知道,binder驅動在傳輸數據的時候,可不是僅僅簡單地遞送數據噢,它會分析被傳輸的數據,找出其中記錄的binder對象,並生成相應的樹節點。若是傳輸的是個binder實體對象,它不只會在發起端對應的nodes樹中添加一個binder_node節點,還會在目標端對應的refs_by_desc樹、refs_by_node樹中添加一個binder_ref節點,並且讓binder_ref節點的node域指向binder_node節點。咱們把前一篇文章的示意圖加以修改,獲得下圖:

image004

圖中用紅色線條來表示傳輸binder實體時在驅動層會添加的紅黑樹節點以及節點之間的關係。

        但是,驅動層又是怎麼知道所傳的數據中有多少binder對象,以及這些對象的確切位置呢?答案很簡單,是你告訴它的。你們還記得在向binder驅動傳遞數據以前,都是要把數據打成parcel包的吧。好比:

virtual status_t addService(const String16& name, const sp<IBinder>& service)
{
    Parcel data, reply;
       
    data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
    data.writeString16(name);
    data.writeStrongBinder(service);  // 把一個binder實體「打扁」並寫入parcel
    status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
    return err == NO_ERROR ? reply.readExceptionCode() : err;
}

請你們注意上面data.writeStrongBinder()一句,它專門負責把一個binder實體「打扁」並寫入parcel。其代碼以下:

status_t Parcel::writeStrongBinder(const sp<IBinder>& val)
{
    return flatten_binder(ProcessState::self(), val, this);
}

 

status_t flatten_binder(const sp<ProcessState>& proc, const sp<IBinder>& binder, Parcel* out)
{
    flat_binder_object obj;
    . . . . . .
    if (binder != NULL) {
        IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            . . . . . .
            obj.type = BINDER_TYPE_HANDLE;
            obj.handle = handle;
            obj.cookie = NULL;
        } else {
            obj.type = BINDER_TYPE_BINDER;
            obj.binder = local->getWeakRefs();
            obj.cookie = local;
        }
    }
    . . . . . .
    return finish_flatten_binder(binder, obj, out);
}

看到了嗎?「打扁」的意思就是把binder對象整理成flat_binder_object變量,若是打扁的是binder實體,那麼flat_binder_object用cookie域記錄binder實體的指針,即BBinder指針,而若是打扁的是binder代理,那麼flat_binder_object用handle域記錄的binder代理的句柄值。

        而後flatten_binder()調用了一個關鍵的finish_flatten_binder()函數。這個函數內部會記錄下剛剛被扁平化的flat_binder_object在parcel中的位置。說得更詳細點兒就是,parcel對象內部會有一個buffer,記錄着parcel中全部扁平化的數據,有些扁平數據是普通數據,而另外一些扁平數據則記錄着binder對象。因此parcel中會構造另外一個mObjects數組,專門記錄那些binder扁平數據所在的位置,示意圖以下:

image005

        一旦到了向驅動層傳遞數據的時候,IPCThreadState::writeTransactionData()會先把Parcel數據整理成一個binder_transaction_data數據,這個在上一篇文章已有闡述,可是當時咱們並無太關內心面的關鍵句子,如今咱們把關鍵句子再列一下:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
                                              int32_t handle, uint32_t code,
                                              const Parcel& data, status_t* statusBuffer)
{
    binder_transaction_data tr;
    . . . . . .
        // 這部分是待傳遞數據
        tr.data_size = data.ipcDataSize();
        tr.data.ptr.buffer = data.ipcData();
        // 這部分是扁平化的binder對象在數據中的具體位置
        tr.offsets_size = data.ipcObjectsCount()*sizeof(size_t);
        tr.data.ptr.offsets = data.ipcObjects();
    . . . . . .
    mOut.write(&tr, sizeof(tr));
    . . . . . .
}

其中給tr.data.ptr.offsets賦值的那句,所作的就是記錄下「待傳數據」中全部binder對象的具體位置,示意圖以下:

image006

所以,當binder_transaction_data傳遞到binder驅動層後,驅動層能夠準確地分析出數據中到底有多少binder對象,並分別進行處理,從而產生出合適的紅黑樹節點。此時,若是產生的紅黑樹節點是binder_node的話,binder_node的cookie域會被賦值成flat_binder_object所攜帶的cookie值,也就是用戶態的BBinder地址值啦。這個新生成的binder_node節點被插入紅黑樹後,會一直嚴陣以待,之後當它成爲另外某次傳輸動做的目標節點時,它的cookie域就派上用場了,此時cookie值會被反映到用戶態,因而用戶態就拿到了BBinder對象。

        咱們再具體看一下IPCThreadState::waitForResponse()函數,當它展轉從睡眠態跳出來時,會進一步解析剛收到的命令,此時會調用executeCommand(cmd)一句。

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    int32_t cmd;
    int32_t err;
 
    while (1)
    {
        if ((err = talkWithDriver()) < NO_ERROR) break;
        . . . . . .
        switch (cmd)
        {
            . . . . . .
            . . . . . .
        default:
            err = executeCommand(cmd);
            . . . . . .
            break;
        }
    }
    . . . . . .
   
    return err;
}

executeCommand()的代碼截選以下:

status_t IPCThreadState::executeCommand(int32_t cmd)
{
    BBinder* obj;
    . . . . . .
    switch (cmd)
    {
        . . . . . .
        . . . . . .
    case BR_TRANSACTION:
        {
            binder_transaction_data tr;
            result = mIn.read(&tr, sizeof(tr));
            . . . . . .
            . . . . . .
            if (tr.target.ptr)
            {
                sp<BBinder> b((BBinder*)tr.cookie);
                const status_t error = b->transact(tr.code, buffer, &reply, tr.flags);
                if (error < NO_ERROR) reply.setError(error);
 
            }
            . . . . . .
           
            if ((tr.flags & TF_ONE_WAY) == 0)
            {
                LOG_ONEWAY("Sending reply to %d!", mCallingPid);
                sendReply(reply, 0);
            }
            else
            {
                LOG_ONEWAY("NOT sending reply to %d!", mCallingPid);
            }
            . . . . . .
        }
        break;
    . . . . . .
    . . . . . .
    default:
        printf("*** BAD COMMAND %d received from Binder driver\n", cmd);
        result = UNKNOWN_ERROR;
        break;
    }
    . . . . . .
    return result;
}

請注意上面代碼中的sp<BBinder> b((BBinder*)tr.cookie)一句,看到了吧,驅動層的binder_node節點的cookie值終於發揮它的做用了,咱們拿到了一個合法的sp<BBinder>。

        接下來,程序走到b->transact()一句。transact()函數的代碼截選以下:

status_t BBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    . . . . . .
    switch (code)
    {
        . . . . . .
        default:
            err = onTransact(code, data, reply, flags);
            break;
    }
    . . . . . .
}

其中最關鍵的一句是調用onTransaction()。由於咱們的binder實體在本質上都是繼承於BBinder的,並且咱們通常都會重載onTransact()函數,因此上面這句onTransact()實際上調用的是具體binder實體的onTransact()成員函數。

        Ok,說了這麼多,咱們大概明白了binder驅動層的紅黑樹節點是怎麼產生的,以及binder_node節點的cookie值是怎麼派上用場的。限於篇幅,咱們先在這裏打住。下一篇文章咱們再來闡述binder事務的傳遞和處理方面的細節。

 

如需轉載本文內容,請註明出處。

http://my.oschina.net/youranhongcha/blog/152963

謝謝。

相關文章
相關標籤/搜索