聽說你Binder機制學的不錯,來解決下這幾個問題(一)

看書的小蝸牛發表於2017-03-15

Binder承擔了絕大部分Android程式通訊的職責,可以看做是Android的血管系統,負責不同服務模組程式間的通訊。在對Binder的理解上,可大可小,日常APP開發並不怎麼涉及Binder通訊知識,最多就是Service及AIDL的使用會涉及部分Binder知識。Binder往小了說可總結成一句話:一種IPC程式間通訊方式,負責程式A的資料,傳送到程式B。往大了說,其實涉及的知識還是很多的,如Android 對於原Binder驅動的擴充套件、Zygote程式孵化中對於Binder通訊的支援、Java層Binder封裝,Native層對於Binder通訊的封裝、Binder訃告機制等等。很多分析Binder框架的文都是從ServiceManager、Binder驅動、addService、getService來分析等來分析,其實這些主要是針對系統提供的服務,但是bindService啟動的服務走的卻還是有很大不同的。本篇文章主要簡述一些Binder難以理解的點,但不會太細的跟蹤分析,只拋磚,自己去發掘玉,由於篇幅過大,分三篇:

  • Binder的定向制導,如何找到目標Binder,喚起程式或者執行緒
  • Binder中的紅黑樹,為什麼會有兩棵binder_ref紅黑樹
  • Binder一次拷貝原理(直接拷貝到目標執行緒的核心空間,核心空間與使用者空間對應)
  • Binder傳輸資料的大小限制(核心4M 上層限制1m-8k),傳輸Bitmap過大,就會崩潰的原因,Activity之間傳輸BitMap
  • 系統服務與bindService等啟動的服務的區別
  • Binder執行緒、Binder主執行緒、Client請求執行緒的概念與區別
  • Client是同步而Server是非同步到底說的什麼
  • Android APP程式天生支援Binder通訊的原理是什麼
  • Android APP有多少Binder執行緒,是固定的麼
  • Binder執行緒的睡眠與喚醒(請求執行緒睡在哪個等待佇列上,喚醒目標端哪個佇列上的執行緒)
  • Binder協議中BC與BR的區別
  • Binder在傳輸資料的時候是如何層層封裝的--不同層次使用的資料結構(命令的封裝
  • Binder驅動傳遞資料的釋放(釋放時機)
  • 一個簡單的Binder通訊C/S模型
  • ServiceManager addService的限制(並非服務都能使用ServiceManager的addService)
  • bindService啟動Service與Binder服務實體的流程
  • Java層Binder實體與與BinderProxy是如何例項化及使用的,與Native層的關係是怎樣的
  • Parcel readStrongBinder與writeStrongBinder的原理

Binder如何精確制導,找到目標Binder實體,並喚醒程式或者執行緒

Binder實體服務其實有兩種,一是通過addService註冊到ServiceManager中的服務,比如ActivityManagerService、PackageManagerService、PowerManagerService等,一般都是系統服務;還有一種是通過bindService拉起的一些服務,一般是開發者自己實現的服務。這裡先看通過addService新增的被ServiceManager所管理的服務。有很多分析ServiceManager的文章,本文不分析ServiceManager,只是簡單提一下,ServiceManager是比較特殊的服務,所有應用都能直接使用,因為ServiceManager對於Client端來說Handle控制程式碼是固定的,都是0,所以ServiceManager服務並不需要查詢,可以直接使用。

理解Binder定向制導的關鍵是理解Binder的四棵紅黑樹,先看一下binder_proc結構體,在它內部有四棵紅黑樹,threads,nodes,refs_by_desc,refs_by_node,nodes就是Binder實體在核心中對應的資料結構,binder_node裡記錄程式相關的binder_proc,還有Binder實體自身的地址等資訊,nodes紅黑樹位於binder_proc,可以知道Binder實體其實是程式內可見,而不是執行緒內。

struct binder_proc {
    struct hlist_node proc_node;
    struct rb_root threads;
    struct rb_root nodes;
    struct rb_root refs_by_desc;
    struct rb_root refs_by_node;
    。。。
    struct list_head todo;
    wait_queue_head_t wait;
    。。。
};複製程式碼

現在假設存在一堆Client與Service,Client如何才能訪問Service呢?首先Service會通過addService將binder實體註冊到ServiceManager中去,Client如果想要使用Servcie,就需要通過getService向ServiceManager請求該服務。在Service通過addService向ServiceManager註冊的時候,ServiceManager會將服務相關的資訊儲存到自己程式的Service列表中去,同時在ServiceManager程式的binder_ref紅黑樹中為Service新增binder_ref節點,這樣ServiceManager就能獲取Service的Binder實體資訊。而當Client通過getService向ServiceManager請求該Service服務的時候,ServiceManager會在註冊的Service列表中查詢該服務,如果找到就將該服務返回給Client,在這個過程中,ServiceManager會在Client程式的binder_ref紅黑樹中新增binder_ref節點,可見本程式中的binder_ref紅黑樹節點都不是本程式自己建立的,要麼是Service程式將binder_ref插入到ServiceManager中去,要麼是ServiceManager程式將binder_ref插入到Client中去。之後,Client就能通過Handle控制程式碼獲取binder_ref,進而訪問Service服務。

聽說你Binder機制學的不錯,來解決下這幾個問題(一)
binder_ref新增邏輯

getService之後,便可以獲取binder_ref引用,進而獲取到binder_proc與binder_node資訊,之後Client便可有目的的將binder_transaction事務插入到binder_proc的待處理列表,並且,如果程式正在睡眠,就喚起程式,其實這裡到底是喚起程式還是執行緒也有講究,對於Client向Service傳送請求的狀況,一般都是喚醒binder_proc上睡眠的執行緒:

struct binder_ref {
    int debug_id;
    struct rb_node rb_node_desc;
    struct rb_node rb_node_node;
    struct hlist_node node_entry;
    struct binder_proc *proc;
    struct binder_node *node;
    uint32_t desc;
    int strong;
    int weak;
    struct binder_ref_death *death;
};複製程式碼

binder_proc為何會有兩棵binder_ref紅黑樹

binder_proc中存在兩棵binder_ref紅黑樹,其實兩棵紅黑樹中的節點是複用的,只是查詢方式不同,一個通過handle控制程式碼,一個通過node節點查詢。個人理解:refs_by_node紅黑樹主要是為了
binder驅動往使用者空間寫資料所使用的,而refs_by_desc是使用者空間向Binder驅動寫資料使用的,只是方向問題。比如在服務addService的時候,binder驅動會在在ServiceManager程式的binder_proc中查詢binder_ref結構體,如果沒有就會新建binder_ref結構體,再比如在Client端getService的時候,binder驅動會在Client程式中通過 binder_get_ref_for_node為Client建立binder_ref結構體,並分配控制程式碼,同時插入到refs_by_desc紅黑樹中,可見refs_by_node紅黑樹,主要是給binder驅動往使用者空間寫資料使用的。相對的refs_by_desc主要是為了使用者空間往binder驅動寫資料使用的,當使用者空間已經獲得Binder驅動為其建立的binder_ref引用控制程式碼後,就可以通過binder_get_ref從refs_by_desc找到響應binder_ref,進而找到目標binder_node。可見有兩棵紅黑樹主要是區分使用物件及資料流動方向,看下面的程式碼就能理解:

// 根據32位的uint32_t desc來查詢,可以看到,binder_get_ref不會新建binder_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) {
        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_get_ref並具備binder_ref的建立功能,相對應的看一下binder_get_ref_for_node,binder_get_ref_for_node紅黑樹主要通過binder_node進行查詢,如果找不到,就新建binder_ref,同時插入到兩棵紅黑樹中去

static struct binder_ref *binder_get_ref_for_node(struct binder_proc *proc,
                          struct binder_node *node)
{
    struct rb_node *n;
    struct rb_node **p = &proc->refs_by_node.rb_node;
    struct rb_node *parent = NULL;
    struct binder_ref *ref, *new_ref;
    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_node);
        if (node < ref->node)
            p = &(*p)->rb_left;
        else if (node > ref->node)
            p = &(*p)->rb_right;
        else
            return ref;
    }

    // binder_ref 可以在兩棵樹裡面,但是,兩棵樹的查詢方式不同,並且通過desc查詢,不具備新建功能
    new_ref = kzalloc(sizeof(*ref), GFP_KERNEL);
    if (new_ref == NULL)
        return NULL;
    binder_stats_created(BINDER_STAT_REF);
    new_ref->debug_id = ++binder_last_id;
    new_ref->proc = proc;
    new_ref->node = node;
    rb_link_node(&new_ref->rb_node_node, parent, p);
    // 插入到proc->refs_by_node紅黑樹中去
    rb_insert_color(&new_ref->rb_node_node, &proc->refs_by_node);
    // 是不是ServiceManager的
    new_ref->desc = (node == binder_context_mgr_node) ? 0 : 1;
    // 分配Handle控制程式碼,為了插入到refs_by_desc
    for (n = rb_first(&proc->refs_by_desc); n != NULL; n = rb_next(n)) {
        ref = rb_entry(n, struct binder_ref, rb_node_desc);
        if (ref->desc > new_ref->desc)
            break;
        new_ref->desc = ref->desc + 1;
    }
    // 找到目標位置
    p = &proc->refs_by_desc.rb_node;
    while (*p) {
        parent = *p;
        ref = rb_entry(parent, struct binder_ref, rb_node_desc);
        if (new_ref->desc < ref->desc)
            p = &(*p)->rb_left;
        else if (new_ref->desc > ref->desc)
            p = &(*p)->rb_right;
        else
            BUG();
    }
    rb_link_node(&new_ref->rb_node_desc, parent, p);
    // 插入到refs_by_desc紅黑樹中區
    rb_insert_color(&new_ref->rb_node_desc, &proc->refs_by_desc);

    if (node) {
        hlist_add_head(&new_ref->node_entry, &node->refs);
        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "node %d\n", proc->pid, new_ref->debug_id,
                 new_ref->desc, node->debug_id);
    } else {
        binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                 "binder: %d new ref %d desc %d for "
                 "dead node\n", proc->pid, new_ref->debug_id,
                  new_ref->desc);
    }
    return new_ref;
}複製程式碼

