聊聊怎樣學習Binder

前言

Binder可以說是整個Android框架最重要的一個基礎。如果不能吃透Binder,就談不上對Android有多么深刻的理解。這個道理相信大部分Android開發者都清楚。但是Binder整個框架又看起來好像深不可測,上至Java,下至kernel。又橫跨眾多進程和服務。想看看源代碼來學習。看著看著就看不明白了,不由得發出“我是誰?我在哪里?”這樣的疑問。這篇文章就想從作者個人的經驗來聊聊如何學習Binder。

Binder的學習

Binder的學習和學習其他源代碼一樣并沒有什么特殊的訣竅。那就是硬著頭皮看源碼,看文章。反復的看,不停的想,至少把妨礙自己理解的堵塞點都搞懂了,整個鏈路從上到下,從下到上都無障礙的跑通了那才算真正掌握了Binder。當然這也是學習所有其他框架源碼的要求。關于學習源碼的更多方法,可以參考我的另一篇文章我是如何學習Flutter源碼的。

源碼浩如煙海,看著看著就不知道跑哪里去了,沒有關系??梢詤⒖记叭说慕涷瀬頌樽约簬?。然而互聯網上說Binder的文章可謂多如牛毛。如何選擇呢?這里我推薦一位大牛的Binder系列文章。一遍看不懂那就多看幾遍,而且要結合Android源碼去看。文章里的每一個代碼片段最好都自己去源碼里對照著研究。這個一是為了加深記憶,二是為了促進消化。

如果全搞明白了,那么恭喜你,這篇文章剩下的內容不用再看了。如果是那種剛看完覺得自己都會了,過一段時間別人問你你又說不出來個子丑寅卯。這種情況是因為你只是死記硬背記住了關于Binder的一些東西而沒有完全理解Binder,有一些關于Binder通信的關鍵點沒有搞清楚,知其然而不知其所以然。處于“半懂不懂”的狀態。如果此時放棄學習Binder那就太可惜了。

寫這篇文章呢,就是希望能用盡量少的搬運源碼的方式來捋一下Binder。希望能幫你疏通堵點,徹底理解Binder。

正如我在前言里說的,Binder深不可測,上至Java,下至kernel。而程序員似乎會有個習慣,不太喜歡去挖自己領域之外的東西,喜歡Java的就不想去看C/C++的東西,搞APP的,搞Framework的不想去瞅一眼kernel里面長啥樣?!拔抑灰肋@東西API是啥,會用就行了。。?!?。某些情況下這可能是沒什么問題的。但不適用于Binder。

要徹底理解Binder,必須要理解Binder驅動(Driver)。

很多對Binder一知半解的同學通常都是卡在這個地方了,不是說硬記住那幾個“BC_TRANSACTION”,“BR_REPLY”命令,然后交互流程是啥樣就可以了。但如果此時你把這些東西也忘了,這篇文章也就不用往下看了,這邊建議你回頭把上面說的Binder系列文章擼幾遍再來。

如果你現在處于“半懂不懂”的狀態,那么接著看下面的章節能不能幫到你來理解Binder。

Binder的理解

本章節假設讀者對Binder架構已經有了初步的了解。所以并不會很詳細的搬源碼。大部分東西都會直接拿來用。涉及驅動的部分也不會做詳盡說明,只會把一些關鍵點串起來。文中也不會涉及Java Binder。Java Binder在這里完全是干擾。

概述

這里首先列一下Binder中的一些內容:

  • 除了service_manager,其他應用級進程/線程,和Binder驅動打交道的都是IPCThreadState。
  • 我們只關注進程間通信的核心流程:transaction。
  • 傳輸發起者需要告訴Binder三個東西:handle(你找誰?), cmd(你要干啥?)和data(東西呢?)。
  • 在客戶端handleBpBinder持有。不管什么樣的服務在Binder眼里就是一串數字。記住這一點。
  • 在server端真正的服務是BBinder來完成的。
