天天看點

Android Bander設計與實作 - 設計篇(轉)

轉載自:https://blog.csdn.net/universus/article/details/6211589

關鍵詞

Binder Android IPC Linux 核心 驅動

摘要

Binder是Android系統程序間通信(IPC)方式之一。Linux已經擁有管道,system V IPC,socket等IPC手段,卻還要倚賴Binder來實作程序間通信,說明Binder具有無可比拟的優勢。深入了解Binder并将之與傳統IPC做對比有助于我們深入領會程序間通信的實作和性能優化。本文将對Binder的設計細節做一個全面的闡述,首先通過介紹Binder通信模型和Binder通信協定了解Binder的設計需求;然後分别闡述Binder在系統不同部分的表述方式和起的作用;最後還會解釋Binder在資料接收端的設計考慮,包括線程池管理,記憶體映射和等待隊列管理等。通過本文對Binder的詳細介紹以及與其它IPC通信方式的對比,讀者将對Binder的優勢和使用Binder作為Android主要IPC方式的原因有深入了解。

1 引言

基于Client-Server的通信方式廣泛應用于從網際網路和資料庫通路到嵌入式手持裝置内部通信等各個領域。智能手機平台特别是Android系統中,為了向應用開發者提供豐富多樣的功能,這種通信方式更是無處不在,諸如媒體播放,視音頻頻捕獲,到各種讓手機更智能的傳感器(加速度,方位,溫度,光亮度等)都由不同的Server負責管理,應用程式隻需做為Client與這些Server建立連接配接便可以使用這些服務,花很少的時間和精力就能開發出令人眩目的功能。Client-Server方式的廣泛采用對程序間通信(IPC)機制是一個挑戰。目前linux支援的IPC包括傳統的管道,System V IPC,即消息隊列/共享記憶體/信号量,以及socket中隻有socket支援Client-Server的通信方式。當然也可以在這些底層機制上架設一套協定來實作Client-Server通信,但這樣增加了系統的複雜性,在手機這種條件複雜,資源稀缺的環境下可靠性也難以保證。

另一方面是傳輸性能。socket作為一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的程序間通信和本機上程序間的低速通信。消息隊列和管道采用存儲-轉發方式,即資料先從發送方緩存區拷貝到核心開辟的緩存區中,然後再從核心緩存區拷貝到接收方緩存區,至少有兩次拷貝過程。共享記憶體雖然無需拷貝,但控制複雜,難以使用。

表 1 各種IPC方式資料拷貝次數

IPC 資料拷貝次數
共享記憶體
Binder 1
Socket/管道/消息隊列 2

還有一點是出于安全性考慮。Android作為一個開放式,擁有衆多開發者的的平台,應用程式的來源廣泛,確定智能終端的安全是非常重要的。終端使用者不希望從網上下載下傳的程式在不知情的情況下偷窺隐私資料,連接配接無線網絡,長期操作底層裝置導緻電池很快耗盡等等。傳統IPC沒有任何安全措施,完全依賴上層協定來確定。首先傳統IPC的接收方無法獲得對方程序可靠的UID/PID(使用者ID/程序ID),進而無法鑒别對方身份。Android為每個安裝好的應用程式配置設定了自己的UID,故程序的UID是鑒别程序身份的重要标志。使用傳統IPC隻能由使用者在資料包裡填入UID/PID,但這樣不可靠,容易被惡意程式利用。可靠的身份标記隻有由IPC機制本身在核心中添加。其次傳統IPC通路接入點是開放的,無法建立私有通道。比如命名管道的名稱,system V的鍵值,socket的ip位址或檔案名都是開放的,隻要知道這些接入點的程式都可以和對端建立連接配接,不管怎樣都無法阻止惡意程式通過猜測接收方位址獲得連接配接。

基于以上原因,Android需要建立一套新的IPC機制來滿足系統對通信方式,傳輸性能和安全性的要求,這就是Binder。Binder基于Client-Server通信模式,傳輸過程隻需一次拷貝,為發送發添加UID/PID身份,既支援實名Binder也支援匿名Binder,安全性高。

2 面向對象的 Binder IPC

Binder使用Client-Server通信方式:一個程序作為Server提供諸如視訊/音頻解碼,視訊捕獲,位址本查詢,網絡連接配接等服務;多個程序作為Client向Server發起服務請求,獲得所需要的服務。要想實作Client-Server通信據必須實作以下兩點:一是server必須有确定的通路接入點或者說位址來接受Client的請求,并且Client可以通過某種途徑獲知Server的位址;二是制定Command-Reply協定來傳輸資料。例如在網絡通信中Server的通路接入點就是Server主機的IP位址+端口号,傳輸協定為TCP協定。對Binder而言,Binder可以看成Server提供的實作某個特定服務的通路接入點, Client通過這個‘位址’向Server發送請求來使用該服務;對Client而言,Binder可以看成是通向Server的管道入口,要想和某個Server通信首先必須建立這個管道并獲得管道入口。

與其它IPC不同,Binder使用了面向對象的思想來描述作為通路接入點的Binder及其在Client中的入口:Binder是一個實體位于Server中的對象,該對象提供了一套方法用以實作對服務的請求,就象類的成員函數。遍布于client中的入口可以看成指向這個binder對象的‘指針’,一旦獲得了這個‘指針’就可以調用該對象的方法通路server。在Client看來,通過Binder‘指針’調用其提供的方法和通過指針調用其它任何本地對象的方法并無差別,盡管前者的實體位于遠端Server中,而後者實體位于本地記憶體中。‘指針’是C++的術語,而更通常的說法是引用,即Client通過Binder的引用通路Server。而軟體領域另一個術語‘句柄’也可以用來表述Binder在Client中的存在方式。從通信的角度看,Client中的Binder也可以看作是Server Binder的‘代理’,在本地代表遠端Server為Client提供服務。本文中會使用‘引用’或‘句柄’這個兩廣泛使用的術語。

面向對象思想的引入将程序間通信轉化為通過對某個Binder對象的引用調用該對象的方法,而其獨特之處在于Binder對象是一個可以跨程序引用的對象,它的實體位于一個程序中,而它的引用卻遍布于系統的各個程序之中。最誘人的是,這個引用和java裡引用一樣既可以是強類型,也可以是弱類型,而且可以從一個程序傳給其它程序,讓大家都能通路同一Server,就象将一個對象或引用指派給另一個引用一樣。Binder模糊了程序邊界,淡化了程序間通信過程,整個系統仿佛運作于同一個面向對象的程式之中。形形色色的Binder對象以及星羅棋布的引用仿佛粘接各個應用程式的膠水,這也是Binder在英文裡的原意。

當然面向對象隻是針對應用程式而言,對于Binder驅動和核心其它子產品一樣使用C語言實作,沒有類和對象的概念。Binder驅動為面向對象的程序間通信提供底層支援。

3 Binder 通信模型

Binder架構定義了四個角色:Server,Client,ServiceManager(以後簡稱SMgr)以及Binder驅動。其中Server,Client,SMgr運作于使用者空間,驅動運作于核心空間。這四個角色的關系和網際網路類似:Server是伺服器,Client是客戶終端,SMgr是域名伺服器(DNS),驅動是路由器。

3.1 Binder 驅動