該函式呼叫在binder_transaction函式中,其實就是在binder驅動訪問target_proc的時候,這也也很容易理解,Handle控制程式碼對於跨程式沒有任何意義,程式A中的Handle,放到程式B中是無效的。

聽說你Binder機制學的不錯,來解決下這幾個問題(一)
兩棵binder_ref紅黑樹

Binder一次拷貝原理

Android選擇Binder作為主要程式通訊的方式同其效能高也有關係,Binder只需要一次拷貝就能將A程式使用者空間的資料為B程式所用。這裡主要涉及兩個點:

  • Binder的map函式,會將核心空間直接與使用者空間對應,使用者空間可以直接訪問核心空間的資料
  • A程式的資料會被直接拷貝到B程式的核心空間(一次拷貝)

      #define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
    
      ProcessState::ProcessState()
          : mDriverFD(open_driver())
          , mVMStart(MAP_FAILED)
          , mManagesContexts(false)
          , mBinderContextCheckFunc(NULL)
          , mBinderContextUserData(NULL)
          , mThreadPoolStarted(false)
          , mThreadPoolSeq(1){
         if (mDriverFD >= 0) {
          ....
              // mmap the binder, providing a chunk of virtual address space to receive transactions.
              mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
         ...
    
          }
      }複製程式碼

    mmap函式屬於系統呼叫,mmap會從當前程式中獲取使用者態可用的虛擬地址空間(vm_area_struct *vma),並在mmap_region中真正獲取vma,然後呼叫file->f_op->mmap(file, vma),進入驅動處理,之後就會在記憶體中分配一塊連續的虛擬地址空間,並預先分配好頁表、已使用的與未使用的標識、初始地址、與使用者空間的偏移等等,通過這一步之後,就能把Binder在核心空間的資料直接通過指標地址對映到使用者空間,供程式在使用者空間使用,這是一次拷貝的基礎,一次拷貝在核心中的標識如下:

      struct binder_proc {
      struct hlist_node proc_node;
      // 四棵比較重要的樹 
      struct rb_root threads;
      struct rb_root nodes;
      struct rb_root refs_by_desc;
      struct rb_root refs_by_node;
      int pid;
      struct vm_area_struct *vma; //虛擬地址空間,使用者控制元件傳過來
      struct mm_struct *vma_vm_mm;
      struct task_struct *tsk;
      struct files_struct *files;
      struct hlist_node deferred_work_node;
      int deferred_work;
      void *buffer; //初始地址
      ptrdiff_t user_buffer_offset; //這裡是偏移
    
      struct list_head buffers;//這個列表連線所有的記憶體塊,以地址的大小為順序,各記憶體塊首尾相連
      struct rb_root free_buffers;//連線所有的已建立對映的虛擬記憶體塊,以記憶體的大小為index組織在以該節點為根的紅黑樹下
      struct rb_root allocated_buffers;//連線所有已經分配的虛擬記憶體塊,以記憶體塊的開始地址為index組織在以該節點為根的紅黑樹下
    
      }複製程式碼

