Android Binder設計與實現 – 設計篇

摘要java

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方式的緣由有深刻了解。node

 

1 引言

基於Client-Server的通訊方式普遍應用於從互聯網和數據庫訪問到嵌入式手持設備內部通訊等各個領域。智能手機平臺特別是Android 系統中,爲了嚮應用開發者提供豐富多樣的功能,這種通訊方式更是無處不在,諸如媒體播放,視音頻頻捕獲,到各類讓手機更智能的傳感器(加速度,方位,溫度,光亮度等)都由不一樣的Server負責管理,應用程序只需作爲Client與這些Server創建鏈接即可以使用這些服務,花不多的時間和精力就能開發出使人眩目的功能。Client-Server方式的普遍採用對進程間通訊(IPC)機制是一個挑戰。目前linux支持的IPC包括傳統的管道,System V IPC,即消息隊列/共享內存/信號量,以及socket中只有socket支持Client-Server的通訊方式。固然也能夠在這些底層機制上架設一套協議來實現Client-Server通訊,但這樣增長了系統的複雜性,在手機這種條件複雜,資源稀缺的環境下可靠性也難以保證。linux

另外一方面是傳輸性能。socket做爲一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通訊和本機上進程間的低速通訊。消息隊列和管道採用存儲-轉發方式,即數據先從發送方緩存區拷貝到內核開闢的緩存區中,而後再從內核緩存區拷貝到接收方緩存區,至少有兩次拷貝過程。共享內存雖然無需拷貝,但控制複雜,難以使用。算法

表 1 各類IPC方式數據拷貝次數數據庫

IPC 數據拷貝次數
共享內存 0
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)以及驅動。其中 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()。 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通訊的全部角色,將在之後章節中一一提到。

圖 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用於寫入請求數據;BC_REPLY用於寫入回覆數據。其後面緊接着一個 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不同,是驅動告知發送方已經發送成功,而不是接收方返回請求數據。因此無論同步仍是異步交互接收方都能得到本消息。
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位。若是該位置上代表此次交互是異步的,接收方不會返回任何數據。驅動利用該位來決定是否構建與返回有關的數據結構。另一位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在什麼位置。

圖 2 BINDER_WRITE_READ數據包實例

 

5 Binder 的表述

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

· 應用程序進程:又分爲Server進程和Client進程

· Binder驅動:Server和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才能使用,由於這時驅動已經在內核中爲你創建了引用,交給你的引用號是合法的。

下表總結了當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進程間通訊的中流砥柱。

原文做者:universus

相關文章
相關標籤/搜索