android 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優先級一樣,其它沒有相同之處,包括身 份,打開文件,棧大小,信號處理,www.aiwalls.com私有數據等。

對於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不是線程池中的線程還可以為線程池分擔部分工作,減少線程池使用率。

 

摘自 LuoXianXiong,您的夥伴

發佈留言