Android Binder設計與實現(3) – 設計篇

來源:互聯網
上載者:User

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優先順序一樣,其它沒有相同之處,包括身份,開啟檔案,棧大小,訊號處理,私人資料等。

       對於Server進程S,可能會有許多Client同時發起請求,為了提高效率往往開闢線程池並發處理收到的請求。怎樣使用線程池實現並發處理呢? 這和具體的IPC機制有關。拿socket舉例,Server端的socket設定為偵聽模式,有一個專門的線程使用該socket偵聽來自Client 的串連請求,即阻塞在accept()上。這個socket就象一隻會生蛋的雞,一旦收到來自Client的請求就會生一個蛋 – 建立新socket並從accept()返回。偵聽線程從線程池中啟動一個背景工作執行緒並將剛下的蛋交給該線程。後續業務處理就由該線程完成並通過這個單與
Client實現互動。

       可是對於Binder來說,既沒有偵聽模式也不會下蛋,怎樣管理線程池呢?一種簡單的做法是,不管三七二十一,先建立一堆線程,每個線程都用 BINDER_WRITE_READ命令讀Binder。這些線程會阻塞在驅動為該Binder的等待隊列上,一旦有來自Client的資料驅動會從隊列中喚醒一個線程來處理。這樣做簡單直觀,省去了線程池,但一開始就建立一堆線程有點浪費資源。於是Binder通訊協定設定了專門命令或訊息協助使用者管理線程
池,包括:

· BINDER_SET_MAX_THREADS

· BC_REGISTER_LOOP

· BC_ENTER_LOOP

· BC_EXIT_LOOP

· BR_SPAWN_LOOPER

     首先要管理線程池就要知道池子有多大,應用程式通過BINDER_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不是線程池中的線程還可以為線程池分擔部分工作,減少線程池使用率。

 

8. 資料包接收隊列與(線程)等待隊列管理

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

9 總結
       Binder使用Client-Server通訊方式,安全性好,簡單高效,再加上其物件導向的設計思想,獨特的接收緩衝管理和線程池管理方式,成為Android處理序間通訊的中流砥柱。

原文作者:universus

 

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.