error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                            &reply, tr.flags);
  • 顯然Binder驅動幫我們完成了從本地的一串數字handle到遠端實體服務BBinder的轉換。

這里就引出了幾個問題:

  1. 本地handle是如何轉換為遠端BBinder的?
  2. 這個handle是從哪里來的呢?

第二個問題可能有些同學會說是從service_manager那里查來的,然而并沒有這么簡單。這個問題我們稍后再解釋。這里先假設我們已經有了這個handle來說明第一個問題。

Binder驅動傳輸流程

這個流程其實就只有三步:第一,客戶端把數據寫進來。第二,把數據從客戶端傳到服務端。第三,通知服務端把數據讀走。

這個流程我希望大家能緊盯著handle因為它是我們理解問題1的唯一線索。

客戶端把數據寫進來

在用戶空間,函數IPCThreadState::writeTransactionData()負責寫入驅動。
這個函數會構造一個binder_transaction_data的結構體tr。我們的handle會被寫進去。

status_t IPCThreadState::writeTransactionData(...
    int32_t handle, ...)
{
    binder_transaction_data tr;

   ...
   // handle在這里
    tr.target.handle = handle;
    tr.code = code;
   ...
}

然后就到了驅動那里

static int binder_thread_write(...) {
...
//我們只關注BC_TRANSACTION
case BC_TRANSACTION:
case BC_REPLY: {
            struct binder_transaction_data tr;
            //tr到了這里
            if (copy_from_user(&tr, ptr, sizeof(tr)))
                return -EFAULT;
            ptr += sizeof(tr);
            //這個函數做傳輸工作
            binder_transaction(proc, thread, &tr,
                       cmd == BC_REPLY, 0);
            break;
        }
 ...
}

寫進來的可能是各種命令,我們只關注transaction

把數據從客戶端傳到服務端

這事是由函數binder_transaction()來干的。注意看一下那幾個入參,proc代表客戶端進程。thread代表客戶端線程,tr呢就是最開始的那個,記住里面放著handle

傳輸的前提是要找到目標的進程\線程。而我們唯一的線索就是handle。怎么通過handle來獲得目標進程\線程呢?

static void binder_transaction(...) {
...
if (tr->target.handle) {
            //通過handle查找目標引用
            target_ref = binder_get_ref(proc, tr->target.handle,
                         true);
            //通過目標引用得到目標節點。
            target_node = target_ref->node;
        }
        ...
}
//從node里拿到目標進程的信息
target_proc = target_node->proc;
...
//然后搞了個binder_transaction的結構體
t = kzalloc(sizeof(*t), GFP_KERNEL);
...
//設置目標進程/線程
t->to_proc = target_proc;
t->to_thread = target_thread;
...
//設置目標節點
t->buffer->target_node = target_node;
...
//要干的活是傳輸
t->work.type = BINDER_WORK_TRANSACTION;
...
//把傳輸任務插入到目標進程/線程的todo隊列。
binder_proc_transaction(t, target_proc, target_thread);
...

把數據從客戶端傳到服務端階段就完成了。從上述關鍵代碼我們可以看到,要完成傳輸就需要拿到目標進程的信息。而我們只有handle。驅動會先拿這個handle在自身進程信息proc內查找節點的應用,然后由這個引用就能得到節點,得到節點就能拿到目標進程的信息target_proc了。那么ref是啥?node又是啥呢?為什么他們能通過handle找的到呢?

node可以說是Binder驅動的核心數據結構。node會以紅黑樹的形式保存在服務端進程結構體內,客戶端則保存的是它的引用ref。而handle來自ref->desc

noderef是什么時候保存到Binder驅動中的呢?這個我們在后面解決handle從哪里來的問題再解釋。

所以在傳輸階段我們看到handle換成了node。在插入到目標進程todo隊列的數據里只有node而沒有handle。handle已經完成了它的使命,接下來讓我們關注node

通知服務端把數據讀走