上面只是在APP啟動的時候開啟的地址對映,但並未涉及到資料的拷貝,下面看資料的拷貝操作。當資料從使用者空間拷貝到核心空間的時候,是直從當前程式的使用者空間接拷貝到目標程式的核心空間,這個過程是在請求端執行緒中處理的,操作物件是目標程式的核心空間。看如下程式碼:

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply){
                   ...
        在通過進行binder事物的傳遞時,如果一個binder事物(用struct binder_transaction結構體表示)需要使用到記憶體,
        就會呼叫binder_alloc_buf函式分配此次binder事物需要的記憶體空間。
        需要注意的是:這裡是從目標程式的binder記憶體空間分配所需的記憶體
        //從target程式的binder記憶體空間分配所需的記憶體大小,這也是一次拷貝,完成通訊的關鍵,直接拷貝到目標程式的核心空間
        //由於使用者空間跟核心空間僅僅存在一個偏移地址,所以也算拷貝到使用者空間
        t->buffer = binder_alloc_buf(target_proc, tr->data_size,
            tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
        t->buffer->allow_user_free = 0;
        t->buffer->debug_id = t->debug_id;
        //該binder_buffer對應的事務    
        t->buffer->transaction = t;
        //該事物對應的目標binder實體 ,因為目標程式中可能不僅僅有一個Binder實體
        t->buffer->target_node = target_node;
        trace_binder_transaction_alloc_buf(t->buffer);
        if (target_node)
            binder_inc_node(target_node, 1, 0, NULL);
        // 計算出存放flat_binder_object結構體偏移陣列的起始地址,4位元組對齊。
        offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
           // struct flat_binder_object是binder在程式之間傳輸的表示方式 //
           // 這裡就是完成binder通訊單邊時候在使用者程式同核心buffer之間的一次拷貝動作 //
          // 這裡的資料拷貝,其實是拷貝到目標程式中去,因為t本身就是在目標程式的核心空間中分配的,
        if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
            binder_user_error("binder: %d:%d got transaction with invalid "
                "data ptr\n", proc->pid, thread->pid);
            return_error = BR_FAILED_REPLY;
            goto err_copy_data_failed;
        }複製程式碼

可以看到binder_alloc_buf(target_proc, tr->data_size,tr->offsets_size, !reply && (t->flags & TF_ONE_WAY))函式在申請記憶體的時候,是從target_proc程式空間中去申請的,這樣在做資料拷貝的時候copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)),就會直接拷貝target_proc的核心空間,而由於Binder核心空間的資料能直接對映到使用者空間,這裡就不在需要拷貝到使用者空間。這就是一次拷貝的原理。核心空間的資料對映到使用者空間其實就是新增一個偏移地址,並且將資料的首地址、資料的大小都複製到一個使用者空間的Parcel結構體,具體可以參考Parcel.cpp的Parcel::ipcSetDataReference函式。