和路由器一樣,Binder驅動雖然默默無聞,卻是通信的核心。盡管名叫‘驅動’,實際上和硬體裝置沒有任何關系,隻是實作方式和裝置驅動程式是一樣的:它工作于核心态,提供open(),mmap(),poll(),ioctl()等标準檔案操作,以字元驅動裝置中的misc裝置注冊在裝置目錄/dev下,使用者通過/dev/binder通路該它。驅動負責程序之間Binder通信的建立,Binder在程序之間的傳遞,Binder引用計數管理,資料包在程序之間的傳遞和互動等一系列底層支援。驅動和應用程式之間定義了一套接口協定,主要功能由ioctl()接口實作,不提供read(),write()接口,因為ioctl()靈活友善,且能夠一次調用實作先寫後讀以滿足同步互動,而不必分别調用write()和read()。Binder驅動的代碼位于linux目錄的drivers/misc/binder.c中。

3.2 ServiceManager 與實名Binder

和DNS類似,SMgr的作用是将字元形式的Binder名字轉化成Client中對該Binder的引用,使得Client能夠通過Binder名字獲得對Server中Binder實體的引用。注冊了名字的Binder叫實名Binder,就象每個網站除了有IP位址外還有自己的網址。Server建立了Binder實體,為其取一個字元形式,可讀易記的名字,将這個Binder連同名字以資料包的形式通過Binder驅動發送給SMgr,通知SMgr注冊一個名叫張三的Binder,它位于某個Server中。驅動為這個穿過程序邊界的Binder建立位于核心中的實體節點以及SMgr對實體的引用,将名字及建立的引用打包傳遞給SMgr。SMgr收資料包後,從中取出名字和引用填入一張查找表中。

細心的讀者可能會發現其中的蹊跷:SMgr是一個程序,Server是另一個程序,Server向SMgr注冊Binder必然會涉及程序間通信。目前實作的是程序間通信卻又要用到程序間通信,這就好象蛋可以孵出雞前提卻是要找隻雞來孵蛋。Binder的實作比較巧妙:預先創造一隻雞來孵蛋:SMgr和其它程序同樣采用Binder通信,SMgr是Server端,有自己的Binder對象(實體),其它程序都是Client,需要通過這個Binder的引用來實作Binder的注冊,查詢和擷取。SMgr提供的Binder比較特殊,它沒有名字也不需要注冊,當一個程序使用BINDER_SET_CONTEXT_MGR指令将自己注冊成SMgr時Binder驅動會自動為它建立Binder實體(這就是那隻預先造好的雞)。其次這個Binder的引用在所有Client中都固定為0而無須通過其它手段獲得。也就是說,一個Server若要向SMgr注冊自己Binder就必需通過0這個引用号和SMgr的Binder通信。類比網絡通信,0号引用就好比域名伺服器的位址,你必須預先手工或動态配置好。要注意這裡說的Client是相對SMgr而言的,一個應用程式可能是個提供服務的Server,但對SMgr來說它仍然是個Client。

3.3 Client 獲得實名Binder的引用

Server向SMgr注冊了Binder實體及其名字後,Client就可以通過名字獲得該Binder的引用了。Client也利用保留的0号引用向SMgr請求通路某個Binder:我申請獲得名字叫張三的Binder的引用。SMgr收到這個連接配接請求,從請求資料包裡獲得Binder的名字,在查找表裡找到該名字對應的條目,從條目中取出Binder的引用,将該引用作為回複發送給發起請求的Client。從面向對象的角度,這個Binder對象現在有了兩個引用:一個位于SMgr中,一個位于發起請求的Client中。如果接下來有更多的Client請求該Binder,系統中就會有更多的引用指向該Binder,就象java裡一個對象存在多個引用一樣。而且類似的這些指向Binder的引用是強類型,進而確定隻要有引用Binder實體就不會被釋放掉。通過以上過程可以看出,SMgr象個火車票代售點,收集了所有火車的車票,可以通過它購買到乘坐各趟火車的票-得到某個Binder的引用。

3.4 匿名 Binder

并不是所有Binder都需要注冊給SMgr廣而告之的。Server端可以通過已經建立的Binder連接配接将建立的Binder實體傳給Client,當然這條已經建立的Binder連接配接必須是通過實名Binder實作。由于這個Binder沒有向SMgr注冊名字,是以是個匿名Binder。Client将會收到這個匿名Binder的引用,通過這個引用向位于Server中的實體發送請求。匿名Binder為通信雙方建立一條私密通道,隻要Server沒有把匿名Binder發給别的程序,别的程序就無法通過窮舉或猜測等任何方式獲得該Binder的引用,向該Binder發送請求。

下圖展示了參與Binder通信的所有角色,将在以後章節中一一提到。

Android Bander設計與實作 - 設計篇(轉)

圖 1 Binder通信示例

4 Binder 協定

Binder協定基本格式是(指令+資料),使用ioctl(fd, cmd, arg)函數實作互動。指令由參數cmd承載,資料由參數arg承載,随cmd不同而不同。下表列舉了所有指令及其所對應的資料:

表 2 Binder通信指令字

指令 含義 arg
BINDER_WRITE_READ 該指令向Binder寫入或讀取資料。參數分為兩段:寫部分和讀部分。如果write_size不為0就先将write_buffer裡的資料寫入Binder;如果read_size不為0再從Binder中讀取資料存入read_buffer中。write_consumed和read_consumed表示操作完成時Binder驅動實際寫入或讀出的資料個數。

struct binder_write_read {

signed long write_size;

signed long write_consumed;

unsigned long write_buffer;

signed long read_size;

signed long read_consumed;

unsigned long read_buffer;

};

BINDER_SET_MAX_THREADS 該指令告知Binder驅動接收方(通常是Server端)線程池中最大的線程數。由于Client是并發向Server端發送請求的,Server端必須開辟線程池為這些并發請求提供服務。告知驅動線程池的最大值是為了讓驅動發現線程數達到該值時不要再指令接收端啟動新的線程。 int max_threads;
BINDER_SET_CONTEXT_MGR 将目前程序注冊為SMgr。系統中同時隻能存在一個SMgr。隻要目前的SMgr沒有調用close()關閉Binder驅動就不能有别的程序可以成為SMgr。 ---
BINDER_THREAD_EXIT 通知Binder驅動目前線程退出了。Binder會為所有參與Binder通信的線程(包括Server線程池中的線程和Client送出請求的線程)建立相應的資料結構。這些線程在退出時必須通知驅動釋放相應的資料結構。 ---
BINDER_VERSION 獲得Binder驅動的版本号。 ---

這其中最常用的指令是BINDER_WRITE_READ。該指令的參數包括兩部分資料:一部分是向Binder寫入的資料,一部分是要從Binder讀出的資料,驅動程式先處理寫部分再處理讀部分。這樣安排的好處是應用程式可以很靈活地處理指令的同步或異步。例如若要發送異步指令可以隻填入寫部分而将read_size置成0;若要隻從Binder獲得資料可以将寫部分置空即write_size置成0;若要發送請求并同步等待傳回資料可以将兩部分都置上。

4.1 BINDER_WRITE_READ 之寫操作

