Android Binder設計與實現 – 設計篇

摘要

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 數據拷貝次數
共享內存 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數據包實例


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章