聽說你Binder機制學的不錯,來解決下這幾個問題(一)
Binder一次拷貝原理.jpg

Binder傳輸資料的大小限制

雖然APP開發時候,Binder對程式設計師幾乎不可見,但是作為Android的資料運輸系統,Binder的影響是全面性的,所以有時候如果不瞭解Binder的一些限制,在出現問題的時候往往是沒有任何頭緒,比如在Activity之間傳輸BitMap的時候,如果Bitmap過大,就會引起問題,比如崩潰等,這其實就跟Binder傳輸資料大小的限制有關係,在上面的一次拷貝中分析過,mmap函式會為Binder資料傳遞對映一塊連續的虛擬地址,這塊虛擬記憶體空間其實是有大小限制的,不同的程式可能還不一樣。

普通的由Zygote孵化而來的使用者程式,所對映的Binder記憶體大小是不到1M的,準確說是 110241024) - (4096 *2) :這個限制定義在ProcessState類中,如果傳輸說句超過這個大小,系統就會報錯,因為Binder本身就是為了程式間頻繁而靈活的通訊所設計的,並不是為了拷貝大資料而使用的:

#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))複製程式碼

而在核心中,其實也有個限制,是4M,不過由於APP中已經限制了不到1M,這裡的限制似乎也沒多大用途:

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    struct vm_struct *area;
    struct binder_proc *proc = filp->private_data;
    const char *failure_string;
    struct binder_buffer *buffer;
    //限制不能超過4M
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;
    。。。
    }複製程式碼

有個特殊的程式ServiceManager程式,它為自己申請的Binder核心空間是128K,這個同ServiceManager的用途是分不開的,ServcieManager主要面向系統Service,只是簡單的提供一些addServcie,getService的功能,不涉及多大的資料傳輸,因此不需要申請多大的記憶體:

int main(int argc, char **argv)
{
    struct binder_state *bs;
    void *svcmgr = BINDER_SERVICE_MANAGER;

        // 僅僅申請了128k
    bs = binder_open(128*1024);
 if (binder_become_context_manager(bs)) {
        ALOGE("cannot become context manager (%s)\n", strerror(errno));
        return -1;
    }

    svcmgr_handle = svcmgr;
    binder_loop(bs, svcmgr_handler);
    return 0;
}    複製程式碼

系統服務與bindService等啟動的服務的區別

服務可分為系統服務與普通服務,系統服務一般是在系統啟動的時候,由SystemServer程式建立並註冊到ServiceManager中的。而普通服務一般是通過ActivityManagerService啟動的服務,或者說通過四大元件中的Service元件啟動的服務。這兩種服務在實現跟使用上是有不同的,主要從以下幾個方面:

  • 服務的啟動方式
  • 服務的註冊與管理
  • 服務的請求使用方式

首先看一下服務的啟動上,系統服務一般都是SystemServer程式負責啟動,比如AMS,WMS,PKMS,電源管理等,這些服務本身其實實現了Binder介面,作為Binder實體註冊到ServiceManager中,被ServiceManager管理,而SystemServer程式裡面會啟動一些Binder執行緒,主要用於監聽Client的請求,並分發給響應的服務實體類,可以看出,這些系統服務是位於SystemServer程式中(有例外,比如Media服務)。在來看一下bindService型別的服務,這類服務一般是通過Activity的startService或者其他context的startService啟動的,這裡的Service元件只是個封裝,主要的是裡面Binder服務實體類,這個啟動過程不是ServcieManager管理的,而是通過ActivityManagerService進行管理的,同Activity管理類似。

再來看一下服務的註冊與管理:系統服務一般都是通過ServiceManager的addService進行註冊的,這些服務一般都是需要擁有特定的許可權才能註冊到ServiceManager,而bindService啟動的服務可以算是註冊到ActivityManagerService,只不過ActivityManagerService管理服務的方式同ServiceManager不一樣,而是採用了Activity的管理模型,詳細的可以自行分析

最後看一下使用方式,使用系統服務一般都是通過ServiceManager的getService得到服務的控制程式碼,這個過程其實就是去ServiceManager中查詢註冊系統服務。而bindService啟動的服務,主要是去ActivityManagerService中去查詢相應的Service元件,最終會將Service內部Binder的控制程式碼傳給Client。

聽說你Binder機制學的不錯,來解決下這幾個問題(一)
系統服務與bindService啟動服務的區別.jpg

Binder執行緒、Binder主執行緒、Client請求執行緒的概念與區別

Binder執行緒是執行Binder服務的載體,只對於服務端才有意義,對請求端來說,是不需要考慮Binder執行緒的,但Android系統的處理機制其實大部分是互為C/S的。比如APP與AMS進行互動的時候,都互為對方的C與S,這裡先不討論這個問題,先看Binder執行緒的概念。

Binder執行緒就是執行Binder實體業務的執行緒,一個普通執行緒如何才能成為Binder執行緒呢?很簡單,只要開啟一個監聽Binder字元裝置的Loop執行緒即可,在Android中有很多種方法,不過歸根到底都是監聽Binder,換成程式碼就是通過ioctl來進行監聽。