Binder寫操作的資料時格式同樣也是(指令+資料)。這時候指令和資料都存放在binder_write_read 結構write_buffer域指向的記憶體空間裡,多條指令可以連續存放。資料緊接着存放在指令後面,格式根據指令不同而不同。下表列舉了Binder寫操作支援的指令:

表 3 Binder寫操作指令字

cmd 含義 arg

BC_TRANSACTION

BC_REPLY

BC_TRANSACTION用于Client向Server發送請求資料;BC_REPLY用于Server向Client發送回複(應答)資料。其後面緊接着一個binder_transaction_data結構體表明要寫入的資料。 struct binder_transaction_data

BC_ACQUIRE_RESULT

BC_ATTEMPT_ACQUIRE

暫未實作 ---
BC_FREE_BUFFER 釋放一塊映射的記憶體。Binder接收方通過mmap()映射一塊較大的記憶體空間,Binder驅動基于這片記憶體采用最佳比對算法實作接收資料緩存的動态配置設定和釋放,滿足并發請求對接收緩存區的需求。應用程式處理完這片資料後必須盡快使用該指令釋放緩存區,否則會因為緩存區耗盡而無法接收新資料。 指向需要釋放的緩存區的指針;該指針位于收到的Binder資料包中

BC_INCREFS

BC_ACQUIRE

BC_RELEASE

BC_DECREFS

這組指令增加或減少Binder的引用計數,用以實作強指針或弱指針的功能。 32位Binder引用号

BC_INCREFS_DONE

BC_ACQUIRE_DONE

第一次增加Binder實體引用計數時,驅動向Binder實體所在的程序發送BR_INCREFS, BR_ACQUIRE消息;Binder實體所在的程序處理完畢回饋BC_INCREFS_DONE,BC_ACQUIRE_DONE

void *ptr:Binder實體在使用者空間中的指針

void *cookie:與該實體相關的附加資料

BC_REGISTER_LOOPER

BC_ENTER_LOOPER

BC_EXIT_LOOPER

這組指令同BINDER_SET_MAX_THREADS一道實作Binder驅動對接收方線程池管理。BC_REGISTER_LOOPER通知驅動線程池中一個線程已經建立了;BC_ENTER_LOOPER通知驅動該線程已經進入主循環,可以接收資料;BC_EXIT_LOOPER通知驅動該線程退出主循環,不再接收資料。 ---
BC_REQUEST_DEATH_NOTIFICATION 獲得Binder引用的程序通過該指令要求驅動在Binder實體銷毀得到通知。雖說強指針可以確定隻要有引用就不會銷毀實體,但這畢竟是個跨程序的引用,誰也無法保證明體由于所在的Server關閉Binder驅動或異常退出而消失,引用者能做的是要求Server在此刻給出通知。

uint32 *ptr; 需要得到死亡通知的Binder引用

void **cookie: 與死亡通知相關的資訊,驅動會在發出死亡通知時傳回給送出請求的程序。

BC_DEAD_BINDER_DONE 收到實體死亡通知書的程序在删除引用後用本指令告知驅動。 void **cookie

在這些指令中,最常用的是BC_TRANSACTION/BC_REPLY指令對,Binder請求和應答資料就是通過這對指令發送給接收方。這對指令所承載的資料包由結構體struct binder_transaction_data定義。Binder互動有同步和異步之分,利用binder_transaction_data中flag域區分。如果flag域的TF_ONE_WAY位為1則為異步互動,即Client端發送完請求互動即結束, Server端不再傳回BC_REPLY資料包;否則Server會傳回BC_REPLY資料包,Client端必須等待接收完該資料包方才完成一次互動。

4.2 BINDER_WRITE_READ :從Binder讀出資料

從Binder裡讀出的資料格式和向Binder中寫入的資料格式一樣,采用(消息ID+資料)形式,并且多條消息可以連續存放。下表列舉了從Binder讀出的指令字及其相應的參數:

表 4 Binder讀操作消息ID

消息 含義 參數
BR_ERROR 發生内部錯誤(如記憶體配置設定失敗) ---

BR_OK

BR_NOOP

操作完成 ---
BR_SPAWN_LOOPER 該消息用于接收方線程池管理。當驅動發現接收方所有線程都處于忙碌狀态且線程池裡的線程總數沒有超過BINDER_SET_MAX_THREADS設定的最大線程數時,向接收方發送該指令要求建立更多線程以備接收資料。 ---

BR_TRANSACTION

BR_REPLY

這兩條消息分别對應發送方的BC_TRANSACTION和BC_REPLY,表示目前接收的資料是請求還是回複。 binder_transaction_data

BR_ACQUIRE_RESULT

BR_ATTEMPT_ACQUIRE

BR_FINISHED

尚未實作 ---
BR_DEAD_REPLY 互動過程中如果發現對方程序或線程已經死亡則傳回該消息 ---
BR_TRANSACTION_COMPLETE 發送方通過BC_TRANSACTION或BC_REPLY發送完一個資料包後,都能收到該消息做為成功發送的回報。這和BR_REPLY不一樣,是驅動告知發送方已經發送成功,而不是Server端傳回請求資料。是以不管同步還是異步互動接收方都能獲得本消息。 ---

BR_INCREFS

BR_ACQUIRE

BR_RELEASE

BR_DECREFS

這一組消息用于管理強/弱指針的引用計數。隻有提供Binder實體的程序才能收到這組消息。

void *ptr:Binder實體在使用者空間中的指針

void *cookie:與該實體相關的附加資料

BR_DEAD_BINDER

BR_CLEAR_DEATH_NOTIFICATION_DONE

向獲得Binder引用的程序發送Binder實體死亡通知書;收到死亡通知書的程序接下來會傳回BC_DEAD_BINDER_DONE做确認。 void **cookie:在使用BC_REQUEST_DEATH_NOTIFICATION注冊死亡通知時的附加參數。
BR_FAILED_REPLY 如果發送非法引用号則傳回該消息 ---

和寫資料一樣,其中最重要的消息是BR_TRANSACTION 或BR_REPLY,表明收到了一個格式為binder_transaction_data的請求資料包(BR_TRANSACTION)或傳回資料包(BR_REPLY)。

4.3 struct binder_transaction_data :收發資料包結構

該結構是Binder接收/發送資料包的标準格式,每個成員定義如下:

表 5 Binder收發資料包結構:binder_transaction_data

成員 含義

union {

size_t handle;

void *ptr;

} target;

對于發送資料包的一方,該成員指明發送目的地。由于目的是在遠端,是以這裡填入的是對Binder實體的引用,存放在target.handle中。如前述,Binder的引用在代碼中也叫句柄(handle)。

當資料包到達接收方時,驅動已将該成員修改成Binder實體,即指向Binder對象記憶體的指針,使用target.ptr來獲得。該指針是接收方在将Binder實體傳輸給其它程序時送出給驅動的,驅動程式能夠自動将發送方填入的引用轉換成接收方Binder對象的指針,故接收方可以直接将其當做對象指針來使用(通常是将其reinterpret_cast成相應類)。

