細讀《深入理解 Android 內核設計思想》(五)Binder 機制 [下]

對冗余挑揀重點,對重點深入補充,輸出結構清晰的精簡版

  1. 深入 binder 驅動內部
    binder_ioctl
    binder_get_thread
    binder_ioctl_write_read
    binder_thread_write
    binder_transaction
    binder_thread_read
    小結
  2. binder Q&A
    如何找到目標進程 Binder 實體
    如何實現(xiàn) Binder 線程的睡眠與喚醒
  3. 最后

深入 binder 驅動內部

前兩篇文章都有提到 binder_ioctl 方法,在 Binder 機制 [上] 中介紹了 binder_ioctl 支持的命令;Binder 機制 [中] 中提到 IPCThreadState 會調用到 binder_ioctl 方法。

書中對 binder 驅動內部調用的講解沒有分為較清晰的步驟,一口氣就是 20 頁篇幅的源碼詳解,理解起來有些難度,容易迷失。在細讀了三四遍后,終于感覺對整體有些掌握了,結合前面的學習與自己的理解,將一次 IPC 調用中 binder 驅動的工作分為以下 5 步:

1.準備數(shù)據(jù),根據(jù)命令分發(fā)給具體的方法去處理
2.找到目標進程的相關信息
3.將數(shù)據(jù)一次拷貝到目標進程所映射的物理內存塊
4.記錄待處理的任務,喚醒目標線程
5.調用線程進入休眠
6.目標進程直接拿到數(shù)據(jù)進行處理,處理完后喚醒調用線程
7.調用線程返回處理結果

與上篇文章一樣仍以 getService() 為例,按照上面的工作步驟為脈絡,深入分析驅動層中的執(zhí)行邏輯,徹底搞定 binder 驅動!

binder_ioctl

在 IPCThreadState 中這樣調用了 binder_ioctl() 方法:

    ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)

binder_ioctl() 方法中會根據(jù) BINDER_WRITE_READ、BINDER_SET_MAX_THREADS 等不同 cmd 轉調到不同的方法去執(zhí)行,這里我們只關注 BINDER_WRITE_READ,簡化后代碼如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){
    int ret;
    //拿到調用進程在 binder_open() 中記錄的 binder_proc
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    binder_lock(__func__);
    //獲取調用線程 binder_thread
    thread = binder_get_thread(proc);
    switch (cmd) {
    case BINDER_WRITE_READ:
        //處理 binder 數(shù)據(jù)讀寫,binder IPC 通信的核心邏輯
        ret = binder_ioctl_write_read(filp, cmd, arg, thread);
        if (ret)
            goto err;
        break;
    case BINDER_SET_MAX_THREADS:{...} //設置 binder 最大線程數(shù)
    case BINDER_SET_CONTEXT_MGR:{...} //設置 service 大管家,即 ServiceManager
    case BINDER_THREAD_EXIT:{...} //binder 線程退出命令,釋放相關資源
    case BINDER_VERSION: {...} //獲取 binder 驅動版本號
    ...
}

Binder 機制 [上] 中詳細介紹過 binder_open() 方法,它主要做了兩個工作:1.創(chuàng)建及初始化每個進程獨有一份的、用來存放 binder 相關數(shù)據(jù)的 binder_proc 結構體,2.將 binder_proc 記錄起來,方便后續(xù)使用。正是通過 file 來記錄的:

static int binder_open(struct inode *nodp, struct file *filp){
    ...
    filp->private_data = proc;
    ...
}

拿到調用進程后,進一步通過 binder_get_thread() 方法拿到調用線程,然后就交給 binder_ioctl_write_read() 方法去執(zhí)行具體的 binder 數(shù)據(jù)讀寫了,可見 binder_ioctl() 方法本身的邏輯非常簡單,將數(shù)據(jù) arg 透傳了出去。下面分別來看 binder_get_thread()、binder_ioctl_write_read() 這兩個方法。

binder_get_thread

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; //從 proc 中獲取紅黑樹根節(jié)點
    //查找 pid 等于當前線程 id 的thread,該紅黑樹以 pid 大小為序存放
    while (*p) {
        parent = *p;
        thread = rb_entry(parent, struct binder_thread, rb_node);
        if (current->pid < thread->pid) //current->pid 是當前調用線程的 id
            p = &(*p)->rb_left;
        else if (current->pid > thread->pid)
            p = &(*p)->rb_right;
        else
            break;
    }
    if (*p == NULL) {//如果沒有找到,則新創(chuàng)建一個
        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);       //初始化待處理隊列
        rb_link_node(&thread->rb_node, parent, p);  //加入到 proc 的 threads 紅黑樹中
        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_thread 是用來描述線程的結構體,binder_get_thread() 方法中邏輯也很簡單,首先從調用進程 proc 中查找當前線程是否已被記錄,如果找到就直接返回,否則新建一個返回,并記錄到 proc 中。也就是說所有調用 binder_ioctl() 的線程,都會被記錄起來。

binder_ioctl_write_read

此方法分為兩部分來看,首先是整體:

static int binder_ioctl_write_read(struct file *filp,
                unsigned int cmd, unsigned long arg,
                struct binder_thread *thread){
    int ret = 0;
    struct binder_proc *proc = filp->private_data;
    unsigned int size = _IOC_SIZE(cmd);
    void __user *ubuf = (void __user *)arg; //用戶傳下來的數(shù)據(jù)賦值給 ubuf
    struct binder_write_read bwr;
    //把用戶空間數(shù)據(jù) ubuf 拷貝到 bwr
    if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
        ret = -EFAULT;
        goto out;
    }
    處理數(shù)據(jù)...
    //將讀寫后的數(shù)據(jù)寫回給用戶空間
    if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
        ret = -EFAULT;
        goto out;
    }