拿ServerManager程式來說,其主線就是Binder執行緒,其做法是通過binder_loop實現不死執行緒:

void binder_loop(struct binder_state *bs, binder_handler func)
{
   ...
    for (;;) {
    <!--關鍵點1-->
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
     <!--關鍵點2-->
        res = binder_parse(bs, 0, readbuf, bwr.read_consumed, func);
        。。
    }
}複製程式碼

上面的關鍵程式碼1就是阻塞監聽客戶端請求,2 就是處理請求,並且這是一個死迴圈,不退出。再來看SystemServer程式中的執行緒,在Android4.3(6.0以後打程式碼就不一樣了)中SystemSever主執行緒便是Binder執行緒,同時一個Binder主執行緒,Binder執行緒與Binder主執行緒的區別是:執行緒是否可以終止Loop,不過目前啟動的Binder執行緒都是無法退出的,其實可以全部看做是Binder主執行緒,其實現原理是,在SystemServer主執行緒執行到最後的時候,Loop監聽Binder裝置,變身死迴圈執行緒,關鍵程式碼如下:

extern "C" status_t system_init()
{
    ...
    ALOGI("System server: entering thread pool.\n");
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
    ALOGI("System server: exiting thread pool.\n");
    return NO_ERROR;
}複製程式碼

ProcessState::self()->startThreadPool()是新建一個Binder主執行緒,而PCThreadState::self()->joinThreadPool()是將當前執行緒變成Binder主執行緒。其實startThreadPool最終也會呼叫joinThreadPool,看下其關鍵函式:

void IPCThreadState::joinThreadPool(bool isMain)
{
     ...
    status_t result;
    do {
        int32_t cmd;
          ...關鍵點1 
        result = talkWithDriver();
        if (result >= NO_ERROR) {
           ...關鍵點2 
            result = executeCommand(cmd);
        }
        // 非主執行緒的可以退出
        if(result == TIMED_OUT && !isMain) {
            break;
        }
        // 死迴圈,不完結,呼叫了這個,就好比是開啟了Binder監聽迴圈,
    } while (result != -ECONNREFUSED && result != -EBADF);
 }

status_t IPCThreadState::talkWithDriver(bool doReceive)
{  
    do {
        ...關鍵點3 
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
   }   複製程式碼

先看關鍵點1 talkWithDriver,其實質還是去掉用ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)去不斷的監聽Binder字元裝置,獲取到Client傳輸的資料後,再通過executeCommand去執行相應的請求,joinThreadPool是普通執行緒化身Binder執行緒最常見的方式。不信,就再看一個MediaService,看一下main_mediaserver的main函式:

int main(int argc, char** argv)
{
   。。。
        sp<ProcessState> proc(ProcessState::self());
        sp<IServiceManager> sm = defaultServiceManager();
        ALOGI("ServiceManager: %p", sm.get());
        AudioFlinger::instantiate();
        MediaPlayerService::instantiate();
        CameraService::instantiate();
        AudioPolicyService::instantiate();
        registerExtensions();
        ProcessState::self()->startThreadPool();
        IPCThreadState::self()->joinThreadPool();
    }複製程式碼

其實還是通過joinThreadPool變身Binder執行緒,至於是不是主執行緒,看一下下面的函式:

void IPCThreadState::joinThreadPool(bool isMain)

void ProcessState::spawnPooledThread(bool isMain)
{
    if (mThreadPoolStarted) {
        String8 name = makeBinderThreadName();
        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
        sp<Thread> t = new PoolThread(isMain);
        t->run(name.string());
    }
}複製程式碼

其實關鍵就是就是傳遞給joinThreadPool函式的isMain是否是true,不過是否是Binder主執行緒並沒有什麼用,因為原始碼中並沒有為這兩者的不同處理留入口,感興趣可以去檢視一下binder中的TIMED_OUT。

最後來看一下普通Client的binder請求執行緒,比如我們APP的主執行緒,在startActivity請求AMS的時候,APP的主執行緒成其實就是Binder請求執行緒,在進行Binder通訊的過程中,Client的Binder請求執行緒會一直阻塞,知道Service處理完畢返回處理結果。

Binder請求的同步與非同步

