中級Android開發應該瞭解的Binder原理

1、基礎概念

Linux的進程空間是相互隔離的。緩存

Linux將內存空間在邏輯上劃分爲內核空間與用戶空間。Linux 操做系統和驅動程序運行在內核空間,應用程序運行在用戶空間,爲了保證內核安全,它們是隔離的。內核空間能夠訪問全部內存空間,而用戶空間不能訪問內核空間。安全

用戶程序只能經過系統調用陷入內核態,從而訪問內核空間。系統調用主要經過 copy_to_user() 和 copy_from_user() 實現,copy_to_user() 用於將數據從內核空間拷貝到用戶空間,copy_from_user() 用於將數據從用戶空間拷貝到內核空間。併發

2、Binder解析

Binder是Android上的一種進程間通訊機制,它基於Client-Server模式實現,由BinderDriver、ServiceManager、Client和Server四個模塊組成。Binder相較於Socket等傳統IPC方式的優點:app

安全性好:爲發送方添加UID/PID身份信息
性能更佳:傳輸過程只要一次數據拷貝,而Socket、管道等傳統IPC手段都至少須要兩次數據拷貝
複製代碼

Binder四大模塊:socket

  • Binder Driver位於內核空間中,主要負責Binder通訊的創建,以及其在進程間的傳遞和Binder引用計數管理/數據包的傳輸等。而Client與Server之間的跨進程通訊則統一經過Binder Driver處理轉發。函數

  • 對於Client來講,只須要知道本身要使用的Binder的名字,而後經過0號引用去訪問ServerManager獲取目標Binder的引用,獲得引用後就能夠像普通方法那樣調用Binder實體的方法。性能

  • Server在生成一個Binder實體的同時會爲其綁定一個別名並將別名傳遞給Binder Driver,Binder Driver接收後若是發現是新增的Binder,那麼就會爲其在內核空間中建立相應的Binder實體節點,而後Binder Driver將該節點的引用傳遞給ServerManager,ServerManager收到後再將該Binder的別名和引用插入到一張數據表中,這跟DNS中存儲的域名到IP地址的映射原理相似。優化

  • ServerManager也是個標準的Server,而且在Android中約定其在Binder通訊的過程當中惟一標識永遠是0,也就是前面提到的0號引用。spa

Android系統啓動過程當中SystemServer會向BinderDriver註冊ServiceManager,BinderDriver自動爲ServiceManager建立Binder實體。全部在這以後啓動的應用進程都會持有這個Binder的句柄,爲0號引用,即全部用戶進程的0號引用都指向該Binder。ActivityManagerService、PackageManagerService等系統服務都是經過Binder機制與應用進行雙向通訊。操作系統

傳統的IPC方式:

* 發送方先將準備好的數據存放在緩存區中
* 而後經過系統調用進入內核中,內核服務程序在內核空間分配內存,將數據從發送方緩存區複製到內核緩存區中。
* 接收方讀數據時也要提供一塊緩存區,內核將數據從內核緩存區拷貝到接收方提供的緩存區中。
* 這種存儲-轉發機制有兩個缺陷:
* 首先是效率低下,須要作兩次拷貝:用戶空間->內核空間->用戶空間。Linux使用copy_from_user()和copy_to_user()實現這兩個跨空間拷貝,在此過程當中若是使用了高端內存(high memory),這種拷貝須要臨時創建/取消頁面映射,形成性能損失。
* 其次是接收數據的緩存要由接收方提供,可接收方不知道到底要多大的緩存纔夠用,只能開闢儘可能大的空間或先調用API接收消息頭得到消息體大小,再開闢適當的空間接收消息體。兩種作法都有不足,不是浪費空間就是浪費時間。
複製代碼