out:
    return ret;
}

起初看到 copy_from_user() 方法時難以理解,因為它看起來是將我們要傳輸?shù)臄?shù)據(jù)拷貝到內核空間了,但目前還沒有看到 server 端的任何線索,bwr 跟 server 端沒有映射關系,那后續(xù)再將 bwr 傳輸給 server 端的時候又要拷貝,這樣豈不是多次拷貝了?

其實這里的 copy_from_user() 方法并沒有拷貝要傳輸?shù)臄?shù)據(jù),而僅是拷貝了持有傳輸數(shù)據(jù)內存地址的 bwr。后續(xù)處理數(shù)據(jù)時會根據(jù) bwr 信息真正的去拷貝要傳輸?shù)臄?shù)據(jù)。

處理完數(shù)據(jù)后,會將處理結果體現(xiàn)在 bwr 中,然后返回給用戶空間處理。那是如何處理數(shù)據(jù)的呢?所謂的處理數(shù)據(jù),就是對數(shù)據(jù)的讀寫而已:

    if (bwr.write_size > 0) {//寫數(shù)據(jù)
        ret = binder_thread_write(proc, 
             thread,
             bwr.write_buffer, bwr.write_size,
             &bwr.write_consumed);
        trace_binder_write_done(ret);
        if (ret < 0) { //寫失敗
            bwr.read_consumed = 0;
            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
                ret = -EFAULT;
            goto out;
        }
    }
    if (bwr.read_size > 0) {//讀數(shù)據(jù)
        ret = binder_thread_read(proc, thread, bwr.read_buffer,
            bwr.read_size,
            &bwr.read_consumed,
            filp->f_flags & O_NONBLOCK);
        trace_binder_read_done(ret);
        if (!list_empty(&proc->todo))
            wake_up_interruptible(&proc->wait);//喚醒等待狀態(tài)的線程
        if (ret < 0) { //讀失敗
            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
                ret = -EFAULT;
            goto out;
        }
    }

可見 binder 驅動內部依賴用戶空間的 binder_write_read 決定是要讀取還是寫入數(shù)據(jù):其內部變量 read_size>0 則代表要讀取數(shù)據(jù),write_size>0 代表要寫入數(shù)據(jù),若都大于 0 則先寫入,后讀取。

至此焦點應該集中在 binder_thread_write() 和 binder_thread_read(),下面分析這兩個方法。

binder_thread_write

在上面的 binder_ioctl_write_read() 方法中調用 binder_thread_write() 時傳入了 bwr.write_buffer、bwr.write_size 等,先搞清楚這些參數(shù)是什么。