很多人都會說,Binder是對Client端同步,而對Service端非同步,其實並不完全正確,在單次Binder資料傳遞的過程中,其實都是同步的。只不過,Client在請求Server端服務的過程中,是需要返回結果的,即使是你看不到返回資料,其實還是會有個成功與失敗的處理結果返回給Client,這就是所說的Client端是同步的。至於說服務端是非同步的,可以這麼理解:在服務端在被喚醒後,就去處理請求,處理結束後,服務端就將結果返回給正在等待的Client執行緒,將結果寫入到Client的核心空間後,服務端就會直接返回了,不會再等待Client端的確認,這就是所說的服務端是非同步的,可以從原始碼來看一下:

  • Client端同步阻塞請求

    status_t IPCThreadState::transact(int32_t handle,

                                    uint32_t code, const Parcel& data,
                                    Parcel* reply, uint32_t flags) 複製程式碼

    {

          if (reply) {
              err = waitForResponse(reply);
          } ...複製程式碼

Client在請求服務的時候 Parcel* reply基本都是非空的(還沒見過空用在什麼位置),非空就會執行waitForResponse(reply),如果看過幾篇Binder分析文章的人應該都會知道,在A端向B寫完資料之後,A會返回給自己一個BR_TRANSACTION_COMPLETE命令,告知自己資料已經成功寫入到B的Binder核心空間中去了,如果是需要回復,在處理完BR_TRANSACTION_COMPLETE命令後會繼續阻塞等待結果的返回:

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult){
    ...
    while (1) {
    if ((err=talkWithDriver()) < NO_ERROR) break;
     cmd = mIn.readInt32();
    switch (cmd) {
       <!--關鍵點1 -->
      case BR_TRANSACTION_COMPLETE:
            if (!reply && !acquireResult) goto finish;
            break;
     <!--關鍵點2 -->
        case BR_REPLY:
            {
                binder_transaction_data tr;
                  // free buffer,先設定資料,直接
                if (reply) {
                    if ((tr.flags & TF_STATUS_CODE) == 0) {
                        // 牽扯到資料利用,與記憶體釋放
                        reply->ipcSetDataReference(...)
            }
            goto finish;
    }
 finish:
 ...
return err;
}複製程式碼

關鍵點1就是處理BR_TRANSACTION_COMPLETE,如果需要等待reply,還要通過talkWithDriver等待結果返回,最後執行關鍵點2,處理返回資料。對於服務端來說,區別就在於關鍵點1 ,來看一下服務端Binder執行緒的程式碼,拿常用的joinThreadPool來看,在talkWithDriver後,會執行executeCommand函式,

void IPCThreadState::joinThreadPool(bool isMain)
{
     ...
    status_t result;
    do {
        int32_t cmd;
          ...關鍵點1 
        result = talkWithDriver();
        if (result >= NO_ERROR) {
           ...關鍵點2 
            result = executeCommand(cmd);
        }
        // 非主執行緒的可以退出
        if(result == TIMED_OUT && !isMain) {
            break;
        }
        // 死迴圈,不完結,呼叫了這個,就好比是開啟了Binder監聽迴圈,
    } while (result != -ECONNREFUSED && result != -EBADF);
 }複製程式碼

executeCommand會進一步呼叫sendReply函式,看一下這裡的特點waitForResponse(NULL, NULL),這裡傳遞的都是null,在上面的關鍵點1的地方我們知道,這裡不需要等待Client返回,因此會直接 goto finish,這就是所說的Client同步,而服務端非同步的邏輯。

// BC_REPLY
status_t IPCThreadState::sendReply(const Parcel& reply, uint32_t flags)
{
    // flag 0
    status_t err;
    status_t statusBuffer;
    err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &statusBuffer);
    if (err < NO_ERROR) return err;
    return waitForResponse(NULL, NULL);
}複製程式碼
  case BR_TRANSACTION_COMPLETE:
        if (!reply && !acquireResult) goto finish;
        break;複製程式碼

請求同步最好的例子就是在Android6.0之前,國產ROM許可權的申請都是同步的,在申請許可權的時候,APP申請許可權的執行緒會阻塞,就算是UI執行緒也會阻塞,ROM為了防止ANR,都會為許可權申請設定一個倒數計時,不操作,就給個預設操作,有興趣可以自己分析。

Android APP程式天生支援Binder通訊的原理是什麼

Android APP程式都是由Zygote程式孵化出來的。常見場景:點選桌面icon啟動APP,或者startActivity啟動一個新程式裡面的Activity,最終都會由AMS去呼叫Process.start()方法去向Zygote程式傳送請求,讓Zygote去fork一個新程式,Zygote收到請求後會呼叫Zygote.forkAndSpecialize()來fork出新程式,之後會通過RuntimeInit.nativeZygoteInit來初始化Andriod APP執行需要的一些環境,而binder執行緒就是在這個時候新建啟動的,看下面的原始碼(Android 4.3):

這裡不分析Zygote,只是給出其大概執行機制,Zygote在啟動後,就會通過runSelectLoop不斷的監聽socket,等待請求來fork程式,如下:

private static void runSelectLoop() throws MethodAndArgsCaller {
    ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
    ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
    FileDescriptor[] fdArray = new FileDescriptor[4];
    ...     
   int loopCount = GC_LOOP_COUNT;
    while (true) {
        int index;
         ...
            boolean done;
            done = peers.get(index).runOnce();
            ...
        }}}複製程式碼

每次fork請求到來都會呼叫ZygoteConnection的runOnce()來處理請求,

boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {

    String args[];
    Arguments parsedArgs = null;
    FileDescriptor[] descriptors;    
        。。。

    try {
       ...關鍵點1 
        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                parsedArgs.niceName);
    } 
    try {
        if (pid == 0) {
            // in child
         ...關鍵點2
            handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
        。。。
    }複製程式碼

runOnce()有兩個關鍵點,關鍵點1 Zygote.forkAndSpecialize就是通過fork系統呼叫來新建程式,關鍵點2 handleChildProc就是對新建的APP程式進行一些初始化工作,為Android Java程式建立一些必須的場景。Zygote.forkAndSpecialize沒什麼可看的,就是Linux中的fork程式,這裡主要看一下handleChildProc

private void handleChildProc(Arguments parsedArgs,
        FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
        throws ZygoteInit.MethodAndArgsCaller {

    //從Process.start啟動的parsedArgs.runtimeInit一般都是true             if (parsedArgs.runtimeInit) {
        if (parsedArgs.invokeWith != null) {
            WrapperInit.execApplication(parsedArgs.invokeWith,
                    parsedArgs.niceName, parsedArgs.targetSdkVersion,
                    pipeFd, parsedArgs.remainingArgs);  
        } else {
            // Android應用啟動都走該分支
       RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,
                    parsedArgs.remainingArgs); 
       }  
}複製程式碼

接著看 RuntimeInit.zygoteInit函式

public static final void zygoteInit(int targetSdkVersion, String[] argv)
        throws ZygoteInit.MethodAndArgsCaller {

    redirectLogStreams();
    commonInit();
    <!--關鍵點1-->
    nativeZygoteInit();
     <!--關鍵點2-->
    applicationInit(targetSdkVersion, argv);
}複製程式碼

先看關鍵點1,nativeZygoteInit屬於Native方法,該方法位於AndroidRuntime.cpp中,其實就是呼叫呼叫到app_main.cpp中的onZygoteInit

static void com_android_internal_os_RuntimeInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
    gCurRuntime->onZygoteInit();
}複製程式碼

關鍵就是onZygoteInit

    virtual void onZygoteInit()
    {
        sp proc = ProcessState::self();
        //啟動新binder執行緒loop
        proc->startThreadPool();
    }複製程式碼

首先,ProcessState::self()函式會呼叫open()開啟/dev/binder裝置,這個時候Client就能通過Binder進行遠端通訊;其次,proc->startThreadPool()負責新建一個binder執行緒,監聽Binder裝置,這樣程式就具備了作為Binder服務端的資格。每個APP的程式都會通過onZygoteInit開啟Binder,既能作為Client,也能作為Server,這就是Android程式天然支援Binder通訊的原因。

聽說你Binder機制學的不錯,來解決下這幾個問題(一)
Android APP程式天然支援Binder通訊.png

Android APP有多少Binder執行緒,是固定的麼?

通過上一個問題我們知道了Android APP執行緒為什麼天然支援Binder通訊,並且可以作為Binder的Service端,同時也對Binder執行緒有了一個瞭解,那麼在一個Android APP的程式裡面究竟有多少個Binder執行緒呢?是固定的嗎。在分析上一個問題的時候,我們知道Android APP程式在Zygote fork之初就為它新建了一個Binder主執行緒,使得APP端也可以作為Binder的服務端,這個時候Binder執行緒的數量就只有一個,假設我們的APP自身實現了很多的Binder服務,一個執行緒夠用的嗎?這裡不妨想想一下SystemServer程式,SystemServer擁有很多系統服務,一個執行緒應該是不夠用的,如果看過SystemServer程式碼可能會發現,對於Android4.3的原始碼,其實一開始為該服務開啟了兩個Binder執行緒。還有個分析Binder常用的服務,media服務,也是在一開始的時候開啟了兩個執行緒。

先看下SystemServer的開始載入的執行緒:通過 ProcessState::self()->startThreadPool()新加了一個Binder執行緒,然後通過IPCThreadState::self()->joinThreadPool();將當前執行緒變成Binder執行緒,注意這裡是針對Android4.3的原始碼,android6.0的這裡略有不同。

extern "C" status_t system_init()
{
    ...
    ALOGI("System server: entering thread pool.\n");
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
    ALOGI("System server: exiting thread pool.\n");
    return NO_ERROR;
}複製程式碼

再看下Media服務,同SystemServer類似,也是開啟了兩個Binder執行緒:

int main(int argc, char** argv)
{      ...
        ProcessState::self()->startThreadPool();
        IPCThreadState::self()->joinThreadPool();
 }    複製程式碼

可以看出Android APP上層應用的程式一般是開啟一個Binder執行緒,而對於SystemServer或者media服務等使用頻率高,服務複雜的程式,一般都是開啟兩個或者更多。來看第二個問題,Binder執行緒的數目是固定的嗎?答案是否定的,驅動會根據目標程式中是否存在足夠多的Binder執行緒來告訴程式是不是要新建Binder執行緒,詳細邏輯,首先看一下新建Binder執行緒的入口:

status_t IPCThreadState::executeCommand(int32_t cmd)
{
    BBinder* obj;
    RefBase::weakref_type* refs;
    status_t result = NO_ERROR;
    switch (cmd) {
    ...
    // 可以根據核心返回資料建立新的binder執行緒
    case BR_SPAWN_LOOPER:
        mProcess->spawnPooledThread(false);
        break;
}複製程式碼

executeCommand一定是從Bindr驅動返回的BR命令,這裡是BR_SPAWN_LOOPER,什麼時候,Binder驅動會向程式傳送BR_SPAWN_LOOPER呢?全域性搜尋之後,發現只有一個地方binder_thread_read,如果直觀的想一下,什麼時候需要新建Binder執行緒呢?很簡單,不夠用的時候,注意上面使用的是spawnPooledThread(false),也就是說這裡啟動的都是普通Binder執行緒。為了瞭解啟動時機,先看一些binder_proc內部判定引數的意義:

struct binder_proc {
    ...
    int max_threads;                 // 程式所能啟動的最大非主Binder執行緒數目
    int requested_threads;            // 請求啟動的非主執行緒數
    int requested_threads_started;//已經啟動的非主執行緒數
    int ready_threads;                // 當前可用的Binder執行緒數
    ...
};複製程式碼

再來看binder_thread_read函式中是麼時候會去請求新建Binder執行緒,以Android APP程式為例子,通過前面的分析知道APP程式天然支援Binder通訊,因為它有一個Binder主執行緒,啟動之後就會阻塞等待Client請求,這裡會更新proc->ready_threads,第一次阻塞等待的時候proc->ready_threads=1,之後睡眠。

binder_thread_read(){
  ...
 retry:
    //當前執行緒todo佇列為空且transaction棧為空,則代表該執行緒是空閒的 ,看看是不是自己被複用了
    wait_for_proc_work = thread->transaction_stack == NULL &&
        list_empty(&thread->todo);
 ...//可用執行緒個數+1
    if (wait_for_proc_work)
        proc->ready_threads++; 
    binder_unlock(__func__);
    if (wait_for_proc_work) {
        ...
            //當程式todo佇列沒有資料,則進入休眠等待狀態
            ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread));
    } else {
        if (non_block) {
            ...
        } else
            //當執行緒todo佇列沒有資料,則進入休眠等待狀態
            ret = wait_event_freezable(thread->wait, binder_has_thread_work(thread));
    }    
    binder_lock(__func__);
    //被喚醒可用執行緒個數-1
    if (wait_for_proc_work)
        proc->ready_threads--; 
    thread->looper &= ~BINDER_LOOPER_STATE_WAITING;
    ...
    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        struct binder_transaction *t = NULL;

        //先考慮從執行緒todo佇列獲取事務資料
        if (!list_empty(&thread->todo)) {
            w = list_first_entry(&thread->todo, struct binder_work, entry);
        //執行緒todo佇列沒有資料, 則從程式todo對獲取事務資料
        } else if (!list_empty(&proc->todo) && wait_for_proc_work) {
            w = list_first_entry(&proc->todo, struct binder_work, entry);
        } else {
        }
         ..
        if (t->buffer->target_node) {
            cmd = BR_TRANSACTION;  //設定命令為BR_TRANSACTION
        } else {
            cmd = BR_REPLY; //設定命令為BR_REPLY
        }
        .. 
done:
    *consumed = ptr - buffer;
    //建立執行緒的條件
    if (proc->requested_threads + proc->ready_threads == 0 &&
        proc->requested_threads_started < proc->max_threads &&
        (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
         BINDER_LOOPER_STATE_ENTERED))) {
         //需要新建的數目執行緒數+1
        proc->requested_threads++;
        // 生成BR_SPAWN_LOOPER命令,用於建立新的執行緒
        put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);
    }
    return 0;
}複製程式碼