static int binder_thread_read(...) {

...
//有活干了
w = list_first_entry(...);
...
//看看是什么活,我們只關心transaction
switch (w->type) {
case BINDER_WORK_TRANSACTION: {
            t = container_of(w, struct binder_transaction, work);
        } break;
}
...
if (t->buffer->target_node) {
            tr.target.ptr = target_node->ptr;
            tr.cookie =  target_node->cookie;
            ...
            cmd = BR_TRANSACTION;
        }
        ...
        tr.code = t->code;
}

cmd設置成了BR_TRANSACTION;binder_thread_read()會拼一個binder_transaction_data。也就是tr出來。它是要被送往用戶空間的,就像之前從用戶空間往binder驅動里寫的也是一個tr一樣。這里注意驅動把node里的兩個東西設置給了tr。ptrcookie。記住它們。

接下來就回到用戶空間了:

status_t IPCThreadState::executeCommand(int32_t cmd) {

//我們只關心傳輸
  case BR_TRANSACTION:
        {
            if (tr.target.ptr) {
                if (reinterpret_cast<RefBase::weakref_type*>(
                        tr.target.ptr)->attemptIncStrong(this)) {
                    error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer,
                            &reply, tr.flags);
                    reinterpret_cast<BBinder*>(tr.cookie)->decStrong(this);
                } 
        }
        break;
}

啊哈,這里我們就能看出來原來cookie的真身就是BBinder。也就是我們服務的真身了啊。

所以捋一下Binder驅動的傳輸過程就是從客戶端的handle在自身進程信息proc里查找ref。再通過ref就能得到node。而node則是存儲在服務端進程信息里的。node就很關鍵了,從它身上能找到目標進程的信息和目標服務的真身BBinder實例。然后回到用戶區直接調用就好了。

傳輸過程就介紹完了,接下來我們還有解決另外一個系列問題,handle從從哪里來?node又是什么時候給保存到內核里去的呢?

對了,這就涉及到services_manager了。

服務注冊及獲取

我們知道services_manager是個特殊的服務。有點像個DNS服務器,其他什么AMS,PMS等等都需要先到它那里注冊以后,客戶們才能在services_manager找到他們。既然services_manager這么特殊,那它在驅動中肯定會有個獨一無二的node吧。

所以services_manager啟動的時候會給Binder驅動發這么個命令BINDER_SET_CONTEXT_MGR。
Binder驅動照辦:

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
    ...
    temp = binder_new_node(proc, 0, 0);
    ...
    context->binder_context_mgr_node = temp;
    binder_put_node(temp);
...
}

函數binder_new_node()是用來新建node

static struct binder_node *binder_new_node(struct binder_proc *proc,
                       binder_uintptr_t ptr,
                       binder_uintptr_t cookie)

三個入參,procptrcookie?;叵胍幌律厦娼榻B傳輸過程的時候這三個參數是不是都用到過?當然,對于services_manager來說只需要進程信息,其他兩個入參都是0。這個特殊的node被設置給context->binder_context_mgr_node

接下來讓我們看一下服務注冊過程:

注冊服務

注冊服務的要點是盯住服務實體Binder是如何轉化為node的。

我們都知道service_manager特殊,特殊就特殊在它的handle就是0。所以無論是注冊服務還是獲取服務,我們不需要去別處問,自己就可以搞出來個代表service_managerBpBinder。注冊的時候是這樣的

virtual status_t addService(const String16& name, const sp<IBinder>& service,
                                bool allowIsolated, int dumpsysPriority) {
        Parcel data, reply;
        data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
        data.writeString16(name);
        data.writeStrongBinder(service);
        data.writeInt32(allowIsolated ? 1 : 0);
        data.writeInt32(dumpsysPriority);
        status_t err = remote()->transact(ADD_SERVICE_TRANSACTION, data, &reply);
        return err == NO_ERROR ? reply.readExceptionCode() : err;
    }

注意這句data.writeStrongBinder(service);也就是說把服務自身給寫到要傳輸的數據里面去了。
寫成啥樣了呢?