void *cookie; 發送方忽略該成員;接收方收到資料包時,該成員存放的是建立Binder實體時由該接收方自定義的任意數值,做為與Binder指針相關的額外資訊存放在驅動中。驅動基本上不關心該成員。
unsigned int code; 該成員存放收發雙方約定的指令碼,驅動完全不關心該成員的内容。通常是Server端定義的公共接口函數的編号。
unsigned int flags; 與互動相關的标志位,其中最重要的是TF_ONE_WAY位。如果該位置上表明這次互動是異步的,Server端不會傳回任何資料。驅動利用該位來決定是否建構與傳回有關的資料結構。另外一位TF_ACCEPT_FDS是出于安全考慮,如果發起請求的一方不希望在收到的回複中接收檔案形式的Binder可以将該位置上。因為收到一個檔案形式的Binder會自動為資料接收方打開一個檔案,使用該位可以防止打開檔案過多。

pid_t sender_pid;

uid_t sender_euid;

該成員存放發送方的程序ID和使用者ID,由驅動負責填入,接收方可以讀取該成員獲知發送方的身份。
size_t data_size; 該成員表示data.buffer指向的緩沖區存放的資料長度。發送資料時由發送方填入,表示即将發送的資料長度;在接收方用來告知接收到資料的長度。
size_t offsets_size; 驅動一般情況下不關心data.buffer裡存放什麼資料,但如果有Binder在其中傳輸則需要将其相對data.buffer的偏移位置指出來讓驅動知道。有可能存在多個Binder同時在資料中傳遞,是以須用數組表示所有偏移位置。本成員表示該數組的大小。

union {

struct {

const void *buffer;

const void *offsets;

} ptr;

uint8_t buf[8];

} data;

data.bufer存放要發送或接收到的資料;data.offsets指向Binder偏移位置數組,該數組可以位于data.buffer中,也可以在另外的記憶體空間中,并無限制。buf[8]是為了無論保證32位還是64位平台,成員data的大小都是8個位元組。

這裡有必要再強調一下offsets_size和data.offsets兩個成員,這是Binder通信有别于其它IPC的地方。如前述,Binder采用面向對象的設計思想,一個Binder實體可以發送給其它程序進而建立許多跨程序的引用;另外這些引用也可以在程序之間傳遞,就象java裡将一個引用賦給另一個引用一樣。為Binder在不同程序中建立引用必須有驅動的參與,由驅動在核心建立并注冊相關的資料結構後接收方才能使用該引用。而且這些引用可以是強類型,需要驅動為其維護引用計數。然而這些跨程序傳遞的Binder混雜在應用程式發送的資料包裡,資料格式由使用者定義,如果不把它們一一标記出來告知驅動,驅動将無法從資料中将它們提取出來。于是就使用數組data.offsets存放使用者資料中每個Binder相對data.buffer的偏移量,用offsets_size表示這個數組的大小。驅動在發送資料包時會根據data.offsets和offset_size将散落于data.buffer中的Binder找出來并一一為它們建立相關的資料結構。在資料包中傳輸的Binder是類型為struct flat_binder_object的結構體,詳見後文。

對于接收方來說,該結構隻相當于一個定長的消息頭,真正的使用者資料存放在data.buffer所指向的緩存區中。如果發送方在資料中内嵌了一個或多個Binder,接收到的資料包中同樣會用data.offsets和offset_size指出每個Binder的位置和總個數。不過通常接收方可以忽略這些資訊,因為接收方是知道資料格式的,參考雙方約定的格式定義就能知道這些Binder在什麼位置。

Android Bander設計與實作 - 設計篇(轉)

圖 2 BINDER_WRITE_READ資料包執行個體

5 Binder 的表述

考察一次Binder通信的全過程會發現,Binder存在于系統以下幾個部分中:

· 應用程式程序:分别位于Server程序和Client程序中

· Binder驅動:分别管理為Server端的Binder實體和Client端的引用

· 傳輸資料:由于Binder可以跨程序傳遞,需要在傳輸資料中予以表述

在系統不同部分,Binder實作的功能不同,表現形式也不一樣。接下來逐一探讨Binder在各部分所扮演的角色和使用的資料結構。

5.1 Binder 在應用程式中的表述

雖然Binder用到了面向對象的思想,但并不限制應用程式一定要使用面向對象的語言,無論是C語言還是C++語言都可以很容易的使用Binder來通信。例如盡管Android主要使用java/C++,象SMgr這麼重要的程序就是用C語言實作的。不過面向對象的方式表述起來更友善,是以本文假設應用程式是用面向對象語言實作的。

Binder本質上隻是一種底層通信方式,和具體服務沒有關系。為了提供具體服務,Server必須提供一套接口函數以便Client通過遠端通路使用各種服務。這時通常采用Proxy設計模式:将接口函數定義在一個抽象類中,Server和Client都會以該抽象類為基類實作所有接口函數,所不同的是Server端是真正的功能實作,而Client端是對這些函數遠端調用請求的包裝。如何将Binder和Proxy設計模式結合起來是應用程式實作面向對象Binder通信的根本問題。

5.1.1 Binder 在Server端的表述 – Binder實體

做為Proxy設計模式的基礎,首先定義一個抽象接口類封裝Server所有功能,其中包含一系列純虛函數留待Server和Proxy各自實作。由于這些函數需要跨程序調用,須為其一一編号,進而Server可以根據收到的編号決定調用哪個函數。其次就要引入Binder了。Server端定義另一個Binder抽象類處理來自Client的Binder請求資料包,其中最重要的成員是虛函數onTransact()。該函數分析收到的資料包,調用相應的接口函數處理請求。

接下來采用繼承方式以接口類和Binder抽象類為基類建構Binder在Server中的實體,實作基類裡所有的虛函數,包括公共接口函數以及資料包處理函數:onTransact()。這個函數的輸入是來自Client的binder_transaction_data結構的資料包。前面提到,該結構裡有個成員code,包含這次請求的接口函數編号。onTransact()将case-by-case地解析code值,從資料包裡取出函數參數,調用接口類中相應的,已經實作的公共接口函數。函數執行完畢,如果需要傳回資料就再建構一個binder_transaction_data包将傳回資料包填入其中。

那麼各個Binder實體的onTransact()又是什麼時候調用呢?這就需要驅動參與了。前面說過,Binder實體須要以Binde傳輸結構flat_binder_object形式發送給其它程序才能建立Binder通信,而Binder實體指針就存放在該結構的handle域中。驅動根據Binder位置數組從傳輸資料中擷取該Binder的傳輸結構,為它建立位于核心中的Binder節點,将Binder實體指針記錄在該節點中。如果接下來有其它程序向該Binder發送資料,驅動會根據節點中記錄的資訊将Binder實體指針填入binder_transaction_data的target.ptr中傳回給接收線程。接收線程從資料包中取出該指針,reinterpret_cast成Binder抽象類并調用onTransact()函數。由于這是個虛函數,不同的Binder實體中有各自的實作,進而可以調用到不同Binder實體提供的onTransact()。

5.1.2 Binder 在Client端的表述 – Binder引用

做為Proxy設計模式的一部分,Client端的Binder同樣要繼承Server提供的公共接口類并實作公共函數。但這不是真正的實作,而是對遠端函數調用的包裝:将函數參數打包,通過Binder向Server發送申請并等待傳回值。為此Client端的Binder還要知道Binder實體的相關資訊,即對Binder實體的引用。該引用或是由SMgr轉發過來的,對實名Binder的引用或是由另一個程序直接發送過來的,對匿名Binder的引用。