顯然,Linux上的跨進程通訊須要內核空間作支持。傳統的跨進程通訊方式有Socket、信號量、管道、內存共享等,他們都屬於Linux內核,但Android上的Binder並不屬於Linux內核,那麼Binder如何實現IPC呢?答案是 Loadable Kernel Module查看wiki, Android利用Linux的動態內核可加載模塊機制(Loadable Kernel Module,LKM),創建Binder Driver掛載爲動態內核,而後經過Binder Driver以mmap的方式將內核空間與接收方的用戶空間進行內存映射,因而只須要從發送方的用戶空間拷貝數據到內核空間中,就實現了一次數據拷貝完成進程間通訊。使用mmap創建內核空間跟用戶空間的映射後,同一份物理內存,既能夠在用戶空間用虛擬地址訪問,也能夠在內核空間用虛擬地址訪問。因此mmap的本質是讓用戶空間中的一塊虛擬地址與內核空間中的一塊虛擬地址指向同一塊物理地址。

Android應用在進程啓動之初會建立一個單例的ProcessState對象,其構造函數執行時會同時完成binder mmap,爲進程分配一塊內存,專門用於Binder通訊。

匿名Binder

在ServiceManager中註冊過的Binder都叫實名Binder。當Client與Server經過實名Binder創建好Binder鏈接後,Server還能夠經過這個鏈接將新的Binder實體封裝進數據包傳遞給Client,這個被傳遞的就叫作匿名Binder,匿名Binder依然會在Binder Driver中生成實體節點,但不會在ServiceManager中註冊。

匿名Binder爲通訊雙方創建起一條私密通道,只要Server沒有把匿名Binder發給別的進程,別的進程就沒法經過窮舉或猜想等任何方式得到該Binder的引用,向該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不是線程池中的線程還能夠爲線程池分擔部分工做,減小線程池使用率。

3、mmap擴展

Linux的三種IO方式:標準IO、直接IO、mmap。

標準IO

應用程序平時使用的read()、write()都屬於標準IO,在發起讀寫操做後實際上是往內核空間的頁緩存讀寫數據。對於寫操做,系統默認是延遲寫入機制,頁緩存的數據會由內核在合適的時機寫入磁盤。

* 用戶發起 write 操做
* 操做系統查找頁緩存
    a.若未命中,則產生缺頁異常,而後建立頁緩存,將用戶傳入的內容寫入頁緩存
    b.若命中,則直接將用戶傳入的內容寫入頁緩存
* 用戶 write 調用完成
* 頁被修改後成爲髒頁,操做系統有兩種機制將髒頁寫回磁盤
    a.用戶手動調用 fsync()
    b.由 pdflush 進程定時將髒頁寫回磁盤
複製代碼

能夠看出write過程當中有兩次數據拷貝,第一次是從內存空間寫入內核空間,第二次是內核將頁緩存數據寫入磁盤。

知識擴展:

相對於機械硬盤,SSD 存儲還有一個「寫入放大」的問題。這個問題主要和 SSD 存儲的物理結構有關。
當 SSD 被所有寫過一遍以後,再寫入的數據是不能夠直接更新,只能夠經過覆蓋重寫,在覆蓋以前須要先擦除數據。
但寫入的最小單位是 Page,擦除的最小單位是 Block,而 Block 遠大於 Page,因此在寫入新數據時就須要先把 Block 上的數據讀出來和要寫入的數據合併在一塊兒,再把 Block 擦除,最後把讀出來的數據從新寫入到存儲上,這樣致使實際寫入的數據可能遠遠大於最開始須要寫入的數據。
複製代碼

直接IO

應用程序直接讀寫磁盤。Android並無提供直接IO的JAVA API。

mmap

mmap是操做系統中一種內存映射的方法。

內存映射:就是將用戶空間的一塊內存區域映射到內核空間。映射關係創建後,用戶對這塊內存區域的修改能夠直接反應到內核空間;反以內核空間對這段區域的修改也能直接反應到用戶空間。

mmap一般用在有物理介質的文件系統上。使用mmap能夠把文件映射到進程的地址空間,實現磁盤地址與進程虛擬空間地址的對應關係。

優勢:
    * 減小系統調用。只須要一次mmap()的系統調用,創建映射關係後就能夠像操做內存同樣。
    * 減小數據拷貝次數。mmap()只須要一次數據拷貝。
缺點:
    * 須要佔用更多的內存。
複製代碼

Java中提供的內存映射實現:MappedByteBuffer

相關文章
相關標籤/搜索