最開始是在用戶空間 IPCThreadState 的 transact() 中通過 writeTransactionData() 方法創(chuàng)建數(shù)據(jù)并寫入 mOut 的,writeTransactionData 方法代碼如下:

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.target.ptr = 0;
    tr.target.handle = handle; //目標 server 的 binder 的句柄
    tr.code = code; //請求碼,getService() 服務對應的是 GET_SERVICE_TRANSACTION
    tr.flags = binderFlags;
    tr.cookie = 0;
    tr.sender_pid = 0;
    tr.sender_euid = 0;
    const status_t err = data.errorCheck(); //驗證數(shù)據(jù)合理性
    if (err == NO_ERROR) {
        tr.data_size = data.ipcDataSize(); //傳輸數(shù)據(jù)大小
        tr.data.ptr.buffer = data.ipcData(); //傳輸數(shù)據(jù)
        tr.offsets_size = data.ipcObjectsCount()*sizeof(binder_size_t);
        tr.data.ptr.offsets = data.ipcObjects();
    } else {...}
    mOut.writeInt32(cmd); // transact 傳入的 cmd 是 BC_TRANSACTION
    mOut.write(&tr, sizeof(tr)); //打包成 binder_transaction_data
    return NO_ERROR;
}

然后在 IPCThreadState 的 talkWithDriver() 方法中對 write_buffer 賦值:

    bwr.write_buffer = (uintptr_t)mOut.data();

搞清楚了數(shù)據(jù)的來源,再來看 binder_thread_write() 方法,binder_thread_write() 方法中處理了大量的 BC_XXX 命令,代碼很長,這里我們只關注當前正在處理的 BC_TRANSACTION 命令,簡化后代碼如下:

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){
    uint32_t cmd;
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer; //就是 bwr.write_buffer
    void __user *ptr = buffer + *consumed; //數(shù)據(jù)起始地址
    void __user *end = buffer + size; //數(shù)據(jù)結束地址
    while (ptr < end && thread->return_error == BR_OK) { //可能有多個命令及對應數(shù)據(jù)要處理,所以要循環(huán)
        if (get_user(cmd, (uint32_t __user *)ptr)) // 讀取一個 cmd
            return -EFAULT;
        ptr += sizeof(uint32_t); //跳過 cmd 所占的空間,指向要處理的數(shù)據(jù)
        switch (cmd) {
            case BC_TRANSACTION:
            case BC_REPLY: {
                 struct binder_transaction_data tr; //與 writeTransactionData 中準備的數(shù)據(jù)結構體對應
                 if (copy_from_user(&tr, ptr, sizeof(tr))) //拷貝到內核空間 tr 中
                    return -EFAULT;
                 ptr += sizeof(tr); //跳過數(shù)據(jù)所占空間,指向下一個 cmd
                 binder_transaction(proc, thread, &tr, cmd == BC_REPLY); //處理數(shù)據(jù)
                 break;
            }
            處理其他 BC_XX 命令...
        }
    *consumed = ptr - buffer; //被寫入處理消耗的數(shù)據(jù)量,對應于用戶空間的 bwr.write_consumed

binder_thread_write() 中從 bwr.write_buffer 中取出了 cmd 和 cmd 對應的數(shù)據(jù),進一步交給 binder_transaction() 處理,需要注意的是,BC_TRANSACTION、BC_REPLY 這兩個命令都是由 binder_transaction() 處理的。

簡單梳理一下,由 binder_ioctl -> binder_ioctl_write_read -> binder_thread_write ,到目前為止還只是在準備數(shù)據(jù),沒有看到跟目標進程相關的任何處理,都屬于 "準備數(shù)據(jù),根據(jù)命令分發(fā)給具體的方法去處理" 第 1 個工作。而到此為止,第 1 個工作便結束,下一步的 binder_transaction() 方法終于要開始后面的工作了。

binder_transaction

binder_transaction() 方法中代碼較長,先總結它干了哪些事:對應開頭列出的工作,此方法中做了非常關鍵的 2-4 步:

  • 找到目標進程的相關信息
  • 將數(shù)據(jù)一次拷貝到目標進程所映射的物理內存塊
  • 記錄待處理的任務,喚醒目標線程

以這些工作為線索,將代碼分為對應的部分來看,首先是找到目標進程的相關信息,簡化后代碼如下:

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply){
    struct binder_transaction *t; //用于描述本次 server 端要進行的 transaction
    struct binder_work *tcomplete; //用于描述當前調用線程未完成的 transaction
    binder_size_t *offp, *off_end;
    struct binder_proc *target_proc; //目標進程
    struct binder_thread *target_thread = NULL; //目標線程
    struct binder_node *target_node = NULL; //目標 binder 節(jié)點
    struct list_head *target_list; //目標 TODO 隊列
    wait_queue_head_t *target_wait; //目標等待隊列
    if(reply){ 
        in_reply_to = thread->transaction_stack;
        ...處理 BC_REPLY,暫不關注
    }else{ 
        //處理 BC_TRANSACTION
        if (tr->target.handle) { //handle 不為 0
            struct binder_ref *ref;
            //根據(jù) handle 找到目標 binder 實體節(jié)點的引用
            ref = binder_get_ref(proc, tr->target.handle);
            target_node = ref->node; //拿到目標 binder 節(jié)點
        } else { 
            // handle 為 0 則代表目標 binder 是 service manager
            // 對于本次調用來說目標就是 service manager
            target_node = binder_context_mgr_node;
        }
    }
    target_proc = target_node->proc; //拿到目標進程
    if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) {
        struct binder_transaction *tmp;
        tmp = thread->transaction_stack;
        while (tmp) {
            if (tmp->from && tmp->from->proc == target_proc)
                target_thread = tmp->from; //拿到目標線程
            tmp = tmp->from_parent;
        }
    }
    target_list = &target_thread->todo; //拿到目標 TODO 隊列
    target_wait = &target_thread->wait; //拿到目標等待隊列

binder_transaction、binder_work 等結構體在上一篇中有介紹,上面代碼中也詳細注釋了它們的含義。比較關鍵的是 binder_get_ref() 方法,它是如何找到目標 binder 的呢?這里暫不延伸,下文再做分析。

繼續(xù)看 binder_transaction() 方法的第 2 個工作,將數(shù)據(jù)一次拷貝到目標進程所映射的物理內存塊

    t = kzalloc(sizeof(*t), GFP_KERNEL); //創(chuàng)建用于描述本次 server 端要進行的 transaction
    tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL); //創(chuàng)建用于描述當前調用線程未完成的 transaction
    if (!reply && !(tr->flags & TF_ONE_WAY)) //將信息記錄到 t 中:
        t->from = thread; //記錄調用線程
    else
        t->from = NULL;
    t->sender_euid = task_euid(proc->tsk);
    t->to_proc = target_proc; //記錄目標進程
    t->to_thread = target_thread; //記錄目標線程
    t->code = tr->code; //記錄請求碼,getService() 對應的是 GET_SERVICE_TRANSACTION
    t->flags = tr->flags;
    //實際申請目標進程所映射的物理內存,準備接收要傳輸?shù)臄?shù)據(jù)
    t->buffer = binder_alloc_buf(target_proc, tr->data_size,
        tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
    //申請到 t->buffer 后,從用戶空間將數(shù)據(jù)拷貝進來,這里就是一次拷貝數(shù)據(jù)的地方!!
    if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
        tr->data.ptr.buffer, tr->data_size)) {
        return_error = BR_FAILED_REPLY;
        goto err_copy_data_failed;
    }