被Client喚醒後proc->ready_threads會-1,之後變成0,這樣在執行到done的時候,就會發現proc->requested_threads + proc->ready_threads == 0,這是新建Binder執行緒的一個必須條件,再看下其他幾個條件

if (proc->requested_threads + proc->ready_threads == 0 &&
            proc->requested_threads_started < proc->max_threads &&
            (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
             BINDER_LOOPER_STATE_ENTERED)))  複製程式碼
  • proc->requested_threads + proc->ready_threads == 0 :如果目前還沒申請新建Binder執行緒,並且proc->ready_threads空閒Binder執行緒也是0,就需要新建一個Binder執行緒,其實就是為了保證有至少有一個空閒的執行緒
  • proc->requested_threads_started < proc->max_threads:目前啟動的普通Binder執行緒數requested_threads_started還沒達到上限(預設APP程式是15)
  • thread->looper & (BINDER_LOOPER_STATE_REGISTERED | BINDER_LOOPER_STATE_ENTERED) 當先執行緒是Binder執行緒,這個是一定滿足的,不知道為什麼列出來

proc->max_threads是多少呢?不同的程式其實設定的是不一樣的,看普通的APP程式,在ProcessState::self()新建ProcessState單利物件的時候會呼叫ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);設定上限,可以看到預設設定的上限是15。