由于繼承了同樣的公共接口類,Client Binder提供了與Server Binder一樣的函數原型,使使用者感覺不出Server是運作在本地還是遠端。Client Binder中,公共接口函數的包裝方式是:建立一個binder_transaction_data資料包,将其對應的編碼填入code域,将調用該函數所需的參數填入data.buffer指向的緩存中,并指明資料包的目的地,那就是已經獲得的對Binder實體的引用,填入資料包的target.handle中。注意這裡和Server的差別:實際上target域是個聯合體,包括ptr和handle兩個成員,前者用于接收資料包的Server,指向 Binder實體對應的記憶體空間;後者用于作為請求方的Client,存放Binder實體的引用,告知驅動資料包将路由給哪個實體。資料包準備好後,通過驅動接口發送出去。經過BC_TRANSACTION/BC_REPLY回合完成函數的遠端調用并得到傳回值。

5.2 Binder 在傳輸資料中的表述

Binder可以塞在資料包的有效資料中越程序邊界從一個程序傳遞給另一個程序,這些傳輸中的Binder用結構flat_binder_object表示,如下表所示:

表 6 Binder傳輸結構:flat_binder_object

成員 含義
unsigned long type

表明該Binder的類型,包括以下幾種:

BINDER_TYPE_BINDER:表示傳遞的是Binder實體,并且指向該實體的引用都是強類型;

BINDER_TYPE_WEAK_BINDER:表示傳遞的是Binder實體,并且指向該實體的引用都是弱類型;

BINDER_TYPE_HANDLE:表示傳遞的是Binder強類型的引用

BINDER_TYPE_WEAK_HANDLE:表示傳遞的是Binder弱類型的引用

BINDER_TYPE_FD:表示傳遞的是檔案形式的Binder,詳見下節

unsigned long flags

該域隻對第一次傳遞Binder實體時有效,因為此刻驅動需要在核心中建立相應的實體節點,有些參數需要從該域取出:

第0-7位:代碼中用FLAT_BINDER_FLAG_PRIORITY_MASK取得,表示處理本實體請求資料包的線程的最低優先級。當一個應用程式提供多個實體時,可以通過該參數調整配置設定給各個實體的處理能力。

第8位:代碼中用FLAT_BINDER_FLAG_ACCEPTS_FDS取得,置1表示該實體可以接收其它程序發過來的檔案形式的Binder。由于接收檔案形式的Binder會在本程序中自動打開檔案,有些Server可以用該标志禁止該功能,以防打開過多檔案。

union {

void *binder;

signed long handle;

};

當傳遞的是Binder實體時使用binder域,指向Binder實體在應用程式中的位址。

當傳遞的是Binder引用時使用handle域,存放Binder在程序中的引用号。

void *cookie; 該域隻對Binder實體有效,存放與該Binder有關的附加資訊。

無論是Binder實體還是對實體的引用都從屬與某個程序,是以該結構不能透明地在程序之間傳輸,必須經過驅動翻譯。例如當Server把Binder實體傳遞給Client時,在發送資料流中,flat_binder_object中的type是BINDER_TYPE_BINDER,binder指向Server程序使用者空間位址。如果透傳給接收端将毫無用處,驅動必須對資料流中的這個Binder做修改:将type該成BINDER_TYPE_HANDLE;為這個Binder在接收程序中建立位于核心中的引用并将引用号填入handle中。對于發生資料流中引用類型的Binder也要做同樣轉換。經過處理後接收程序從資料流中取得的Binder引用才是有效的,才可以将其填入資料包binder_transaction_data的target.handle域,向Binder實體發送請求。

這樣做也是出于安全性考慮:應用程式不能随便猜測一個引用号填入target.handle中就可以向Server請求服務了,因為驅動并沒有為你在核心中建立該引用,必定會被驅動拒絕。唯有經過身份認證确認合法後,由‘權威機構’(Binder驅動)親手授予你的Binder才能使用,因為這時驅動已經在核心中為你使用該Binder做了注冊,交給你的引用号是合法的。

下表總結了當flat_binder_object結構穿過驅動時驅動所做的操作:

表 7 驅動對flat_binder_object的操作

Binder 類型( type 域) 在發送方的操作 在接收方的操作

BINDER_TYPE_BINDER

BINDER_TYPE_WEAK_BINDER

隻有實體所在的程序能發送該類型的Binder。如果是第一次發送驅動将建立實體在核心中的節點,并儲存binder,cookie,flag域。 如果是第一次接收該Binder則建立實體在核心中的引用;将handle域替換為建立的引用号;将type域替換為BINDER_TYPE_(WEAK_)HANDLE

BINDER_TYPE_HANDLE

BINDER_TYPE_WEAK_HANDLE

獲得Binder引用的程序都能發送該類型Binder。驅動根據handle域提供的引用号查找建立在核心的引用。如果找到說明引用号合法,否則拒絕該發送請求。

如果收到的Binder實體位于接收程序中:将ptr域替換為儲存在節點中的binder值;cookie替換為儲存在節點中的cookie值;type替換為BINDER_TYPE_(WEAK_)BINDER。

如果收到的Binder實體不在接收程序中:如果是第一次接收則建立實體在核心中的引用;将handle域替換為建立的引用号

BINDER_TYPE_FD 驗證handle域中提供的打開檔案号是否有效,無效則拒絕該發送請求。 在接收方建立新的打開檔案号并将其與提供的打開檔案描述結構綁定。
5.2.1 檔案形式的 Binder

除了通常意義上用來通信的Binder,還有一種特殊的Binder:檔案Binder。這種Binder的基本思想是:将檔案看成Binder實體,程序打開的檔案号看成Binder的引用。一個程序可以将它打開檔案的檔案号傳遞給另一個程序,進而另一個程序也打開了同一個檔案,就象Binder的引用在程序之間傳遞一樣。

一個程序打開一個檔案,就獲得與該檔案綁定的打開檔案号。從Binder的角度,linux在核心建立的打開檔案描述結構struct file是Binder的實體,打開檔案号是該程序對該實體的引用。既然是Binder那麼就可以在程序之間傳遞,故也可以用flat_binder_object結構将檔案Binder通過資料包發送至其它程序,隻是結構中type域的值為BINDER_TYPE_FD,表明該Binder是檔案Binder。而結構中的handle域則存放檔案在發送方程序中的打開檔案号。我們知道打開檔案号是個局限于某個程序的值,一旦跨程序就沒有意義了。這一點和Binder實體使用者指針或Binder引用号是一樣的,若要跨程序同樣需要驅動做轉換。驅動在接收Binder的程序空間建立一個新的打開檔案号,将它與已有的打開檔案描述結構struct file勾連上,從此該Binder實體又多了一個引用。建立的打開檔案号覆寫flat_binder_object中原來的檔案号交給接收程序。接收程序利用它可以執行read(),write()等檔案操作。

