1. 程式人生 > >Android Binder機制原理

Android Binder機制原理

Binder是Android系統程序間通訊(IPC)方式之一。Linux已經擁有的程序間通訊IPC手段包括(Internet Process Connection): 管道(Pipe)、訊號(Signal)和跟蹤(Trace)、插口(Socket)、報文佇列(Message)、共享記憶體(Share Memory)和訊號量(Semaphore)。本文詳細介紹Binder作為Android主要IPC方式的優勢。

一、引言

基於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

還有一點是出於安全性考慮。終端使用者不希望從網上下載的程式在不知情的情況下偷窺隱私資料,連線無線網路,長期操作底層裝置導致電池很快耗盡等等。傳統IPC沒有任何安全措施,完全依賴上層協議來確保。首先傳統IPC的接收方無法獲得對方程序可靠的UID和PID(使用者ID程序ID),從而無法鑑別對方身份。Android為每個安裝好的應用程式分配了自己的UID,故程序的UID是鑑別程序身份的重要標誌。使用傳統IPC只能由使用者在資料包裡填入UID和PID,但這樣不可靠,容易被惡意程式利用。可靠的身份標記只有由IPC機制本身在核心中新增。其次傳統IPC訪問接入點是開放的,無法建立私有通道。比如命名管道的名稱,systemV的鍵值,socket的ip地址或檔名都是開放的,只要知道這些接入點的程式都可以和對端建立連線,不管怎樣都無法阻止惡意程式通過猜測接收方地址獲得連線。

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

二、面向物件的 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驅動為面向物件的程序間通訊提供底層支援。

三、Binder 通訊模型

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

3.1 Binder 驅動

和路由器一樣,Binder驅動雖然默默無聞,卻是通訊的核心。儘管名叫‘驅動’,實際上和硬體裝置沒有任何關係,只是實現方式和裝置驅動程式是一樣的。它工作於核心態,驅動負責程序之間Binder通訊的建立,Binder在程序之間的傳遞,Binder引用計數管理,資料包在程序之間的傳遞和互動等一系列底層支援。

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協議(略)

五、Binder表述

六、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只需一次拷貝的‘祕密’。

七、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不是執行緒池中的執行緒還可以為執行緒池分擔部分工作,減少執行緒池使用率。

八、資料包接收佇列與(執行緒)等待佇列管理

通常資料傳輸的接收端有兩個佇列:資料包接收佇列和(執行緒)等待佇列,用以緩解供需矛盾。當超市裡的進貨(資料包)太多,貨物會堆積在倉庫裡;購物的人(執行緒)太多,會排隊等待在收銀臺,道理是一樣的。在驅動中,每個程序有一個全域性的接收佇列,也叫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端的處理能力或耗盡執行緒池裡的執行緒,進而阻塞同步互動。

九、總結

Binder使用Client-Server通訊方式,安全性好,簡單高效,再加上其面向物件的設計思想,獨特的接收快取管理和執行緒池管理方式,成為Android程序間通訊的中流砥柱。