status_t flatten_binder(){
...
flat_binder_object obj;
...
 IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            const int32_t handle = proxy ? proxy->handle() : 0;
            obj.hdr.type = BINDER_TYPE_HANDLE;
            obj.binder = 0;
            obj.handle = handle;
            obj.cookie = 0;
        } else {
            obj.hdr.type = BINDER_TYPE_BINDER;
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
            obj.cookie = reinterpret_cast<uintptr_t>(local);
        }
...
}

因為這里是注冊服務,所以給的Binder是實體Binder,走的是else分支。類型為BINDER_TYPE_BINDER,cookie設置為service實體。

注冊服務是一次handle為0的Binder傳輸?;叵胛覀冎坝懻摰膫鬏斶^程,handle如果是0的話會在傳輸過程走不一樣的分支:

static void binder_transaction(...) {
...
if (tr->target.handle) {
            ...
        } else {
            target_node = context->binder_context_mgr_node;
        }
//從node里拿到目標進程的信息
target_proc = target_node->proc;

...
        switch (hdr->type) {
        case BINDER_TYPE_BINDER:
        case BINDER_TYPE_WEAK_BINDER: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_binder(fp, t, thread);
        } break;
        case BINDER_TYPE_HANDLE:
        case BINDER_TYPE_WEAK_HANDLE: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_handle(fp, t, thread);
        } break;

...

//把傳輸任務插入到目標進程/線程的todo隊列。
binder_proc_transaction(t, target_proc, target_thread);
...

對于注冊服務的流程來說在傳輸的時候有兩個地方需要注意,

  1. 由于handle是0,所以直接會找到binder_context_mgr_node。這個node也就是我們之前看到的service_manager初始化的時候創建的,也就是說我們直接就扎到了service_manager
  2. 還記得我們之前把服務實體放在傳輸數據里面了嗎?這里驅動會檢查傳輸的數據,如果發現有BINDER_TYPE_XXX類型的都要做轉換,為啥要轉呢?因為我的實體到你那里就得是個handle啊, 否則都不在同一個進程,不轉換的話你拿我的實體也沒用啊。所以,這里我們的服務實體要做一次轉換了:
static int binder_translate_binder(struct flat_binder_object *fp,
                   struct binder_transaction *t,
                   struct binder_thread *thread)
{
    struct binder_node *node;
    struct binder_ref *ref;
    struct binder_proc *proc = thread->proc;
    struct binder_proc *target_proc = t->to_proc;

    node = binder_get_node(proc, fp->binder);
    if (!node) {
        node = binder_new_node(proc, fp->binder, fp->cookie);
    }

    ref = binder_get_ref_for_node(target_proc, node, &thread->todo);

    if (fp->hdr.type == BINDER_TYPE_BINDER)
        fp->hdr.type = BINDER_TYPE_HANDLE;
    else
        fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
    fp->binder = 0;
    fp->handle = ref->desc;
    fp->cookie = 0;
...
}

注意看這里的轉換,首先是在自己的node樹里面找有沒有實體服務Binder對應的node。這里因為我們是要注冊服務,所以node不存在,需要新建一個。你看至此我們的新服務已經有了自己的node了。然后呢會在目標進程信息,也就是service_manager中找這個node的引用,沒有的話會新建一個引用的,這個過程會產生一個數放在ref->desc。對了,這個數就是我們的新服務在service_manager那邊的handle了。最后就是對原始數據做變身了。類型從BINDER變為HANDLE。清空bindercookie。給handle字段賦值,我們的實體Binder就這樣華麗的變成自己的分身了。

千萬不要搞混了,這里做轉換的是我們要注冊的服務,完全是處在事務數據中。而傳輸這些事務數據的是handle為0,對應node是binder_context_mgr_node的事務。

然后就是service_manager那邊讀取傳輸的數據。拿到那個轉換以后的fp->handle和服務的名字一起保存起來。

總體而言注冊服務:

  • 是一次對service_manager的傳輸,只不過需要在開始的時候把服務實體Binder放在要傳輸的數據里。
  • Binder驅動會在傳輸過程中做一次從實體服務Binder到對應handle的變換。
  • 在這個變換的過程中會創建實體服務Binder對應的node。

