理解Android Binder机制原理
原文地址:?http://blog.csdn.net/universus/article/details/6211589
?
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進程間通信的中流砥柱。
總結
以上是生活随笔為你收集整理的理解Android Binder机制原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软考-架构师-第六章-开发方法 第二节
- 下一篇: Faster R-CNN算法