static int open_driver()
{
    int fd = open("/dev/binder", O_RDWR);
    ...
    size_t maxThreads = 15;
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
    ...
}複製程式碼

如果滿足新建的條件,就會將proc->requested_threads加1,並在驅動執行完畢後,利用put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);通知服務端在使用者空間發起新建Binder執行緒的操作,新建的是普通Binder執行緒,最終再進入binder_thread_write的BC_REGISTER_LOOPER:

int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread,
            void __user *buffer, int size, signed long *consumed)
  {
        ...
        case BC_REGISTER_LOOPER:
              ...
                // requested_threads -- 
                proc->requested_threads--;
                proc->requested_threads_started++;
            }
}複製程式碼

這裡會將proc->requested_threads復原,其實就是-1,並且啟動的Binder執行緒數+1。

個人理解,之所以採用動態新建Binder執行緒的意義有兩點,第一:如果沒有Client請求服務,就保持執行緒數不變,減少資源浪費,需要的時候再分配新執行緒。第二:有請求的情況下,保證至少有一個空閒執行緒是給Client端,以提高Server端響應速度。

聽說你 Binder 機制學的不錯,來解決下這幾個問題(二)
聽說你 Binder 機制學的不錯,來解決下這幾個問題(三)
僅供參考,歡迎指正

相關文章