接下來我們看一下獲取服務的過程。貌似應該已經一目了然了??蛻舳四弥彰秩?code>service_manager那里去請求handle。拿到以后就可以實例化BpBinder去調用服務了。

獲取服務

獲取服務的過程分兩段,一個是把服務名字傳過去,另一個是把handle傳回來。去程沒啥好說的,和上面的注冊服務過程差不多,還少了中間做轉換的步驟。回程就是不一樣的操作了。service_manager把查到的handle通過BC_REPLY給送回來的。別看handle只是個數,在傳輸的時候必須把它包裝一下,變成這樣的:

    obj->hdr.type = BINDER_TYPE_HANDLE;
    obj->handle = handle;
    obj->cookie = 0;

類型是BINDER_TYPE_HANDLE

這個數據結構會寫入Binder驅動,命令BC_REPLY會走到和BC_TRANSACTION一樣的函數,也就是binder_transaction(),但是某些分支有有所不同。

binder_transaction(...){

if (reply) {
in_reply_to = thread->transaction_stack;
target_thread = in_reply_to->from;
target_proc = target_thread->proc;
}
...
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
            struct flat_binder_object *fp;
            fp = to_flat_binder_object(hdr);
            ret = binder_translate_handle(fp, t, thread);
        } break;
        ...
}

可見返回過程不需要再查找node因為調用端是誰是清楚的。直接把from變成target就行了。

因為返回數據塊里面放著獲取到的服務handle。同樣的這里會給它做個變換。

看一下binder_translate_handle()如何做變換:

static int binder_translate_handle(struct flat_binder_object *fp,
                   struct binder_transaction *t,
                   struct binder_thread *thread)
{
    struct binder_ref *ref;
    struct binder_proc *proc = thread->proc;
    struct binder_proc *target_proc = t->to_proc;

    ref = binder_get_ref(proc, fp->handle,
                 fp->hdr.type == BINDER_TYPE_HANDLE);

    if (ref->node->proc == target_proc) {
        if (fp->hdr.type == BINDER_TYPE_HANDLE)
            fp->hdr.type = BINDER_TYPE_BINDER;
        else
            fp->hdr.type = BINDER_TYPE_WEAK_BINDER;
        fp->binder = ref->node->ptr;
        fp->cookie = ref->node->cookie;
        
    } else {
        struct binder_ref *new_ref;
        new_ref = binder_get_ref_for_node(target_proc, ref->node, NULL);
        fp->binder = 0;
        fp->handle = new_ref->desc;
        fp->cookie = 0;
    }
}

這里的轉換有兩個分支,如果此服務進程和目標進程相同,則將handle轉換成實體Binder。也就是說你去service_manager那里獲取到了和你同一個進程的服務,自然可以轉換為實體Binder直接拿來用了。

如果不是同一個進程呢?則會到目標進程信息那里去獲取一個新的handle。替換掉原來的handle返回給調用者。

為什么會這樣呢?這就需要深入了解noderef的關系。

  • node實體只保存在創建者進程所對應的proc中。
  • ref是保存在當前進程中對外部node實體的引用,不同的進程對同一個noderef是不同的。
  • 所以可以認為handle是進程私有的,不要想當然的認為從service_manager拿到的handle就是和自己進程拿到的一樣。

總結

Binder雖然結構復雜,難以全面掌握。但有時又是Android開發者在實際工作中不得不面對的問題。掌握Binder需要投入大量精力和努力。通過一些文章的指引,是可以在學習Binder的路途上減輕一些壓力,避免一些誤區的。本文就希望能幫助開發者打通一些在學習Binder時可能會遇到的阻塞點,特別是這些阻塞點基本上會存在于Binder驅動層。畢竟樓再高,蓋起來還是要先從地基打起,Binder驅動就是Binder這座大廈的基礎。打好這個基礎,往上的就都是包裹和通道了,理解起來也就會比較輕松。希望能對大家有所幫助。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。