本篇旨在對作者universus在《Android Bander設計與實現 - 設計篇》中表述錯誤的地方做修正。
Android Binder設計與實現 - 設計篇
摘要
Binder是Android系統提供的進程間通信(IPC)方式之一。Linux已經擁有管道、system V IPC消息隊列/共享內存/信號量)和socket等IPC手段,確還是依賴Binder實現進程間通信,說明Binder具有無可比擬的優勢。深入了解Binder并將之與傳統IPC做對比有助于我們深入領會進程間通信的實現和性能優化。本文將對Binder的設計細節做一個全面的闡述,首先我們通過介紹Binder通信模型和Binder通信協議了解Binder的設計,然后分別闡述Binder在系統不同部分的表述方式和起的作用,最后還會解釋Binder在數據接收端的設計考慮,包括線程池管理、內存映射和等待隊列管理等。
1 引言
基于Client-Server的通信方式廣泛的應用于互聯網和數據庫訪問。Android系統中為了向應用開發者提供豐富的功能,這種通信方式也是無處不在,從媒體播放、音視頻捕捉,到監聽傳感器都由不同的Server負責管理,應用程序只需作為Client與這些Server建立連接遍可以使用這些服務。因為Client和Server分處不同進程,所以Client-Server方式的廣泛使用對進程間通信(IPC)機制的可靠性是一個挑戰。目前Linux支持的IPC方式中只有socket支持Client-Server的通信方式。當然也可以在其他底層機制上架設一套協議來實現Client-Server通信,但這樣增加了系統的復雜性,在手機這種條件復雜、資源稀缺的環境下通信的可靠性也難以保證。
另一方面是傳輸性能。socket作為一款通用接口,其傳輸效率低、開銷大,主要用于跨網絡的進程間通信和本機進程間的低速通信。消息隊列和管道采用的存儲-轉發機制,即數據先從發送方緩存區拷貝到內核開辟的緩存區中,然后再從內核緩存區拷貝到接收方緩存區,至少要兩次拷貝過程。共享內存雖然沒有拷貝過程,但是因為其控制復雜并且需要其他IPC機制保證進程間同步,所以難以使用。
表 1 各種IPC方式拷貝次數
IPC | 數據拷貝次數 |
---|---|
共享內存 | 0 |
Binder | 1 |
Socket/管道/消息隊列 | 2 |
還有一點是出于安全性考慮。Android作為一個開放式,擁有眾多開發者的平臺,應用程序的來源廣泛,確保終端的安全是非常重要的。終端用戶不希望從網上下載的程序在不知情的情況下偷窺隱私數據、連接無線網絡以及長期操作底層設備導致電池很快耗盡等等。傳統IPC沒有任何安全措施,完全依賴上層協議確保。
首先傳統IPC的接收方無法獲得對方進程可靠的UID/PID(用戶ID/進程ID),從未無法鑒別對方身份。Android為每個應用程序分配自己的UID,同時也為應用程序的每個進程分配PID,所以進程的UID和PID是鑒別進程身份的重要標志。傳統IPC只能由用戶填寫UID/PID,但這樣不可靠,容易被惡意程序利用。可靠的身份標示只能由IPC機制本身在內核添加。
其次傳統IPC訪問的接入點是開放的,無法建立私有通道。比如命名管道的名稱、System V 的鍵值或者socket的ip地址和端口號都是開放的,只要知道了這些接入點的程序都可以對端建立連接,不管怎樣都無法阻止程序通過猜測接收端地址獲得連接。(注:Binder機制的IPC過程中,Server可以通過對外提供API文檔也是可以為滿足條件的Client提供開放式接入點的,比如Google官方以及其他第三方平臺提供的服務,此處作者想說明的是Client在不知道Server對外提供的接入點的情況下是無法通過窮舉來建立與Server的連接的。)
基于以上原因,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的地址;
- 指定通信協議來傳輸數據。
以上兩點可以用網絡通信比喻,網絡通信中Server的訪問接入點就是Server主機的IP地址+端口號,傳輸協議為TCP/UDP協議。對Binder機制而言,Binder就可以看做Server提供給外部訪問的接入點,Client通過這個接入點向Server發送請求來使用服務。
與其他IPC不同,Binder使用了面向對象的思想來描述訪問的接入點——Binder,及其在Client中的入口。Binder是一個位于Server的實體對象,該對象提供了一套方法來實現Client對Server的請求,這就是類的成員函數。Client中持有的訪問Server的入口是指向這個Binder對象的指針,一旦Client獲得了這個指針就可以調用它所指向的Binder對象的方法從而訪問Server。在Client看來,它通過Binder指針調用遠程Server的方法和通過指針調用本地進程的方法并沒有區別,盡管前者指向的是存在于遠程Server的實體對象,而后者是的實體在本地內存中。“指針(Pointer)”是C/C++的術語,Java中的說法是“引用(Reference)”,即Client通過Binder的引用訪問Server,而軟件領域的另一個術語“句柄(Handle)”也可以用來表述Binder在Client中的存在方式。從通信的角度看,Client中的Binder也可以看做是Server Binder的“代理(Proxy)”,在本地代表遠端Server為Client提供服務。本文中會使用“引用(Reference)”或“句柄(Handle)”這兩個被廣泛使用的術語。
面向對象思想的引入將進程間通信的過程轉化為Client通過Binder對象的引用來調用該對象中成員函數的過程,而其獨特之處在于Binder對象是一個可以跨進程引用的對象,他的實體位于一個進程,而它的引用卻遍布于各個進程中。最誘人的是,這個引用和Java里的一樣既可以是強類型,也可以是弱類型(Weak Reference),而且可以從一個進程傳給其它進程,讓大家都能訪問同一個Server,就像將一個對象的引用賦值給另一個引用一樣。Binder模糊了進程邊界,淡化了進程間通信過程,整個系統仿佛運行在同一個面向對象的程序之中。形形色色的Binder對象以及星羅棋布的Binder引用仿佛粘結各個進程的膠水,這也是Binder在英語里的原意。
當然面向對象只是針對同樣面向對象的應用程序而言,Binder驅動和內核其它模塊一樣也是使用C語言實現的,沒有類和對象的概念,Binder驅動為面向對象的進程間通信提供了底層支持。
3 Binder通信模式
Binder框架定義了四個角色:Server、Client、ServiceManager(以下簡稱SMgr)以及Binder驅動。其中Server、Client和SMgr運行于用戶空間,Binder驅動運行于內核空間。這四個角色的關系和互聯網類似: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中。(注:Android是基于Linux內核開發的,Binder并不是Linux內核的一部分,得益于Linux的Loadable Kernel Module——動態內核可加載模塊簡稱LKM機制,Linux中模塊是具有獨立功能的程序,它可以被單獨編譯但不能獨立運行,想要運行它需要將它鏈接到內核作為內核的一部分運行。Android系統通過動態添加一個模塊到內核空間,用戶進程通過這個內核模塊實現通信,這個運行在內核空間負責各個用戶進程通過Binder通信的內核模塊就叫做Binder驅動。)
3.2 ServiceManager 與實名Binder
和DNS類似,SMgr的作用是將字符形式的Binder名字轉換成Client中對該Binder的引用,是的Client能通過Binder名字獲得對Server中Binder實體的引用。注冊了名字的Binder叫實名Binder,就像每個網站除了有IP地址外還有自己的網址(域名)。Server創建了Binder實體,為其取了一個字符形式可讀易記的名字,將這個Binder連同名字以數據包的形式通過Binder驅動發送給SMgr,驅動為這個穿過進程邊界的Binder創建位于內核中的實體節點以及一個對這個實體的引用,將名字及新建的引用打包傳遞給SMgr,SMgr收到數據包后,從中取出名字和引用填入一張表中。
細心的讀者可能會發現其中的蹊蹺:SMgr是一個進程,Server是另一個進程,Server向SMgr注冊Binder必然會涉及進程間通信。當前實現的是進程間通信卻又要用到進程間通信,這就好像蛋生雞的前提是要有只雞生蛋。Binder的實現比較巧妙,SMgr和其它進程同樣采用Binder通信,SMgr是Server端,有自己的Binder對象或者說Binder實體,其它進程都是Client(包括待注冊的為其它Client提供服務的Server在SMgr看來都是Client),Client需要通過這個Binder的引用來實現Binder的注冊和查詢。SMgr提供的Binder實體比較特殊,它沒有名字也不需要注冊,當一個進程使用BINDER_SET_CONTEXT_MGR命令將自己注冊成SMgr時Binder驅動會為他創建Binder實體節點(注:這個實體節點與Server向SMgr發起注冊請求時Binder驅動為它創建的實體節點一樣,只是創建的過程不一樣,SMgr的節點是由BINDER_SET_CONTEXT_MGR命令創造,其它Server的節點是發起注冊時創造)。Client想得到這個Binder的引用也不需要從別處獲得,所有Client中的0號引用就是SMgr中Binder實體的引用。類比網絡通信,0號引用就是域名服務器DNS的服務器地址,已經預先配置好了。也就是說,一個Server若要向SMgr注冊自己的Binder就必須通過0這個引用號和SMgr的Binder通信。
3.3 Client獲得實名Binder的引用
Server通過0號向SMgr注冊了“名字”和對應的Binder實體后,Client也利用0號引用向SMgr請求指定名字對應的Binder引用。SMgr收到這個鏈接請求,從請求數據包獲得Binder的名字,在注冊表中找到該名字對應的條目,再從條目中取出Binder的引用,將該引用作為回復發送給Client。從面向對象的角度看,這個Server的Binder對象現在有了兩個引用,一個位于SMgr中,另一個位于發起請求的Client中,如果接下來有更多的Client請求該Binder,系統中就會有更多的引用指向該Binder,就像Java里的一個對象存在多個引用一樣。而且類似的這些指向Binder的引用是強類型的,從而確保只要有引用指向Binder對象,這個對象就不會被釋放掉。
3.4 匿名Binder
并不是所有Binder都需要注冊給SMgr廣而告之的。Server端可以通過已經建立的Binder連接將創建的Binder實體傳給Client,當然這條已經建立的Binder連接必須是通過實名Binder建立的。由于這個后創建的Binder沒有在SMgr注冊名字,所以是個匿名Binder。Client將會收到這個匿名Binder的引用,通過這個匿名Binder引用向位于Server中的Binder實體發送請求。(注:匿名Binder使Server可以創建多個可被Client引用的Binder實體甚至是不同類型的多種Binder實體,相對的實名Binder一個Server只能有一個就是 onBinder 方法返回的Binder。或許這是Android出于多態的考慮,比方說Server可以提供多種服務,但是不希望只用一個Binder實體實現,因為那樣意味著客戶端會收到一個過度冗余的Binder引用,除了自己需要的方法外還有許多自己用不到的方法,所以Server干脆注冊一個實名Binder用來給Client提供匿名Binder的選擇,Client可以根據自己的需要選擇獲取Server端對應的那個匿名Binder使用用這個匿名Binder代理服務)。
下圖展示了參與Binder通信的所有角色,將在以后章節中一一提到。
4 Binder協議
Binder協議基本格式是(命令+數據),使用ioctl(fd,cmd,arg)函數實現交互。參數cmd是命令,參數arg時數據,arg隨cmd不同而不同。下表列舉了所有命令及其所對應的數據:
表 2 Binder通信命令字
cmd | 含義 | 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驅動,確切地說是向Binder驅動為Server中Binder實體創建的位于Binder驅動的入口節點)寫入數據,一部分是要從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會因為某種原因銷毀這個實體。 | unit32 *ptr; 需要得到銷毀通知的Binder引用 void **cookie; 與銷毀通知相關的消息,驅動會在發出銷毀通知時返回給發出請求的進程。 |
BC_DEAD_BINDER_DONE | 收到Binder實體銷毀通知的進程刪除引用后用本命令告知驅動。 | 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+數據)形式,并且多條消息可以連續存放。下表列舉了讀出的消息及其相應的參數:
表 4 binder讀操作消息ID
消息 | 含義 | arg |
---|---|---|
BR_ERROR | 發生內部錯誤(如內存分配失敗) | |
BR_OK BR_NOOP |
操作完成 | |
BR_SPAWN_LOOPER | 該消息用于接收方線程池管理,當驅動發現接收方所有線程都處于忙碌狀態且線程池里的線程總數沒有超過BINDER_SET_MAX_THREADS設置的最大線程數時,向接收方發送該命令要求創建更多的線程以備接收數據。 | |
BR_TRANSACTION BR_REPLY |
這對消息分別對應發送方的BC_TRANSACTION和BC_REPLY,表示當前接收的數據是請求還是回復。 | struct binder_transaction_data |
BR_ACQUIRE_RESULT BR_ATTEMPT_ACQUIRE BR_FINISHED |
尚未實現 | |
BR_DEAD_BINDER BR_CLEAR_DEATH_NOTIFICATION_DONE |
向獲得Binder引用的進程發送Binder實體銷毀通知;收到銷毀通知的進程接下來會返回BC_DEAD_BINDER_DONE。 | void **cookie;在使用BC_REQUEST_DEATH_NOTIFICATION注冊銷毀通知時的附加信息。 |
BR_FAILED_REPLY | 如果發送非法引用號則返回該消息 |
和寫數據一樣,其中最重要的消息是BR_TRASACTION/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實體的引用,存放在handle中。如前所述, Binder的引用在代碼中也叫做句柄(handle)。數據包經過驅動時, 驅動會根據handle找到對應的Binder實體也就是Binder對象內存的指針, 存入ptr中,該指針是接收方在將Binder實體傳輸給其它進程時(可以是 向SMgr注冊Binder,也可以是接收雙方通過已建立的連接傳遞Binder) 提交給驅動的,驅動程序能夠將發送方填入的引用轉換成接收方Binder 對象的指針,所以接收方可以直接將其當做對象指針來使用(通常將其 reinterpret_cast成相應類)。 |
void *cookie; | 發送方忽略該成員;接收方收到數據包時,該成員存放的是創建Binder 實體時由該接收方自定義的任意數值,作為與Binder指針相關的額外信 息存放在驅動中,驅動基本上不關心該成員。 |
unsigned int code; | 該成員存放收發雙方約定的命令碼,通常是Server端定義的公共接口函 數的編碼,Server就是用這個編碼確定Client調用的是哪個方法的,驅 動完全不關心該成員的內容。 |
unsigned int flags; | 與交互相關標志位,其中最重要的是 TF_ONE_WAY 位,如果該位為1 表明這次交互是異步的,Client端不關心方法的執行以及執行后的返回值 ,發送線程也不會被阻塞,驅動則不會構建與返回值相關的數據結構, 所以Server端的方法即便有返回值也不會被傳遞到Client端。另一位 TF_ACCEPT_FDS 是出于安全考慮,如果發起請求的一方不希望收到 回復中有文件形式的Binder則可將該位置為1,因為收到一個文件形式的 Binder會自動為接收方打開一個文件,使用該位可以防止打開過多文件。 |
pid_t sender_pid; uid_t sender_uid; |
這兩個成員分別存放發送方的進程ID PID和用戶ID UID,接收方可以通過 讀取該成員獲取發送發的身份,這兩個成員是由驅動填寫的,以防止應用 程序通過篡改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; ??unit8_t buf[8]; } data; |
用于存放發送或者接收到的數據,data.offsets 中的元素指向Binder在 buffer內存中的偏移位,該數據組可以位于 data.buffer 中,也可以在另 外的內存空間中,并無限制。buf[8]是為了保證無論是32位機還是64位 機,成員 data 的大小都是8個字節。 |
這里必須再強調一下 offsets_size 和 data.offsets 兩個成員,這是Binder通信有別于其它IPC的地方。如前所述,Binder采用面向對象的設計思路,一個Binder實體的指針可以發送給其它進程從而建立許多跨進程的引用,另外,這些引用也可以在進程之間傳遞,就像Java里將一個引用賦給另一個引用一樣。為Binder在不同進程建立引用必須有驅動參與,由驅動在內核創建并注冊相關的數據結構后接收方才能使用該引用。而且這些引用可以是強類型的,需要驅動為其維護引用計數。然而這些跨進程的Binder混雜在應用程序發送的數據包中,數據格式由用戶定義,如果不把他們一一標示出來告知驅動,驅動將無法從數據中將他們提取出來。于是就使用數組 data.offsets 存放用戶數據中每個Binde在 data.buffer 中的偏移量,用 offset_size 標示這個數組的長度。驅動在發送數據包時會根據 data.offsets 和 offsets_size 將散落在data.buffer 中的Binder找出來并一一為他們創建相關的數據結構。在數據包中傳輸的Binder是類型為 struct flat_binder_object 的結構體,詳見后文。
對于接收方來書,binder_transaction_data 只相當于一個定長的信息頭,真正的用戶數據存放在 data.buffer 所指向的緩存區中。如果發送方在數據中內嵌了一個或多個Binder,接收到的數據包中同樣會用 data.offsets 和 offsets_size 指出每個Binder的位置和總數,不過通常接收方可以忽略這些信息,因為接收方是知道數據格式的(注:序列化與反序列化),參考雙方的格式定義就能知道Binder在什么位置。
5 Binder 的表述
考察一次Binder通信的全過程會發現,Binder會出現在系統的以下幾部分中:
- 應用程序進程:分別位于Server進程和Client進程中;
- 傳輸數據:由于Binder可以跨進程傳遞,需要在傳輸數據中予以表述;
- Binder驅動:分別管理Server端的Binder實體和Client端的Binder引用。
在系統的不同部分,Binder實現的功能不同,表現形式也不一樣,都要一個數據結構來承載,或者是object或者是struct。接下來逐一介紹Binder在各部分所扮演的角色和使用的數據結構。
5.1 Binder在應用程序中的表述
雖然Binder用到了面向對象的思想,但并不限制應用程序一定要使用面向對象語言,無論是C語言還是C++語言都可以很容易的使用Binder來通信。例如盡管Android主要使用Java/C++,像SMgr這么重要的進程就是使用C語言實現的。不過面向對象的表述方式更方便,所以本文假設應用程序是用面向對象語言實現的。
Binder本質上只是一種通信方式,和具體服務沒有關系。為了提供具體服務,Server必須提供一套接口函數以便Client通過遠程調用方式使用各種服務。這時通常采用Proxy設計模式:將接口函數定義在一個抽象類中,Server和Client都會以該抽象類為基類實現所有接口函數,所不同的是Server端是真正功能實現類,而Client只是對這些函數的遠程調用請求的包裝。如何將Binder和Proxy設計模式結合起來是應用程序實現面向對象Binder通信需要解決的根本問題。
(注:研究Binder在Server/Client端的表述最好的辦法還是研究由IDE根據AIDL文件生成的 .java類,研究這個類還可以幫助我們理解Binder在Server/Client端的工作過程。)
5.1.1 Binder在Server端的表述——Binder實體
作為Proxy設計模式的基礎,首先定義一個接口類封裝Server所有功能,其中包含一系列純虛函數留待Server和Proxy各自實現。定義了接口類后就要引入Binder了。為Server端定義一個Binder抽象類處理來自Client的Binder請求數據包。由于上述接口中的那些函數需要跨進程調用,所以需要為其一一編號,以便Server可以根據收到的編號決定調用哪個函數。Binder抽象類中最重要的成員是虛函數onTransact(),該函數分析收到的數據包,調用相應的接口函數處理請求。其次是asBinder(),這個方法用于返回Server端的Binder引用,如果Client與Server在相同進程直接返回Server端的引用,如果在不同進程則返回Server端引用的代理。
onTransact()方法需要特別介紹一下,方法的原型是public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
這是個參數都源于驅動對binder_transaction_data數據包的解析,code就是上面所講的編號,用來區分Client端調用的是Server端的哪個方法,數值由雙方約定;data是Client需要傳遞給Server的數據(如果有的話);reply是Server端方法執行完成后的返回值(如果有的話);flags是一個標記位,在 4.3一節介紹binder_transaction_data包結構時有介紹。在onTransact()方法中會 case-by-case 的比對code,發現與code匹配的方法時就會調用Server端的這個方法并傳入由data反序列化得到的參數,onTransact()會接收Server端方法執行完成后的返回值并在序列化之后存入reply中。
那么各個Binder實體的onTransact()方法又是什么時候調用的呢?這就需要驅動參與了。前面說過,Binder實體需要以Binder傳輸結構flat_binder_object形式發送給其它進程才能建立Binder通信,而Binder實體指針就存放在該結構體的handle域中。驅動根據Binder位置數組從傳輸數據中獲取該Binder的傳輸結構,為它創建位于內核中的Binder節點,將Binder實體指針記錄在該節點中(注:這里也可以提前預知,驅動中的“Binder實體”、“節點”、“實體節點”都是指這個Server端Binder實體的指針)。如果接下來有其他進程向該Binder發送數據,驅動會根據節點記錄的信息將Binder實體指針填入 binder_transaction_data 的 target.ptr 域中發送給接收進程,接收進程從數據包中取出該指針,reinterpret_cast(C++提供的類型轉化運算符)成Binder抽象類型的引用并調用onTransact()方法,由于這是個虛函數,不同的Binder實現類有各自的實現,所以可以調用到不同Binder實體提供的onTransact()方法。
5.1.2 Binder在Client端的表述-Binder引用
作為Proxy設計模式的一部分,Client端的Binder同樣要實現Server端提供的公共接口類并實現接口方法,但這并不是真正的實現,而是對遠程方法的包裝:將方法參數序列化打包,再將打包好的數據通過Binder引用發送給Server端并等待返回值(如果是同步方法且有返回值的話)。為此Client端還要知道Server端Binder實體的相關信息,即對Binder實體的引用。該引用由SMgr返回或者從以建立的鏈接中索取(匿名Binder)。
由于實現了同樣的公共接口,Client Binder提供了與Server Binder一樣的函數原型(注:這里需要特別注意,Client端的Binder并沒繼承android.os.Binder類,它只是實現了根據Server對外提供的服務而定義的公共接口,所以確切地說Client端這個類并不是Binder類,作者為了行文方便使用Client Binder這個名詞不可以理解為Client端也實現了一個Binder類,而Server Binder確實繼承自android.os.Binder它是一個名副其實的Binder類),使用戶感覺不出Server是運行在本地還是遠端。Client Binder中公共接口方法的包裝方式是:調用 transact() 方法,傳入code、data、reply以及flags,如果是遠程調用方法會進入Binder驅動中,驅動會創建一個 binder_transaction_data 數據包,將調用該方法所需的參數填入 data.buffer指向的緩存中并指明數據包的目的地,那就是Client已經獲得的對Server端Binder實體的引用,填入數據包的 taget.handle 中,注意這里和Server的區別:target 域有兩個成員 ptr 和 handle,前者指針指向的是Server端Binder實體所在的內存空間,Server通過它就可以找到Binder實體然后執行遠程需要調用的方法;后者存放Client端持有的Binder引用,用于告知驅動將數據包路由給哪個Binder實體。數據包準備好后驅動就可以根據 taget.handle 將數據包發送至Server端,經過 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,詳見 5.2.1節。 |
unsigned long flags | 該域只對第一次傳遞Binder實體時有效,因為此時驅動需要在內核中創建相應的實體節點 ,有些參數需要從該域取出: 第0-7位,表示處理本實體請求數據包的線程的最低優先級,當一個應用程序提供多個實體 時,可以通過該參數調整分配給各個實體的處理能力; 第8位置1表示該實體可以接收其他線程發過來的文件形式的Binder,由于接收文件形式的 Binder會在接收方進程中自動打開文件,有些接收方可以用該位標示禁止此功能,以防止 打開過多文件。 |
union { ??void *binder; ??signed long handle; }; |
當傳遞的是Binder實體時使用binder域,指向Binder實體在應用程序中的地址; 當傳遞的是Binder引用時使用handle域,存放Binder在進程中的引用號(注意 這里不是指針,而是 signed long型,說明這是個編號,所以作者用“引用號”來表述)。 |
void *cookie | 該域只對Binder實體有效,存放與該實體有關的附加信息。 |
無論是Binder實體還是對實體的引用都從屬于某個進程,所以該結構不能透明地在進程間傳遞,必須經過驅動翻譯。例如當Server想把Binder實體傳遞給Client時,在發送數據流中,flat_binder_object 中的 type 是 BINDER_TYPE_BINDER,binder域指向Server進程用戶空間地址,對Client來說是毫無用處的(進程隔離),驅動必須對數據流中的這個Binder做修改:先將 type 改為 BINDER_TYPE_HANDLE,然后為這個Binder的接收進程創建位于內核中的引用并將引用號填入handle中。
這樣做也是出于安全考慮的:應用程序不能通過編造引用的方式向Server請求服務了。因為引用是由內核創建且在內核中是有記錄的,用戶猜測的引用在內核中是找不到的,必定會被驅動拒絕。唯有經過身份認證確認合法的、由Binder驅動親自授予的Binder引用才能使用。
下表總結了當 flat_binder_object 結構穿過驅動時驅動所做的操作:
表 7 驅動對 flat_binder_object 的操作
Binder類型(type域) | 對發送方數據的操作 | 對接收方數據的操作 |
---|---|---|
BINDER_TYPE_BINDER BINDER_TYPE_WEAK_BINDER |
只有發送方所在進程才能發送該類型的Binder。如果是第一次發送,驅動將為實體創建位于內核的節點并保存binder、cookie、flags域 | 如果是第一次接收該Binder則創建實體在內核中的引用;將handle域替換為新建的引用;將type替換為BINDER_TYPE_(WEAK_)HANDLE。 |
BINDER_TYPE_HANDLE BINDER_TYPE_WEAK_HANDLE |
獲得Binder引用的進程都能發送該類型的引用。驅動根據handle域在內核以注冊的引用中檢索,如果找到了說明合法,否則拒絕請求。 | 如果引用對應的Binder實體位于接收進程則將binder域替換為保存在節點中的binder值;cookie、flags也替換為節點中保存的;type替換為BINDER_TYPE_(WEAK_)BINDER。 如果Binder實體不在接收進程并且是第一次接收則在內核中新建一個引用并填入handle中。 |
BINDER_TYPE_FD | 驗證handle域中提供的文件號是否有效,無效則拒絕發送。 | 在接受方創建一個打開文件號將它與發送端提供的打開文件描述結構綁定。- |
(注:如何判斷Binder引用對應的Binder實體是否在接收進程中將在 5.3節中介紹。)
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.2.2 內存共享與Binder (注:這一節為補充內容)
由于需要傳輸的數據需要先拷貝到內核空間,所以Binder傳輸數據的大小是有限制的,每個進程分配的空間不足1M:
#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
但是實際使用時限制會更小,因為Binder通信是可以并發的,多個線程共用這個不足1M的空間,所以分配給每個線程的空間會更小且不確定,一旦傳輸的數據超出了剩余空間大小系統就會拋出 android.os.TransactionTooLargeException 異常。所以當需要傳輸較大數據時(比如傳遞一個較大的Bitmap)可以考慮使用共享內存的方式。
下面給出示例代碼:
發送方:
try {
// MemoryFile是對native的封裝,構造函數的第一個參數是虛擬文件的名字可以為null,第二個參數是文件的大小
MemoryFile memoryFile = new MemoryFile("share_memory", 1024);
memoryFile.getOutputStream().write(new String("hello share memory").getBytes());
// 由于MemoryFile的getFileDescriptor方法是@hide的,所以需要用反射的方式獲得FileDescriptor
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor fd = (FileDescriptor) method.invoke(memoryFile);
memoryFile.getOutputStream().close();
// mBinder是已建立的Binder連接;shareMemory是在AIDL接口中定義的用于傳遞ParcelFileDescriptor的方法
// 使用dup方法是為了將FileDescriptor序列化
mBinder.shareMemory(ParcelFileDescriptor.dup(fd));
} catch (IOException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
接收方:
// sf 是發送方穿過來的ParcelFileDescriptor
FileDescriptor fileDescriptor = sf.getFileDescriptor();
FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
byte[] buf = new byte[32];
try {
fileInputStream.read(buf);
fileInputStream.close();
Log.d("shareMemory", new String(buf));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
由示例可以看出用Binder共享內存與共享文件使用的方法類似,二者都是通過文件描述符操作數據的,不同點在于共享內存方式傳遞的文件描述符是通過MemoryFile得到的。MemoryFile是對底層ashmem(Anonymous Shared Memory匿名共享內存)的封裝,ashmem開辟一段共享內存后使用mmap建立用戶空間與共享內存的映射并返回與映射關聯的文件描述符,通信雙方就可以通過對這個文件描述符的讀寫完成數據傳遞了。
5.3 Binder在驅動中的表述
驅動是Binder通信的核心,系統中所有Binder實體及每個實體在各個進程中的引用都登記在驅動中;驅動需要記錄Binder引用和實體間多對一的關系;為Binder實體創建位于內核空間的節點;記錄Binder實體位于哪個進程中;為引用找到對應的實體;為Binder實體創建或查找對應的引用;通過管理Binder的強/弱引用創建/銷毀Binder實體等等。
驅動里的Binder(實體節點和引用)是什么時候創建的呢?前面提到過,為了實現實名Binder的注冊,系統必須創建SMgr用于注冊實名Binder的Binder實體,負責實名Binder注冊過程中的進程間通信。既然創建了實體就要有對應的引用:驅動將所有進程中的0號引用都預留給SMgr的Binder實體,無須特殊操作即可使用0號引用注冊實名Binder。接下來隨著應用程序不斷地注冊實名Binder,不斷向SMgr索要Binder引用,不斷將Binder引用從一個進程傳遞給另一個進程,越來越多的Binder以 flat_binder_object 結構穿越驅動做跨進程的遷徙。由于 binder_transaction_data 中 data.offset 數組的存在,所有流經驅動的Binder都逃不過驅動的眼睛。Binder將對這些穿越進程邊界的Binder進行操作,操作詳解請見 表7。隨著越來越多的Binder實體或引用在進程間傳遞,驅動會在內核中創建越來越多的節點或引用。
5.3.1 Binder實體在驅動中的表述
驅動中的Binder實體也叫節點,隸屬于提供實體的進程,由 struct binder_node表示。
表 8 Binder節點描述結構:binder_node
成員 | 含義 |
---|---|
int debug_id; | 用于調試 |
struct binder_work work; | 當節點引用數發生變化,需要通知節點所屬進程,將該成員加入所屬進程的 todo隊列里,喚醒所屬進程執行引用計數修改。 |
union { ??struct rb_node rb_node; ??struct hlist_node dead_node; }; |
驅動為每個進程都維護一顆紅黑樹,用于根據Binder實體在用戶空間的指針 查找節點,rb_node是紅黑樹上的一個節點。 Binder實體已銷毀,如果還有引用沒有切斷,就用dead_node將該節點放到 另一個哈西表中。 |
struct binder_proc *proc; | 指向所屬的進程,即提供該節點的Binder實體所屬進程。 |
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_eak_ref; |
這組標志用于控制驅動與Binder實體艘在進程交互式修改引用計數。 |
unsigned has_async_transaction; | 該成員表明該節點的todo隊列中有異步交互尚未完成。驅動將所有發往 接收端的數據包暫存在接收進程開辟的todo隊列中。對于異步交互驅動 做了適當的流控:如果todo隊列里有異步交互尚待處理則該成員置1, 這將導致新發送來的異步交互被存放在本結構的 asynch_todo隊列中, 而不直接發送到todo隊列,目的是為同步交互讓路,避免長時間阻塞 同步交互的發送端。(注:引入這個標志位也是為了讓異步交互得以執行 ,因為異步交互要執行就必然要被放到todo隊列中。) |
unsigned accept_fds; | 表明節點是否同意接收文件形式的Binder,來自flat_binder_object的flags 成員的FLAT_BINDER_FLAG_ACCEPTS_FDS位。由于接收文件Binder會 為進程自動打開一個文件,占用有限的文件描述符(一般是1024個), 節點可以根據該成員的值判斷否以拒絕這種行為。 |
int min_priority | 設置處理Binder請求的線程的最低優先級,發送線程將數據提交給接收線程 處理時,驅動會將發送線程的優先級也賦予給接收線程,使得數據即使跨 進程也能以同樣優先級得到處理。不過如果發送線程優先級過低,接收線程 將以預設的這個最小值運行。該域的值來自于flat_binder_object的flags成員。 |
struct list_head async_todo | 異步交互等待隊列,用于分流。 |
(注:內核為每個進程創建了一個描述Binder上下文信息的結構體binder_proc存放諸如:用于處理用戶請求的線程所構成的紅黑樹-threads;binder實體紅黑樹-nodes;binder引用紅黑樹-refs_by_node;進程映射的物理內存在內核空間的起始位置-*buffer;該進程的待處理事件隊列-todo;等待隊列-wait,以及最大線程數-max_thread等信息。struct rb_root nodes;
就是其中用于存儲Binder實體節點binder_node的紅黑樹的根節點,struct rb_root refs_by_node;
就是維護下一節要介紹的的用于描述Binder引用的結構體binder_ref的紅黑樹的根節點。binder_proc詳解)
每個進程都有一棵紅黑樹用于存放創建好的節點。每當驅動在傳輸數據中偵測到一個代表Binder實體的flat_binder_object,先以該結構體中的binder域為索引在紅黑樹上查找有沒有與哪個節點的ptr域相同,如果沒有就創建一個新的節點并添加到樹上。由于同一個進程的內存地址是唯一的,所以不會因為重復創建造成混亂。
5.3.2 Binder引用在驅動中的表述
和實體一樣,Binder的引用也是驅動根據傳輸數據中的flat_binder_object創建的,隸屬于獲得該引用的進程,用 struct binder_ref 表示:
表 9 Binder引用描述結構體:binder_ref
成員 | 含義 |
---|---|
int debug_id; | 用于調試 |
struct rb_node rb_node_desc; | 用于關聯到refs_by_desc紅黑樹中;這棵樹用引用號排序。 |
struct rb_node rb_node_node; | 用于關聯到refs_by_node紅黑樹中;這棵樹用Binder實體地址排序。 |
struct hlist_node node_entry; | 該域將本引用作為節點鏈入binder_node中的refs隊列。 |
struct binder_proc *proc; | 本引用所屬進程。 |
struct binder_node *node; | 本引用指向Binder實體在驅動中的節點,binder_node |
unit32_t desc; | 本引用的引用號 |
int strong; | 強引用計數。 |
int weak; | 弱引用計數。 |
struct binder_ref_death *death; | 應用程序向驅動發送BC_REQUEST_DEATH_NOTIFICATION或 BC_CLEAR_DEATH_NOTIFICATION命令從而能在Binder實體 銷毀時收到來自驅動的提醒。該域不為空表明用戶訂閱了對應實體 銷毀的通知。 |
就像一個對象有很多指針一樣,同一個Binder實體可以有很多引用,不同的是這些引用分布在不同進程。Client端想通過自己持有的Binder引用發起遠程調用時,驅動會先檢索Client進程的binder_proc.refs_by_desc樹中查找是否存在這個引用,如果不存在說明請求非法將被拒絕,如果存在則根據引用的binder_ref.node指針找到它所指向的Binder實體節點binder_node,得到的binder_node的ptr域存放的就Binder實體在其用戶空間(Server)的指針了;與之相對的,Server端接到Client端發來的數據包后會根據binder_transaction_data.code判斷Client調用的是哪個方法,待方法執行完后再取出包內的引用號根據引用號,根據引用號在內核中Binder實體節點binder_node的鏈表refs中找到對應的引用,將需要返回的數據填入reply中發送給驅動,驅動再通過引用轉發給Client,經過這個流程就完成了一次遠程調用。
7 Binder接收線程管理
Binder通信實際上是位于不同進程中的線程之間的通信。假定進程S是Server端,提供Binder實體,線程c1從Client進程C中通過Binder引用向進程S發送請求,S為了處理這個請求需要啟動線程s1,而此時線程c1處于接收返回數據的等待狀態。s1處理完請求就會將處理結果返回給c1,c1被喚醒并得到處理結果。在這個過程中,s1仿佛c1在進程S中的代理,代表c1執行遠程服務,而給c1的感覺就像是穿越到進程S中執行了一段代碼又回到了進程C。為了使這種穿越更加真實,驅動會將c1的一些屬性賦給s1,特別是c1的線程優先級,這樣s1會使用和c1類似的時間完成任務。很多資料會用“線程遷移”來形容這種現象,容易讓人產生誤解,一來線程根本不可能在進程之間跳來跳去,二來s1除了和c1優先級一樣,其他沒有相同之處,包括身份、打開文件、棧大小、信號處理和私有數據等。
由于Server進程S可能會有許多Client同時發起請求,為了提高效率往往會開辟線程池并發處理收到的請求。怎樣使用線程池處理并發請求呢?這和具體的IPC機制有關。拿socket舉例,Server端的socket設置為偵聽模式,有一個專門的線程使用該socket偵聽來自Client的連接請求,該線程阻塞在accept()方法上,這個socket就像一只會下蛋的雞,一旦收到來自Client的請求就會生一個蛋——創建新的socket并通過accept()返回給監聽線程,偵聽線程從線程池中啟動一個工作線程并將剛創建的socket交給線程,后續業務處理就由該線程完成并通過這個socket與Client實現交互。
可是對于Binder來書,既沒有偵聽模式也不會下蛋,那它怎么管理線程池呢?一種簡單的做法是,不管三七二十一,先創建一堆線程,每個線程都用BINDER_WRITE_READ命令讀Binder傳來的數據。這些線程會阻塞在驅動為該Binder設置的等待隊列(todo)上,一旦有來自Client的數據驅動就會從隊列中喚醒一個線程來處理請求。這樣做簡單直觀但一開始就創建一堆線程有點浪費資源,于是Binder協議引入了專門命令或消息幫助用戶管理線程池,包括:
- BINDER_SET_MAX_THREADS
- BC_REGISTER_LOOPER
- BC_ENTER_LOOPER
- BC_EXIT_LOOPER
- BC_SPAWN_LOOPER
首先要管理線程池就要知道線程池有多大,應用程序通過BINDER_SET_MAX_THREADS告知驅動最多可以創建多少線程,之后每個線程在創建、進入主循環、退出主循環分別使用BC_REGISTER_LOOPER、BC_ENTER_LOOPER和BC_EXIT_LOOPER。每當驅動接收完數據包返回給讀Binder的線程時都要檢查一下是不是已經沒有閑置的線程了,如果沒有閑置線程了且線程總數沒超出線程池最大值就會在當前讀出的數據后面追加一條BR_SPAWN_LOOPER消息,告訴用戶線程不夠用了再啟動一些。線程已啟動又會發送BC_REGISTER_LOOPER消息告知驅動更新狀態。這樣只要線程沒有耗盡,總是有空閑線程在等待隊列中隨時待命,計時處理請求。
關于工作線程的啟動,Binder驅動還做了一點有趣的優化。當進程P1的線程T1向進程P2發起請求時,驅動會先查看一下P2是否存在某個線程正在等待發往進程P1的請求。這種情況通常發生在兩個進程都有Binder實體并互相調用時。假如驅動在進程P2中發現了這樣的線程T2,就會要求T2來處理T1的這次請求。因為T2既然向T1發起了請求且尚未完成,說明T2肯定(或將會)阻塞在讀取返回包的狀態。這是可以讓T2順便做點事情,總比在那里閑著好。而且如果T2不是線程池中的線程還可以為線程池分擔工作,減少線程池使用率。
8 數據包接收隊列與(線程)等待隊列管理
通常數據傳輸的接收端有兩個隊列:數據包接收隊列和(線程)等待隊列,用以緩解供需矛盾。就像當超市里的進的貨(數據包)太多,貨物會先堆積在倉庫里;購物的人(線程)太多,會排隊等待在收銀臺,道理是一樣的。在驅動中,每個進程都有一個全局的接收隊列——todo,存放不是發往特定線程的數據包;相應的有一個全局等待隊列,線程在等待隊列里等待接收數據。每個線程有自己的todo隊列,存放發送給該線程的數據包;相應的每個線程都有各自私有的等待隊列,專門用于本線程等待接收自己todo隊列里的數據,雖然名為隊列,其實只有自己在等待。
由于發送是沒有特別標記,驅動怎么判斷哪些數據該進入全局todo隊列,哪些該送入特定線程的todo隊列呢?這里有兩個規則,規則1:Client發給Server的請求數據包都提交到Server進程的全局todo隊列,不過有個特例,就是上一節談到的Binder對工作線程啟動的優化,經過優化,來自T1的請求不是提交給P2的全局todo隊列,而是送入T2的私有todo隊列;規則2:對同步請求的返回數據包(由BC_REPLY發送的包)都發送到發起請求的線程的私有todo隊列中。如上面的例子,如果進程P1的線程T1發給進程P2的線程T2的是同步請求,那么T2返回的數據包將送進T1的私有todo隊列而不是提交到P1的全局todo隊列。
數據包進入接收隊列的規則也就決定了線程進入等待隊列的規則,即一個線程只要不接收返回數據包則應該在全局等待隊列中等待新任務,否則就應該在其私有的等待隊列中等待Server的返回數據包。
這些規則是驅動對Binder通信雙方的限制條件,體現在應用程序上就是同步請求交互過程的一致性:1)Client端,等待返回數據包的線程必須是發送請求的線程,而不是由一個線程發送請求包,另一個線程等待接收數據包,否則將收不到返回數據包;2)Server端,發送對應返回數據包的線程必須是收到請求數據包的線程,否則返回的數據包將無法送交發送請求的線程。這是因為返回數據包的目的Binder不是發送返回數據包的進程指定的而是驅動記錄在收到請求數據包的線程里,如果發送返回包的線程不是收到請求包的線程驅動將無從知曉返回包將送往何處。
接下來探討一下Binder驅動是如何遞交同步交互和異步交互的。我們知道,同步交互和異步交互的區別是同步交互的請求端(Client)在發出請求數據包后需要等待應答端(Server)的返回數據包,而異步交互的發送端在發送請求數據包后交互即結束發送線程不會被阻塞。對于這兩種交互的請求數據包驅動可以不做區分統統丟到接收端的todo隊列中一個一個處理,但是驅動沒有這樣做,而是對異步交互做了限流,另其為同步交互讓路。具體做法是:對于某個Binder實體,只要接收端todo隊列中有一個異步交互沒有處理完畢,那么接下來所有發給該實體的異步交互將不再投遞到todo隊列中,而是投遞到另一條用于處理異步交互的隊列async_todo中,但這期間同步交互依然不受限制的直接進入todo隊列,一直到todo隊列中的異步交互執行完畢,下一個異步交互才能脫離async_todo隊列進入todo隊列。之所以這么做是因為同步交互的請求端需要等待返回包,必須迅速處理完以免影響請求端的響應速度,而異步交互屬于“發射后不管”,稍微延遲一會兒不會有什么影響,所以用async_todo隊列將除已經在todo隊列內的那個異步交互以外的異步交互請求暫存起來,以免突發大量異步交互擠占Server端的處理能力或消耗線程池里的線程,進而阻塞同步交互。
9 總結
Binder使用Client-Server通信方式,相比于Linux傳統的IPC方式來說安全性更好,更簡單高效。再加上其面向對象的設計思想,獨特的接收緩存管理和線程池管理方式,成為Android進程間通信的中流砥柱。
10 校注者總結
這篇博客是對原作者博客中存在的一些錯漏進行校正和注解,雖然因為我自身能力的限制不能對所有錯漏一一注解,但力求盡我所能讓原博客的內容更加精到,同時也避免在校注過程中引入其它錯誤。Binder驅動的源碼我只看了一部分,所以后續我還會給這篇博客繼續增加內容,希望對大家都有幫助。
這里為大家再推薦另一篇或者說另一系列的博客Android Binder機制系列第一篇作者是wangkuiwu,他對Binder的講解也很深入用心,大家可以對照兩篇博客一起學習。