為什么在拷貝之前要先申請物理內存呢?在 Binder 機制 [上] 中介紹 binder_mmap() 時詳細分析過,雖然 binder_mmap() 直接映射了 (1M-8K) 的虛擬內存,但卻只申請了 1 頁的物理頁面,等到實際使用時再動態(tài)申請。也就是說,在 binder_ioctl() 實際傳輸數(shù)據(jù)的時候,再通過 binder_alloc_buf() 方法去申請物理內存。

至此已經(jīng)將要傳輸?shù)臄?shù)據(jù)拷貝到目標進程,目標進程可以直接讀取到了,接下來要做的就是將目標進程要處理的任務記錄起來,然后喚醒目標進程,這樣在目標進程被喚醒后,才能知道要處理什么任務。

最后來看 binder_transaction() 方法的第 3 個工作,記錄待處理的任務,喚醒目標線程

    if (reply) { //如果是處理 BC_REPLY,pop 出來棧頂記錄的 transaction(實際上是刪除鏈表頭元素)
        binder_pop_transaction(target_thread, in_reply_to);
    } else if (!(t->flags & TF_ONE_WAY)) {
        //如果不是 oneway,將 server 端要處理的 transaction 記錄到當前調用線程
        t->need_reply = 1;
        t->from_parent = thread->transaction_stack;
        thread->transaction_stack = t;
    } else {
        ...暫不關注 oneway 的情況
    }
    t->work.type = BINDER_WORK_TRANSACTION;
    list_add_tail(&t->work.entry, target_list); //加入目標的處理隊列中
    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; //設置調用線程待處理的任務類型
    list_add_tail(&tcomplete->entry, &thread->todo); //記錄調用線程待處理的任務
    if (target_wait)
        wake_up_interruptible(target_wait); //喚醒目標線程

