Android Binder機制の設(shè)計與實現(xiàn)6-7(Binder 內(nèi)存映射和接收緩存區(qū)管理/Binder 接收線程管理)6 Binder 內(nèi)存映射和接收緩存區(qū)管理暫且撇開Binder,,考慮一下傳統(tǒng)的IPC方式中,,數(shù)據(jù)是怎樣從發(fā)送端到達(dá)接收端的呢?通常的做法是,,發(fā)送方將準(zhǔn)備好的數(shù)據(jù)存放在緩存區(qū)中,,調(diào)用 API通過系統(tǒng)調(diào)用進入內(nèi)核中。內(nèi)核服務(wù)程序在內(nèi)核空間分配內(nèi)存,,將數(shù)據(jù)從發(fā)送方緩存區(qū)復(fù)制到內(nèi)核緩存區(qū)中,。接收方讀數(shù)據(jù)時也要提供一塊緩存區(qū),內(nèi)核將數(shù) 據(jù)從內(nèi)核緩存區(qū)拷貝到接收方提供的緩存區(qū)中并喚醒接收線程,,完成一次數(shù)據(jù)發(fā)送,。這種存儲-轉(zhuǎn)發(fā)機制有兩個缺陷:首先是效率低下,需要做兩次拷貝:用戶空間 ->內(nèi)核空間->用戶空間,。Linux使用copy_from_user()和copy_to_user()實現(xiàn)這兩個跨空間拷貝,,在此過程 中如果使用了高端內(nèi)存(high memory),這種拷貝需要臨時建立/取消頁面映射,,造成性能損失,。其次是接收數(shù)據(jù)的緩存要由接收方提供,,可接收方不知道到底要多大的緩存才夠用,只能 開辟盡量大的空間或先調(diào)用API接收消息頭獲得消息體大小,,再開辟適當(dāng)?shù)目臻g接收消息體,。兩種做法都有不足,不是浪費空間就是浪費時間,。 Binder采用一種全新策略:由Binder驅(qū)動負(fù)責(zé)管理數(shù)據(jù)接收緩存,。我們注意到Binder驅(qū)動實現(xiàn)了mmap()系統(tǒng)調(diào)用,這對字符設(shè)備是 比較特殊的,,因為mmap()通常用在有物理存儲介質(zhì)的文件系統(tǒng)上,,而象Binder這樣沒有物理介質(zhì),純粹用來通信的字符設(shè)備沒必要支持mmap(),。 Binder驅(qū)動當(dāng)然不是為了在物理介質(zhì)和用戶空間做映射,而是用來創(chuàng)建數(shù)據(jù)接收的緩存空間,。先看mmap()是如何使用的: fd = open(“/dev/binder”, O_RDWR); mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0); 這樣Binder的接收方就有了一片大小為MAP_SIZE的接收緩存區(qū),。mmap()的返回值是內(nèi)存映射在用戶空間的地址,不過這段空間是由驅(qū)動 管理,,用戶不必也不能直接訪問(映射類型為PROT_READ,,只讀映射)。 接收緩存區(qū)映射好后就可以做為緩存池接收和存放數(shù)據(jù)了,。前面說過,,接收數(shù)據(jù)包的結(jié)構(gòu)為binder_transaction_data,但這只是消 息頭,,真正的有效負(fù)荷位于data.buffer所指向的內(nèi)存中,。這片內(nèi)存不需要接收方提供,恰恰是來自mmap()映射的這片緩存池,。在數(shù)據(jù)從發(fā)送方向 接收方拷貝時,,驅(qū)動會根據(jù)發(fā)送數(shù)據(jù)包的大小,使用最佳匹配算法從緩存池中找到一塊大小合適的空間,,將數(shù)據(jù)從發(fā)送緩存區(qū)復(fù)制過來,。要注意的是,存放 binder_transaction_data結(jié)構(gòu)本身以及表4中所有消息的內(nèi)存空間還是得由接收者提供,,但這些數(shù)據(jù)大小固定,,數(shù)量也不多,不會給接收 方造成不便,。映射的緩存池要足夠大,,因為接收方的線程池可能會同時處理多條并發(fā)的交互,每條交互都需要從緩存池中獲取目的存儲區(qū),,一旦緩存池耗竭將產(chǎn)生導(dǎo) 致無法預(yù)期的后果,。 有分配必然有釋放,。接收方在處理完數(shù)據(jù)包后,就要通知驅(qū)動釋放data.buffer所指向的內(nèi)存區(qū),。在介紹Binder協(xié)議時已經(jīng)提到,,這是由命 令BC_FREE_BUFFER完成的。 通過上面介紹可以看到,,驅(qū)動為接收方分擔(dān)了最為繁瑣的任務(wù):分配/釋放大小不等,,難以預(yù)測的有效負(fù)荷緩存區(qū),而接收方只需要提供緩存來存放大小固 定,,可以預(yù)測的消息頭即可,。在效率上,由于mmap()分配的內(nèi)存是映射在接收方用戶空間里的,,所有總體效果就相當(dāng)于對有效負(fù)荷數(shù)據(jù)做了一次從發(fā)送方用戶 空間到接收方用戶空間的直接數(shù)據(jù)拷貝,,省去了內(nèi)核中暫存這個步驟,提升了一倍的性能,。順便再提一點,,Linux內(nèi)核實際上沒有從一個用戶空間到另一個用戶 空間直接拷貝的函數(shù),需要先用copy_from_user()拷貝到內(nèi)核空間,,再用copy_to_user()拷貝到另一個用戶空間,。為了實現(xiàn)用戶空 間到用戶空間的拷貝,mmap()分配的內(nèi)存除了映射進了接收方進程里,,還映射進了內(nèi)核空間,。所以調(diào)用copy_from_user()將數(shù)據(jù)拷貝進內(nèi)核 空間也相當(dāng)于拷貝進了接收方的用戶空間,這就是Binder只需一次拷貝的‘秘密’,。
7 Binder 接收線程管理Binder通信實際上是位于不同進程中的線程之間的通信,。假如進程S是Server端,提供Binder實體,,線程T1從Client進程C1中 通過Binder的引用向進程S發(fā)送請求,。S為了處理這個請求需要啟動線程T2,而此時線程T1處于接收返回數(shù)據(jù)的等待狀態(tài),。T2處理完請求就會將處理結(jié) 果返回給T1,,T1被喚醒得到處理結(jié)果。在這過程中,,T2仿佛T1在進程S中的代理,,代表T1執(zhí)行遠(yuǎn)程任務(wù),而給T1的感覺就是象穿越到S中執(zhí)行一段代碼 又回到了C1,。為了使這種穿越更加真實,,驅(qū)動會將T1的一些屬性賦給T2,特別是T1的優(yōu)先級nice,,這樣T2會使用和T1類似的時間完成任務(wù),。很多資 料會用‘線程遷移’來形容這種現(xiàn)象,,容易讓人產(chǎn)生誤解。一來線程根本不可能在進程之間跳來跳去,,二來T2除了和T1優(yōu)先級一樣,,其它沒有相同之處,包括身 份,,打開文件,,棧大小,信號處理,,私有數(shù)據(jù)等,。 對于Server進程S,可能會有許多Client同時發(fā)起請求,,為了提高效率往往開辟線程池并發(fā)處理收到的請求,。怎樣使用線程池實現(xiàn)并發(fā)處理呢? 這和具體的IPC機制有關(guān),。拿socket舉例,,Server端的socket設(shè)置為偵聽模式,有一個專門的線程使用該socket偵聽來自Client 的連接請求,,即阻塞在accept()上。這個socket就象一只會生蛋的雞,,一旦收到來自Client的請求就會生一個蛋 – 創(chuàng)建新socket并從accept()返回,。偵聽線程從線程池中啟動一個工作線程并將剛下的蛋交給該線程。后續(xù)業(yè)務(wù)處理就由該線程完成并通過這個單與 Client實現(xiàn)交互,。 可是對于Binder來說,,既沒有偵聽模式也不會下蛋,怎樣管理線程池呢,?一種簡單的做法是,,不管三七二十一,先創(chuàng)建一堆線程,,每個線程都用 BINDER_WRITE_READ命令讀Binder,。這些線程會阻塞在驅(qū)動為該Binder的等待隊列上,一旦有來自Client的數(shù)據(jù)驅(qū)動會從隊列 中喚醒一個線程來處理,。這樣做簡單直觀,,省去了線程池,但一開始就創(chuàng)建一堆線程有點浪費資源,。于是Binder協(xié)議設(shè)置了專門命令或消息幫助用戶管理線程 池,,包括: · INDER_SET_MAX_THREADS · BC_REGISTER_LOOP · BC_ENTER_LOOP · BC_EXIT_LOOP · BR_SPAWN_LOOPER 首先要管理線程池就要知道池子有多大,應(yīng)用程序通過INDER_SET_MAX_THREADS告訴驅(qū)動最多可以創(chuàng)建幾個線程,。以后每個線程在創(chuàng) 建,,進入主循環(huán),,退出主循環(huán)時都要分別使用BC_REGISTER_LOOP,BC_ENTER_LOOP,,BC_EXIT_LOOP告知驅(qū)動,,以便驅(qū)動 收集和記錄當(dāng)前線程池的狀態(tài)。每當(dāng)驅(qū)動接收完數(shù)據(jù)包返回讀Binder的線程時,,都要檢查一下是不是已經(jīng)沒有閑置線程了,。如果是,而且線程總數(shù)不會超出線 程池最大線程數(shù),,就會在當(dāng)前讀出的數(shù)據(jù)包后面再追加一條BR_SPAWN_LOOPER消息,,告訴用戶線程即將不夠用了,請再啟動一些,,否則下一個請求可 能不能及時響應(yīng),。新線程一啟動又會通過BC_xxx_LOOP告知驅(qū)動更新狀態(tài)。這樣只要線程沒有耗盡,,總是有空閑線程在等待隊列中隨時待命,,及時處理請 求。 關(guān)于工作線程的啟動,,Binder驅(qū)動還做了一點小小的優(yōu)化,。當(dāng)進程P1的線程T1向進程P2發(fā)送請求時,驅(qū)動會先查看一下線程T1是否也正在處理 來自P2某個線程請求但尚未完成(沒有發(fā)送回復(fù)),。這種情況通常發(fā)生在兩個進程都有Binder實體并互相對發(fā)時請求時,。假如驅(qū)動在進程P2中發(fā)現(xiàn)了這樣 的線程,比如說T2,,就會要求T2來處理T1的這次請求,。因為T2既然向T1發(fā)送了請求尚未得到返回包,說明T2肯定(或?qū)┳枞谧x取返回包的狀態(tài),。 這時候可以讓T2順便做點事情,,總比等在那里閑著好。而且如果T2不是線程池中的線程還可以為線程池分擔(dān)部分工作,,減少線程池使用率,。 |
|