傳個檔案為啥要這麼麻煩,直接将檔案名用Binder傳過去,接收方用open()打開不就行了嗎?其實這還是有差別的。首先對同一個打開檔案共享的層次不同:使用檔案Binder打開的檔案共享linux VFS中的struct file,struct dentry,struct inode結構,這意味着一個程序使用read()/write()/seek()改變了檔案指針,另一個程序的檔案指針也會改變;而如果兩個程序分别使用同一檔案名打開檔案則有各自的struct file結構,進而各自獨立維護檔案指針,互不幹擾。其次是一些特殊裝置檔案要求在struct file一級共享才能使用,例如android的另一個驅動ashmem,它和Binder一樣也是misc裝置,用以實作程序間的共享記憶體。一個程序打開的ashmem檔案隻有通過檔案Binder發送到另一個程序才能實作記憶體共享,這大大提高了記憶體共享的安全性,道理和Binder增強了IPC的安全性是一樣的。

5.3 Binder 在驅動中的表述

驅動是Binder通信的核心,系統中所有的Binder實體以及每個實體在各個程序中的引用都登記在驅動中;驅動需要記錄Binder引用->實體之間多對一的關系;為引用找到對應的實體;在某個程序中為實體建立或查找到對應的引用;記錄Binder的歸屬地(位于哪個程序中);通過管理Binder的強/弱引用建立/銷毀Binder實體等等。

驅動裡的Binder是什麼時候建立的呢?前面提到過,為了實作實名Binder的注冊,系統必須建立第一隻雞–為SMgr建立的,用于注冊實名Binder的Binder實體,負責實名Binder注冊過程中的程序間通信。既然建立了實體就要有對應的引用:驅動将所有程序中的0号引用都預留給該Binder實體,即所有程序的0号引用天然地都指向注冊實名Binder專用的Binder,無須特殊操作即可以使用0号引用來注冊實名Binder。接下來随着應用程式不斷地注冊實名Binder,不斷向SMgr索要Binder的引用,不斷将Binder從一個程序傳遞給另一個程序,越來越多的Binder以傳輸結構 - flat_binder_object的形式穿越驅動做跨程序的遷徙。由于binder_transaction_data中data.offset數組的存在,所有流經驅動的Binder都逃不過驅動的眼睛。Binder将對這些穿越程序邊界的Binder做如下操作:檢查傳輸結構的type域,如果是BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER則建立Binder的實體;如果是BINDER_TYPE_HANDLE或BINDER_TYPE_WEAK_HANDLE則建立Binder的引用;如果是BINDER_TYPE_HANDLE則為程序打開檔案,無須建立任何資料結構。詳細過程可參考表7。随着越來越多的Binder實體或引用在程序間傳遞,驅動會在核心裡建立越來越多的節點或引用,當然這個過程對使用者來說是透明的。

5.3.1 Binder 實體在驅動中的表述

驅動中的Binder實體也叫‘節點’,隸屬于提供實體的程序,由struct binder_node結構來表示:

表 8 Binder節點描述結構:binder_node

成員 含義
int debug_id; 用于調試
struct binder_work work; 當本節點引用計數發生改變,需要通知所屬程序時,通過該成員挂入所屬程序的to-do隊列裡,喚醒所屬程序執行Binder實體引用計數的修改。

union {

struct rb_node rb_node;

struct hlist_node dead_node;

};

每個程序都維護一棵紅黑樹,以Binder實體在使用者空間的指針,即本結構的ptr成員為索引存放該程序所有的Binder實體。這樣驅動可以根據Binder實體在使用者空間的指針很快找到其位于核心的節點。rb_node用于将本節點鍊入該紅黑樹中。

銷毀節點時須将rb_node從紅黑樹中摘除,但如果本節點還有引用沒有切斷,就用dead_node将節點隔離到另一個連結清單中,直到通知所有程序切斷與該節點的引用後,該節點才可能被銷毀。

struct binder_proc *proc; 本成員指向節點所屬的程序,即提供該節點的程序。
struct hlist_head refs; 本成員是隊列頭,所有指向本節點的引用都連結在該隊列裡。這些引用可能隸屬于不同的程序。通過該隊列可以周遊指向該節點的所有引用。
int internal_strong_refs; 用以實作強指針的計數器:産生一個指向本節點的強引用該計數就會加1。
int local_weak_refs; 驅動為傳輸中的Binder設定的弱引用計數。如果一個Binder打包在資料包中從一個程序發送到另一個程序,驅動會為該Binder增加引用計數,直到接收程序通過BC_FREE_BUFFER通知驅動釋放該資料包的資料區為止。
int local_strong_refs; 驅動為傳輸中的Binder設定的強引用計數。同上。
void __user *ptr; 指向使用者空間Binder實體的指針,來自于flat_binder_object的binder成員
void __user *cookie; 指向使用者空間的附加指針,來自于flat_binder_object的cookie成員

unsigned has_strong_ref;

unsigned pending_strong_ref;

unsigned has_weak_ref;

unsigned pending_weak_ref

這一組标志用于控制驅動與Binder實體所在程序互動式修改引用計數
unsigned has_async_transaction; 該成員表明該節點在to-do隊列中有異步互動尚未完成。驅動将所有發送往接收端的資料包暫存在接收程序或線程開辟的to-do隊列裡。對于異步互動,驅動做了适當流控:如果to-do隊列裡有異步互動尚待處理則該成員置1,這将導緻新到的異步互動存放在本結構成員 – asynch_todo隊列中,而不直接送到to-do隊列裡。目的是為同步互動讓路,避免長時間阻塞發送端。
unsigned accept_fds 表明節點是否同意接受檔案方式的Binder,來自flat_binder_object中flags成員的FLAT_BINDER_FLAG_ACCEPTS_FDS位。由于接收檔案Binder會為程序自動打開一個檔案,占用有限的檔案描述符,節點可以設定該位拒絕這種行為。
int min_priority

設定處理Binder請求的線程的最低優先級。發送線程将資料送出給接收線程處理時,驅動會将發送線程的優先級也賦予接收線程,使得資料即使跨了程序也能以同樣優先級得到處理。不過如果發送線程優先級過低,接收線程将以預設的最小值運作。

該域的值來自于flat_binder_object中flags成員。

struct list_head async_todo 異步互動等待隊列;用于分流發往本節點的異步互動包

每個程序都有一棵紅黑樹用于存放建立好的節點,以Binder在使用者空間的指針作為索引。每當在傳輸資料中偵測到一個代表Binder實體的flat_binder_object,先以該結構的binder指針為索引搜尋紅黑樹;如果沒找到就建立一個新節點添加到樹中。由于對于同一個程序來說記憶體位址是唯一的,是以不會重複建設造成混亂。

5.3.2 Binder 引用在驅動中的表述

和實體一樣,Binder的引用也是驅動根據傳輸資料中的flat_binder_object建立的,隸屬于獲得該引用的程序,用struct binder_ref結構體表示:

表 9 Binder引用描述結構:binder_ref