再次梳理一下,至此已經(jīng)完成了前四個工作:

1.準備數(shù)據(jù),根據(jù)命令分發(fā)給具體的方法去處理
2.找到目標進程的相關信息
3.將數(shù)據(jù)一次拷貝到目標進程所映射的物理內存塊
4.記錄待處理的任務,喚醒目標線程

其中第 1 個工作涉及到的方法為 binder_ioctl() -> binder_get_thread() -> binder_ioctl_write_read() -> binder_thread_write() ,主要是一些數(shù)據(jù)的準備和方法轉跳,沒做什么實質的事情。而 binder_transaction() 方法中做了非常重要的 2-4 工作。

剩下的工作還有:

5.調用線程進入休眠
6.目標進程直接拿到數(shù)據(jù)進行處理,處理完后喚醒調用線程
7.調用線程返回處理結果

可以想到,5 和 6 其實沒有時序上的限制,而是并行處理的。下面先來看第 5 個工作:調用線程是如何進入休眠等待服務端執(zhí)行結果的。

binder_thread_read

在喚醒目標線程后,調用線程就執(zhí)行完 binder_thread_write() 寫完了數(shù)據(jù),返回到 binder_ioctl_write_read() 方法中,接著執(zhí)行 binder_thread_read() 方法。

而調用線程的休眠就是在此方法中觸發(fā)的,下面將 binder_thread_read() 分為兩部分來看,首先是是否阻塞當前線程的判斷邏輯:

static int binder_thread_read(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed, int non_block){
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer; //bwr.read_buffer
    void __user *ptr = buffer + *consumed; //數(shù)據(jù)起始地址
    void __user *end = buffer + size; //數(shù)據(jù)結束地址
    if (*consumed == 0) {
        if (put_user(BR_NOOP, (uint32_t __user *)ptr))
            return -EFAULT;
        ptr += sizeof(uint32_t);
    }
    //是否要準備睡眠當前線程
    wait_for_proc_work = thread->transaction_stack == NULL &&
            list_empty(&thread->todo);
    if (wait_for_proc_work) {
        if (non_block) { //non_block 為 false
            if (!binder_has_proc_work(proc, thread))
                ret = -EAGAIN;
        } else
            ret = wait_event_freezable_exclusive(proc->wait, 
                        binder_has_proc_work(proc, thread));
    } else {
        if (non_block) { //non_block 為 false
            if (!binder_has_thread_work(thread))
                ret = -EAGAIN;
        } else
            ret = wait_event_freezable(thread->wait, 
                        binder_has_thread_work(thread));
    }

consumed 即用戶空間的 bwr.read_consumed,這里是 0 ,所以將一個 BR_NOOP 加到了 ptr 中。

怎么理解 wait_for_proc_work 條件呢?在 binder_transaction() 方法中將 server 端要處理的 transaction 記錄到了當前調用線程 thread->transaction_stack 中;將當前調用線程待處理的任務記錄到了 thread->todo 中。所以這里的 thread->transaction_stack 和 thread->todo 都不為空,wait_for_proc_work 為 false,代表不準備阻塞當前線程。

但 wait_for_proc_work 并不是決定是否睡眠的最終條件,接著往下看,其中 non_block 恒為 false,那是否要睡眠當前線程就取決于 binder_has_thread_work() 的返回值,binder_has_thread_work() 方法如下:

static int binder_has_thread_work(struct binder_thread *thread){
    return !list_empty(&thread->todo) || thread->return_error != BR_OK ||
        (thread->looper & BINDER_LOOPER_STATE_NEED_RETURN);
}

thread->todo 不為空,所以 binder_has_thread_work() 返回 true,當前調用線程不進入休眠,繼續(xù)往下執(zhí)行。你可能會有疑問,不是說調用線程的休眠就是在 binder_thread_read() 方法中觸發(fā)的嗎?確實是,只不過不是本次,先接著分析 binder_thread_read() 繼續(xù)往下要執(zhí)行的邏輯:

struct binder_work *w;
w = list_first_entry(&thread->todo, struct binder_work,entry);
switch (w->type) {
    case BINDER_WORK_TRANSACTION_COMPLETE: {
        cmd = BR_TRANSACTION_COMPLETE;
        if (put_user(cmd, (uint32_t __user *)ptr))
            return -EFAULT;
        ptr += sizeof(uint32_t);
        binder_stat_br(proc, thread, cmd);
        list_del(&w->entry); //刪除 binder_work 在 thread->todo 中的引用
        kfree(w);
    }
    case BINDER_WORK_NODE{...}
    case BINDER_WORK_DEAD_BINDER{...}
    ...

在上面 binder_transaction() 方法最后,將 BINDER_WORK_TRANSACTION_COMPLETE 類型的 binder_work 加入到 thread->todo 中。而這里就是對這個 binder_work 進行處理,將一個 BR_TRANSACTION_COMPLETE 命令加到了 ptr 中。

梳理一下目前的邏輯,至此已經(jīng)順序執(zhí)行完 binder_thread_write()、binder_thread_read() 方法,并且在 binder_thread_read() 中往用戶空間傳輸了兩個命令:BR_NOOP 和 BR_TRANSACTION_COMPLETE。

本次 binder_ioctl() 調用就執(zhí)行完了,然后會回到 IPCThreadState 中,上一篇文章中詳細分析了 IPCThreadState 中的代碼,這里就不再展開,簡單概括一下后續(xù)執(zhí)行的邏輯:

mIn 中有 BR_NOOP 和 BR_TRANSACTION_COMPLETE 兩個命令,首先處理 BR_NOOP 命令,此命令什么也沒做,由于 talkWithDriver() 處于 while 循環(huán)中,會再一次進入 talkWithDriver(),但因為此時 mIn 中還有數(shù)據(jù)沒讀完,不會調用 binder_ioctl()。

然后處理 BR_TRANSACTION_COMPLETE 命令,如果是 oneway 就直接結束本次 IPC 調用,否則再一次進入 talkWithDriver(),第二次進入 talkWithDriver 時,bwr.write_size = 0,bwr.read_size > 0,所以會第二次調用 binder_ioctl() 方法。第二次執(zhí)行 binder_ioctl() 時,bwr.write_size = 0,bwr.read_size > 0,所以不會再執(zhí)行 binder_thread_write() 方法,而只執(zhí)行 binder_thread_read() 方法。

第二次執(zhí)行 binder_thread_read() 時,thread->todo 已經(jīng)被處理為空,但是 thread->transaction_stack 還不為空,wait_for_proc_work 仍然為 false,但最終決定是否要休眠的條件成立了: binder_has_thread_work(thread) 返回 false,由此當前調用線程通過 wait_event_freezable() 進入休眠。

小結

至此還剩下兩個工作:

6.目標進程直接拿到數(shù)據(jù)進行處理,處理完后喚醒調用線程
7.調用線程返回處理結果

但是已經(jīng)不用再看代碼了,因為上述方法已經(jīng)覆蓋了剩下的工作。對于 getService() 來說,目標進程就是 Service Manager,相關的代碼在 Binder 機制 [上] 也已經(jīng)做過詳細的分析。用圖來概括整體的工作。調用進程邏輯:

Service Manager 端邏輯:

本節(jié)完整的分析了一次 IPC 調用中 binder 驅動內部具體的執(zhí)行邏輯,此部分也是 binder 機制中最難的,而將最難的部分掌握后,可以極大的提高信心。

想要完全掌握本節(jié)內容,前提是必須對前兩篇 binder 文章已經(jīng)熟悉,因為 binder 驅動和用戶空間的 IPCThreadState 以及 servicemanager 關聯(lián)是十分緊密的,如果沒有掌握,是無法真正理清本節(jié)內容的。

binder Q&A

在上面分析 binder 驅動代碼過程中,主要是根據(jù)文章開頭的七個工作為線索,為了不偏離主線,在遇到比較重要的知識點時都沒做延伸,這里以 Q&A 的方式補充分析。

如何找到目標進程 Binder 實體

servicemanager 的 binder 實體固定為 binder_context_mgr_node,直接返回即可。如何獲取其他 binder 實體呢?在上面的 binder_transaction() 方法中是這樣獲取目標 binder 的:

    //根據(jù) handle 找到目標 binder 實體節(jié)點的引用
    ref = binder_get_ref(proc, tr->target.handle);

binder_get_ref() 方法如下:

static struct binder_ref *binder_get_ref(struct binder_proc *proc,
                uint32_t desc){
    struct rb_node *n = proc->refs_by_desc.rb_node; //取紅黑樹根結點
    struct binder_ref *ref;
    while (n) { //遍歷查詢指定 desc(handle) 值的 binder 實體
        ref = rb_entry(n, struct binder_ref, rb_node_desc);
        if (desc < ref->desc)
            n = n->rb_left;
        else if (desc > ref->desc)
            n = n->rb_right;
        else
            return ref;
    }
    return NULL;
}

可見是在調用進程的 binder_proc 結構體中獲取到的,那目標 binder 實體又是什么時候存儲到調用進程中的呢?

對于實名的 Server,當它利用 addService() 把自身加到 ServiceManager 中時,會"路過" binder 驅動,binder 驅動就會把這一 binder 實體記錄到 ServiceManager 的 binder_proc->refs_by_desc 和 binder_proc->refs_by_node 中。

當一個 Binder Client 通過 getService() 向 ServiceManager 發(fā)起查詢時,ServiceManager 就可以準確的告訴 Client 目標 Binder 實體,將其也記錄到 Client 的 binder_proc->refs_by_desc 和 binder_proc->refs_by_node 中。

上面說的是通過 addService() 將自身注冊到 Service Manager 中的 Server,任何 Binder Client 都能通過 Service Manager 獲取到,這種叫做 "實名" Server。 而 Android 中還存在另一種 Binder Server,并不在 Service Manager 中注冊,比如我們自定義的 Service,這種可以叫做 "匿名" Server。

如何找到一個匿名 Server 的 binder 實體呢?匿名 Server 一般要通過其他實名 Server 為中介來傳遞,比如 IWindowSession 是靠 WindowManagerService 來傳遞的,當 Binder Client 調用 openSession 真正生成一個 Session 對象,這個對象作為 reply 第一次 "路過" binder 驅動時,就會被記錄到 Client 的 binder_proc->refs_by_desc 和 binder_proc->refs_by_node 中。

如何實現(xiàn) Binder 線程的睡眠與喚醒

答案在上文中已經(jīng)有很詳細的分析了,這里再概括一下:

喚醒:在 binder_transaction() 方法中寫完數(shù)據(jù)后,通過 wake_up_interruptible(target_wait) 喚醒目標線程

睡眠:在 binder_thread_read() 中,通過 wait_event_freezable_exclusive() 或 wait_event_freezable() 調用進入睡眠

最后

最后再對 binder 的整體架構做一個簡要的概述。對于一個比較典型的、兩個應用之間的 IPC 通信流程而言:

image.png

Client 通過 ServiceManager 或 AMS 獲取到的遠程 binder 實體,一般會用 Proxy 做一層封裝,比如 ServiceManagerProxy、 AIDL 生成的 Proxy 類。而被封裝的遠程 binder 實體是一個 BinderProxy。

BpBinder 和 BinderProxy 其實是一個東西:遠程 binder 實體,只不過一個 Native 層、一個 Java 層,BpBinder 內部持有了一個 binder 句柄值 handle。

ProcessState 是進程單例,負責打開 Binder 驅動設備及 mmap;IPCThreadState 為線程單例,負責與 binder 驅動進行具體的命令通信。

由 Proxy 發(fā)起 transact() 調用,會將數(shù)據(jù)打包到 Parcel 中,層層向下調用到 BpBinder ,在 BpBinder 中調用 IPCThreadState 的 transact() 方法并傳入 handle 句柄值,IPCThreadState 再去執(zhí)行具體的 binder 命令。

由 binder 驅動到 Server 的大概流程就是:Server 通過 IPCThreadState 接收到 Client 的請求后,層層向上,最后回調到 Stub 的 onTransact() 方法。

當然這不代表所有的 IPC 流程,比如 Service Manager 作為一個 Server 時,便沒有上層的封裝,也沒有借助 IPCThreadState,而是初始化后通過 binder_loop() 方法直接與 binder 驅動通信的。

鏈接:細讀《深入理解 Android 內核設計思想》系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容