成員 含義
int debug_id; 調試用
struct rb_node rb_node_desc; 每個程序有一棵紅黑樹,程序所有引用以引用号(即本結構的desc域)為索引添入該樹中。本成員用做連結到該樹的一個節點。
struct rb_node rb_node_node; 每個程序又有一棵紅黑樹,程序所有引用以節點實體在驅動中的記憶體位址(即本結構的node域)為所引添入該樹中。本成員用做連結到該樹的一個節點。
struct hlist_node node_entry; 該域将本引用做為節點鍊入所指向的Binder實體結構binder_node中的refs隊列
struct binder_proc *proc; 本引用所屬的程序
struct binder_node *node; 本引用所指向的節點(Binder實體)
uint32_t desc; 本結構的引用号
int strong; 強引用計數
int weak; 弱引用計數
struct binder_ref_death *death; 應用程式向驅動發送BC_REQUEST_DEATH_NOTIFICATION或BC_CLEAR_DEATH_NOTIFICATION指令進而當Binder實體銷毀時能夠收到來自驅動的提醒。該域不為空表明使用者訂閱了對應實體銷毀的‘噩耗’。

就象一個對象有很多指針一樣,同一個Binder實體可能有很多引用,不同的是這些引用可能分布在不同的程序中。和實體一樣,每個程序使用紅黑樹存放所有正在使用的引用。不同的是Binder的引用可以通過兩個鍵值索引:

· 對應實體在核心中的位址。注意這裡指的是驅動建立于核心中的binder_node結構的位址,而不是Binder實體在使用者程序中的位址。實體在核心中的位址是唯一的,用做索引不會産生二義性;但實體可能來自不同使用者程序,而實體在不同使用者程序中的位址可能重合,不能用來做索引。驅動利用該紅黑樹在一個程序中快速查找某個Binder實體所對應的引用(一個實體在一個程序中隻建立一個引用)。

· 引用号。引用号是驅動為引用配置設定的一個32位辨別,在一個程序内是唯一的,而在不同程序中可能會有同樣的值,這和程序的打開檔案号很類似。引用号将傳回給應用程式,可以看作Binder引用在使用者程序中的句柄。除了0号引用在所有程序裡都固定保留給SMgr,其它值由驅動動态配置設定。向Binder發送資料包時,應用程式将引用号填入binder_transaction_data結構的target.handle域中表明該資料包的目的Binder。驅動根據該引用号在紅黑樹中找到引用的binder_ref結構,進而通過其node域知道目标Binder實體所在的程序及其它相關資訊,實作資料包的路由。

6 Binder 記憶體映射和接收緩存區管理

暫且撇開Binder,考慮一下傳統的IPC方式中,資料是怎樣從發送端到達接收端的呢?通常的做法是,發送方将準備好的資料存放在緩存區中,調用API通過系統調用進入核心中。核心服務程式在核心空間配置設定記憶體,将資料從發送方緩存區複制到核心緩存區中。接收方讀資料時也要提供一塊緩存區,核心将資料從核心緩存區拷貝到接收方提供的緩存區中并喚醒接收線程,完成一次資料發送。這種存儲-轉發機制有兩個缺陷:首先是效率低下,需要做兩次拷貝:使用者空間->核心空間->使用者空間。Linux使用copy_from_user()和copy_to_user()實作這兩個跨空間拷貝,在此過程中如果使用了高端記憶體(high memory),這種拷貝需要臨時建立/取消頁面映射,造成性能損失。其次是接收資料的緩存要由接收方提供,可接收方不知道到底要多大的緩存才夠用,隻能開辟盡量大的空間或先調用API接收消息頭獲得消息體大小,再開辟适當的空間接收消息體。兩種做法都有不足,不是浪費空間就是浪費時間。

Binder采用一種全新政策:由Binder驅動負責管理資料接收緩存。我們注意到Binder驅動實作了mmap()系統調用,這對字元裝置是比較特殊的,因為mmap()通常用在有實體存儲媒體的檔案系統上,而象Binder這樣沒有實體媒體,純粹用來通信的字元裝置沒必要支援mmap()。Binder驅動當然不是為了在實體媒體和使用者空間做映射,而是用來建立資料接收的緩存空間。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

這樣Binder的接收方就有了一片大小為MAP_SIZE的接收緩存區。mmap()的傳回值是記憶體映射在使用者空間的位址,不過這段空間是由驅動管理,使用者不必也不能直接通路(映射類型為PROT_READ,隻讀映射)。

接收緩存區映射好後就可以做為緩存池接收和存放資料了。前面說過,接收資料包的結構為binder_transaction_data,但這隻是消息頭,真正的有效負荷位于data.buffer所指向的記憶體中。這片記憶體不需要接收方提供,恰恰是來自mmap()映射的這片緩存池。在資料從發送方向接收方拷貝時,驅動會根據發送資料包的大小,使用最佳比對算法從緩存池中找到一塊大小合适的空間,将資料從發送緩存區複制過來。要注意的是,存放binder_transaction_data結構本身以及表4中所有消息的記憶體空間還是得由接收者提供,但這些資料大小固定,數量也不多,不會給接收方造成不便。映射的緩存池要足夠大,因為接收方的線程池可能會同時處理多條并發的互動,每條互動都需要從緩存池中擷取目的存儲區,一旦緩存池耗竭将産生導緻無法預期的後果。

有配置設定必然有釋放。接收方在處理完資料包後,就要通知驅動釋放data.buffer所指向的記憶體區。在介紹Binder協定時已經提到,這是由指令BC_FREE_BUFFER完成的。

通過上面介紹可以看到,驅動為接收方分擔了最為繁瑣的任務:配置設定/釋放大小不等,難以預測的有效負荷緩存區,而接收方隻需要提供緩存來存放大小固定,最大空間可以預測的消息頭即可。在效率上,由于mmap()配置設定的記憶體是映射在接收方使用者空間裡的,所有總體效果就相當于對有效負荷資料做了一次從發送方使用者空間到接收方使用者空間的直接資料拷貝,省去了核心中暫存這個步驟,提升了一倍的性能。順便再提一點,Linux核心實際上沒有從一個使用者空間到另一個使用者空間直接拷貝的函數,需要先用copy_from_user()拷貝到核心空間,再用copy_to_user()拷貝到另一個使用者空間。為了實作使用者空間到使用者空間的拷貝,mmap()配置設定的記憶體除了映射進了接收方程序裡,還映射進了核心空間。是以調用copy_from_user()将資料拷貝進核心空間也相當于拷貝進了接收方的使用者空間,這就是Binder隻需一次拷貝的‘秘密’。

7 Binder 接收線程管理

Binder通信實際上是位于不同程序中的線程之間的通信。假如程序S是Server端,提供Binder實體,線程T1從Client程序C1中通過Binder的引用向程序S發送請求。S為了處理這個請求需要啟動線程T2,而此時線程T1處于接收傳回資料的等待狀态。T2處理完請求就會将處理結果傳回給T1,T1被喚醒得到處理結果。在這過程中,T2仿佛T1在程序S中的代理,代表T1執行遠端任務,而給T1的感覺就是象穿越到S中執行一段代碼又回到了C1。為了使這種穿越更加真實,驅動會将T1的一些屬性賦給T2,特别是T1的優先級nice,這樣T2會使用和T1類似的時間完成任務。很多資料會用‘線程遷移’來形容這種現象,容易讓人産生誤解。一來線程根本不可能在程序之間跳來跳去,二來T2除了和T1優先級一樣,其它沒有相同之處,包括身份,打開檔案,棧大小,信号處理,私有資料等。

對于Server程序S,可能會有許多Client同時發起請求,為了提高效率往往開辟線程池并發處理收到的請求。怎樣使用線程池實作并發處理呢?這和具體的IPC機制有關。拿socket舉例,Server端的socket設定為偵聽模式,有一個專門的線程使用該socket偵聽來自Client的連接配接請求,即阻塞在accept()上。這個socket就象一隻會生蛋的雞,一旦收到來自Client的請求就會生一個蛋 – 建立新socket并從accept()傳回。偵聽線程從線程池中啟動一個工作線程并将剛下的蛋交給該線程。後續業務處理就由該線程完成并通過這個單與Client實作互動。

可是對于Binder來說,既沒有偵聽模式也不會下蛋,怎樣管理線程池呢?一種簡單的做法是,不管三七二十一,先建立一堆線程,每個線程都用BINDER_WRITE_READ指令讀Binder。這些線程會阻塞在驅動為該Binder設定的等待隊列上,一旦有來自Client的資料驅動會從隊列中喚醒一個線程來處理。這樣做簡單直覺,省去了線程池,但一開始就建立一堆線程有點浪費資源。于是Binder協定引入了專門指令或消息幫助使用者管理線程池,包括:

· INDER_SET_MAX_THREADS

· BC_REGISTER_LOOP

· BC_ENTER_LOOP

· BC_EXIT_LOOP

· BR_SPAWN_LOOPER

首先要管理線程池就要知道池子有多大,應用程式通過INDER_SET_MAX_THREADS告訴驅動最多可以建立幾個線程。以後每個線程在建立,進入主循環,退出主循環時都要分别使用BC_REGISTER_LOOP,BC_ENTER_LOOP,BC_EXIT_LOOP告知驅動,以便驅動收集和記錄目前線程池的狀态。每當驅動接收完資料包傳回讀Binder的線程時,都要檢查一下是不是已經沒有閑置線程了。如果是,而且線程總數不會超出線程池最大線程數,就會在目前讀出的資料包後面再追加一條BR_SPAWN_LOOPER消息,告訴使用者線程即将不夠用了,請再啟動一些,否則下一個請求可能不能及時響應。新線程一啟動又會通過BC_xxx_LOOP告知驅動更新狀态。這樣隻要線程沒有耗盡,總是有空閑線程在等待隊列中随時待命,及時處理請求。

關于工作線程的啟動,Binder驅動還做了一點小小的優化。當程序P1的線程T1向程序P2發送請求時,驅動會先檢視一下線程T1是否也正在處理來自P2某個線程請求但尚未完成(沒有發送回複)。這種情況通常發生在兩個程序都有Binder實體并互相對發時請求時。假如驅動在程序P2中發現了這樣的線程,比如說T2,就會要求T2來處理T1的這次請求。因為T2既然向T1發送了請求尚未得到傳回包,說明T2肯定(或将會)阻塞在讀取傳回包的狀态。這時候可以讓T2順便做點事情,總比等在那裡閑着好。而且如果T2不是線程池中的線程還可以為線程池分擔部分工作,減少線程池使用率。

8 資料包接收隊列與(線程)等待隊列管理

通常資料傳輸的接收端有兩個隊列:資料包接收隊列和(線程)等待隊列,用以緩解供需沖突。當超市裡的進貨(資料包)太多,貨物會堆積在倉庫裡;購物的人(線程)太多,會排隊等待在收銀台,道理是一樣的。在驅動中,每個程序有一個全局的接收隊列,也叫to-do隊列,存放不是發往特定線程的資料包;相應地有一個全局等待隊列,所有等待從全局接收隊列裡收資料的線程在該隊列裡排隊。每個線程有自己私有的to-do隊列,存放發送給該線程的資料包;相應的每個線程都有各自私有等待隊列,專門用于本線程等待接收自己to-do隊列裡的資料。雖然名叫隊列,其實線程私有等待隊列中最多隻有一個線程,即它自己。

由于發送時沒有特别标記,驅動怎麼判斷哪些資料包該送入全局to-do隊列,哪些資料包該送入特定線程的to-do隊列呢?這裡有兩條規則。規則1:Client發給Server的請求資料包都送出到Server程序的全局to-do隊列。不過有個特例,就是上節談到的Binder對工作線程啟動的優化。經過優化,來自T1的請求不是送出給P2的全局to-do隊列,而是送入了T2的私有to-do隊列。規則2:對同步請求的傳回資料包(由BC_REPLY發送的包)都發送到發起請求的線程的私有to-do隊列中。如上面的例子,如果程序P1的線程T1發給程序P2的線程T2的是同步請求,那麼T2傳回的資料包将送進T1的私有to-do隊列而不會送出到P1的全局to-do隊列。

資料包進入接收隊列的潛規則也就決定了線程進入等待隊列的潛規則,即一個線程隻要不接收傳回資料包則應該在全局等待隊列中等待新任務,否則就應該在其私有等待隊列中等待Server的傳回資料。還是上面的例子,T1在向T2發送同步請求後就必須等待在它私有等待隊列中,而不是在P1的全局等待隊列中排隊,否則将得不到T2的傳回的資料包。

這些潛規則是驅動對Binder通信雙方施加的限制條件,展現在應用程式上就是同步請求互動過程中的線程一緻性:1) Client端,等待傳回包的線程必須是發送請求的線程,而不能由一個線程發送請求包,另一個線程等待接收包,否則将收不到傳回包;2) Server端,發送對應傳回資料包的線程必須是收到請求資料包的線程,否則傳回的資料包将無法送交發送請求的線程。這是因為傳回資料包的目的Binder不是使用者指定的,而是驅動記錄在收到請求資料包的線程裡,如果發送傳回包的線程不是收到請求包的線程驅動将無從知曉傳回包将送往何處。

接下來探讨一下Binder驅動是如何遞交同步互動和異步互動的。我們知道,同步互動和異步互動的差別是同步互動的請求端(client)在送出請求資料包後須要等待應答端(Server)的傳回資料包,而異步互動的發送端送出請求資料包後互動即結束。對于這兩種互動的請求資料包,驅動可以不管三七二十一,統統丢到接收端的to-do隊列中一個個處理。但驅動并沒有這樣做,而是對異步互動做了限流,令其為同步互動讓路,具體做法是:對于某個Binder實體,隻要有一個異步互動沒有處理完畢,例如正在被某個線程處理或還在任意一條to-do隊列中排隊,那麼接下來發給該實體的異步互動包将不再投遞到to-do隊列中,而是阻塞在驅動為該實體開辟的異步互動接收隊列(Binder節點的async_todo域)中,但這期間同步互動依舊不受限制直接進入to-do隊列獲得處理。一直到該異步互動處理完畢下一個異步互動方可以脫離異步互動隊列進入to-do隊列中。之是以要這麼做是因為同步互動的請求端需要等待傳回包,必須迅速處理完畢以免影響請求端的響應速度,而異步互動屬于‘發射後不管’,稍微延時一點不會阻塞其它線程。是以用專門隊列将過多的異步互動暫存起來,以免突發大量異步互動擠占Server端的處理能力或耗盡線程池裡的線程,進而阻塞同步互動。

9 總結

Binder使用Client-Server通信方式,安全性好,簡單高效,再加上其面向對象的設計思想,獨特的接收緩存管理和線程池管理方式,成為Android程序間通信的中流砥柱。