Android Binder 進程間通訊機制梳理
什麼是 Binder ?
Binder是Android系統中進程間通訊(IPC)的一種方式,也是Android系統中最重要的特性之一。Binder的設計採用了面向對象的思想,在Binder通訊模型的四個角色裡面;他們的代表都是「Binder」,這樣,對於Binder通訊的使用者而言,Server裡面的Binder和Client裡面的Binder沒有什麼不同,一個Binder對象就代表了所有,它不用關心實現的細節,甚至不用關心驅動以及SM的存在;這就是抽象。
-
通常意義下,Binder指的是一種通訊機制;我們說AIDL使用Binder進行通訊,指的就是Binder這種IPC機制。
-
對於Server進程來說,Binder指的是Binder本地對象
-
對於Client來說,Binder指的是Binder代理對象,它只是Binder本地對象的一個遠程代理;對這個Binder代理對象的操作,會通過驅動最終轉發到Binder本地對象上去完成;對於一個擁有Binder對象的使用者而言,它無須關心這是一個Binder代理對象還是Binder本地對象;對於代理對象的操作和對本地對象的操作對它來說沒有區別。
-
對於傳輸過程而言,Binder是可以進行跨進程傳遞的對象;Binder驅動會對具有跨進程傳遞能力的對象做特殊處理:自動完成代理對象和本地對象的轉換。
面向對象思想的引入將進程間通訊轉化為通過對某個Binder對象的引用調用該對象的方法,而其獨特之處在於Binder對象是一個可以跨進程引用的對象,它的實體(本地對象)位於一個進程中,而它的引用(代理對象)卻遍佈於系統的各個進程之中。最誘人的是,這個引用和java里引用一樣既可以是強類型,也可以是弱類型,而且可以從一個進程傳給其它進程,讓大家都能訪問同一Server,就象將一個對象或引用賦值給另一個引用一樣。Binder模糊了進程邊界,淡化了進程間通訊過程,整個系統彷彿運行於同一個面向對象的程式之中。形形色色的Binder對象以及星羅棋布的引用彷彿粘接各個應用程式的膠水,這也是Binder在英文里的原意。
理解Binder對於理解整個Android系統有著非常重要的作用,如果對Binder不了解,就很難對Android系統機制有更深入的理解。
1. Binder 架構
-
圖中紅色代表整個 framework 層 binder 架構相關組件,Binder 類代表 Server 端,BinderProxy類程式碼Client端;
-
圖中藍色代表 Native 層 Binder 架構相關組件,上層 framework 層的 Binder 邏輯是建立在Native層架構基礎之上的,核心邏輯都是交予Native層方法來處理。framework層的ServiceManager類與 Native 層的功能並不完全對應,framework 層的 ServiceManager 類的實現最終是通過BinderProxy傳遞給Native層來完成的,
Binder 通訊採用 C/S 架構,從組件視角來說,包含 Client、 Server、 ServiceManager 以及 Binder 驅動,其中 ServiceManager 用於管理系統中的各種服務。
Binder 在 framework 層進行了封裝,通過 JNI 技術調用 Native(C/C++)層的 Binder 架構。
Binder 在 Native 層以 ioctl 的方式與 Binder 驅動通訊。
BpBinder(客戶端)和BBinder(服務端)都是Android中Binder通訊相關的代表,它們都從IBinder類中派生而來,關係圖如下:
- client端:BpBinder.transact()來發送事務請求;
- server端:BBinder.onTransact()會接收到相應事務。
2. Binder 機制
首先需要註冊服務端,只有註冊了服務端,客戶端才有通訊的目標,服務端通過 ServiceManager 註冊服務,註冊的過程就是向 Binder 驅動的全局鏈表 binder_procs 中插入服務端的資訊(binder_proc 結構體,每個 binder_proc 結構體中都有 todo 任務隊列),然後向 ServiceManager 的 svcinfo 列表中快取一下註冊的服務。
有了服務端,客戶端就可以跟服務端通訊了,通訊之前需要先獲取到服務,拿到服務的代理,也可以理解為引用。比如下面的程式碼:
//獲取WindowManager服務引用 WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
獲取服務端的方式就是通過 ServiceManager 向 svcinfo 列表中查詢一下返回服務端的代理,svcinfo 列表就是所有已註冊服務的通訊錄,保存了所有註冊的服務資訊。
有了服務端的引用我們就可以向服務端發送請求了,通過 BinderProxy 將我們的請求參數發送給 ServiceManager,通過共享記憶體的方式使用內核方法 copy_from_user() 將我們的參數先拷貝到內核空間,這時我們的客戶端進入等待狀態,然後 Binder 驅動向服務端的 todo 隊列裡面插入一條事務,執行完之後把執行結果通過 copy_to_user() 將內核的結果拷貝到用戶空間(這裡只是執行了拷貝命令,並沒有拷貝數據,binder只進行一次拷貝),喚醒等待的客戶端並把結果響應回來,這樣就完成了一次通訊。
怎麼樣是不是很簡單,以上就是 Binder 機制的主要通訊方式,下面我們來看看具體實現。
3. Binder 驅動
我們先來了解下用戶空間與內核空間是怎麼交互的。
先了解一些概念
用戶空間/內核空間
詳細解釋可以參考 Kernel Space Definition; 簡單理解如下:
Kernel space 是 Linux 內核的運行空間,User space 是用戶程式的運行空間。 為了安全,它們是隔離的,即使用戶的程式崩潰了,內核也不受影響。
Kernel space 可以執行任意命令,調用系統的一切資源; User space 只能執行簡單的運算,不能直接調用系統資源,必須通過系統介面(又稱 system call),才能向內核發出指令。
系統調用/內核態/用戶態
雖然從邏輯上抽離出用戶空間和內核空間;但是不可避免的的是,總有那麼一些用戶空間需要訪問內核的資源;比如應用程式訪問文件,網路是很常見的事情,怎麼辦呢?
Kernel space can be accessed by user processes only through the use of system calls.
用戶空間訪問內核空間的唯一方式就是系統調用;通過這個統一入口介面,所有的資源訪問都是在內核的控制下執行,以免導致對用戶程式對系統資源的越權訪問,從而保障了系統的安全和穩定。用戶軟體良莠不齊,要是它們亂搞把系統玩壞了怎麼辦?因此對於某些特權操作必須交給安全可靠的內核來執行。
當一個任務(進程)執行系統調用而陷入內核程式碼中執行時,我們就稱進程處於內核運行態(或簡稱為內核態)此時處理器處於特權級最高的(0級)內核程式碼中執行。當進程在執行用戶自己的程式碼時,則稱其處於用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶程式碼中運行。處理器在特權等級高的時候才能執行那些特權CPU指令。
內核模組/驅動
通過系統調用,用戶空間可以訪問內核空間,那麼如果一個用戶空間想與另外一個用戶空間進行通訊怎麼辦呢?很自然想到的是讓作業系統內核添加支援;傳統的 Linux 通訊機制,比如 Socket,管道等都是內核支援的;但是 Binder 並不是 Linux 內核的一部分,它是怎麼做到訪問內核空間的呢? Linux 的動態可載入內核模組(Loadable Kernel Module,LKM)機制解決了這個問題;模組是具有獨立功能的程式,它可以被單獨編譯,但不能獨立運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。這樣,Android系統可以通過添加一個內核模組運行在內核空間,用戶進程之間的通過這個模組作為橋樑,就可以完成通訊了。
在 Android 系統中,這個運行在內核空間的,負責各個用戶進程通過 Binder 通訊的內核模組叫做 Binder 驅動;
驅動程式一般指的是設備驅動程式(Device Driver),是一種可以使電腦和設備通訊的特殊程式。相當於硬體的介面,作業系統只有通過這個介面,才能控制硬體設備的工作;
和路由器一樣,Binder驅動雖然默默無聞,卻是通訊的核心。儘管名叫『驅動』,實際上和硬體設備沒有任何關係,只是實現方式和設備驅動程式是一樣的:它工作於內核態,提供open(),mmap(),poll(),ioctl()等標準文件操作,以字元驅動設備中的misc設備註冊在設備目錄/dev下,用戶通過/dev/binder訪問該它。驅動負責進程之間Binder通訊的建立,Binder在進程之間的傳遞,Binder引用計數管理,數據包在進程之間的傳遞和交互等一系列底層支援。驅動和應用程式之間定義了一套介面協議,主要功能由ioctl()介面實現,不提供read(),write()介面,因為ioctl()靈活方便,且能夠一次調用實現先寫後讀以滿足同步交互,而不必分別調用write()和read()。Binder驅動的程式碼位於linux目錄的drivers/misc/binder.c中。
熟悉了上面這些概念,我們再來看下上面的圖,用戶空間中 binder_open(), binder_mmap(), binder_ioctl() 這些方法通過 system call 來調用內核空間 Binder 驅動中的方法。內核空間與用戶空間共享記憶體通過 copy_from_user(), copy_to_user() 內核方法來完成用戶空間與內核空間記憶體的數據傳輸。 Binder驅動中有一個全局的 binder_procs 鏈表保存了服務端的進程資訊。
4. Binder記憶體機制
binder_mmap:binder_mmap(文件描述符,用戶虛擬記憶體空間)
主要功能:首先在內核虛擬地址空間,申請一塊與用戶虛擬記憶體相同大小的記憶體;然後再申請1個page大小的物理記憶體,再將同一塊物理記憶體分別映射到內核虛擬地址空間和用戶虛擬記憶體空間,從而實現了用戶空間的Buffer和內核空間的Buffer同步操作的功能。
binder_mmap通過加鎖,保證一次只有一個進程分配記憶體,保證多進程間的並發訪問。其中user_buffer_offset
是虛擬進程地址與虛擬內核地址的差值(該值為負數)。也就是說同一物理地址,當內核地址為kernel_addr,則進程地址為proc_addr = kernel_addr + user_buffer_offset。 因為分配給內核的虛擬地址是高位,所以是負值。
這也是Binder進程間通訊效率高的核心機制所在,如下圖
虛擬進程地址空間(vm_area_struct)和虛擬內核地址空間(vm_struct)都映射到同一塊物理記憶體空間。當Client端與Server端發送數據時,Client(作為數據發送端)先從自己的進程空間把IPC通訊數據copy_from_user
拷貝到內核空間,而Server端(作為數據接收端)與內核共享數據,不再需要拷貝數據,而是通過記憶體地址空間的偏移量,即可獲悉記憶體地址,整個過程只發生一次記憶體拷貝。一般地做法,需要Client端進程空間拷貝到內核空間,再由內核空間拷貝到Server進程空間,會發生兩次拷貝。
這裡解釋下:這裡有三個業務方:發送方,binder 驅動(內核),接收方。其中接收方和內核是共享記憶體的。發送方需要將數據拷貝到內核指向的地址空間。
對於進程和內核虛擬地址映射到同一個物理記憶體的操作是發生在數據接收端,而數據發送端還是需要將用戶態的數據複製到內核態。到此,可能有讀者會好奇,為何不直接讓發送端和接收端直接映射到同一個物理空間,那樣就連一次複製的操作都不需要了,0次複製操作那就與Linux標準內核的共享記憶體的IPC機制沒有區別了,對於共享記憶體雖然效率高,但是對於多進程的同步問題比較複雜,而管道/消息隊列等IPC需要複製2兩次,效率較低。這裡就不先展開討論Linux現有的各種IPC機制跟Binder的詳細對比,總之Android選擇Binder的基於速度和安全性的考慮。
下面這圖是從Binder在進程間數據通訊的流程圖,從圖中更能明了Binder的記憶體轉移關係。
5. Binder 進程與執行緒
對於底層Binder驅動,通過 binder_procs 鏈表記錄所有創建的 binder_proc 結構體,binder 驅動層的每一個 binder_proc 結構體都與用戶空間的一個用於 binder 通訊的進程一一對應,且每個進程有且只有一個 ProcessState 對象,這是通過單例模式來保證的。在每個進程中可以有很多個執行緒,每個執行緒對應一個 IPCThreadState 對象,IPCThreadState 對象也是單例模式,即一個執行緒對應一個 IPCThreadState 對象,在 Binder 驅動層也有與之相對應的結構,那就是 Binder_thread 結構體。在 binder_proc 結構體中通過成員變數 rb_root threads,來記錄當前進程內所有的 binder_thread。
Binder 執行緒池:每個 Server 進程在啟動時創建一個 binder 執行緒池,並向其中註冊一個 Binder 執行緒;之後 Server 進程也可以向 binder 執行緒池註冊新的執行緒,或者 Binder 驅動在探測到沒有空閑 binder 執行緒時主動向 Server 進程註冊新的的 binder 執行緒。對於一個 Server 進程有一個最大 Binder 執行緒數限制,默認為16個 binder 執行緒,例如 Android 的 system_server 進程就存在16個執行緒。對於所有 Client 端進程的 binder 請求都是交由 Server 端進程的 binder 執行緒來處理的。
Binder IPC機制,就是指在進程間傳輸數據(binder_transaction_data),一次數據的傳輸,稱為事務(binder_transaction)。對於多個不同進程向同一個進程發送事務時,這個同一個進程或執行緒的事務需要串列執行,在Binder驅動中為binder_proc和binder_thread都有todo隊列。
也就是說對於進程間的通訊,就是發送端把binder_transaction節點,插入到目標進程或其子執行緒的todo隊列中,等目標進程或執行緒不斷循環地從todo隊列中取出數據並進行相應的操作。
在Binder驅動層,每個接收端進程都有一個todo隊列,用於保存發送端進程發送過來的binder請求,這類請求可以由接收端進程的任意一個空閑的binder執行緒處理;接收端進程存在一個或多個binder執行緒,在每個binder執行緒里都有一個todo隊列,也是用於保存發送端進程發送過來的binder請求,這類請求只能由當前binder執行緒來處理。binder執行緒在空閑時進入可中斷的休眠狀態,當自己的todo隊列或所屬進程的todo隊列有新的請求到來時便會喚醒,如果是由所需進程喚醒的,那麼進程會讓其中一個執行緒處理響應的請求,其他執行緒再次進入休眠狀態。
6. ServiceManager 啟動
了解了 Binder 驅動,怎麼與 Binder 驅動進行通訊呢?那就是通過 ServiceManager,好多文章稱 ServiceManager 是 Binder 驅動的守護進程,大管家,其實 ServiceManager 的作用很簡單就是提供了查詢服務和註冊服務的功能。下面我們來看一下 ServiceManager 啟動的過程。
ServiceManager 分為 framework 層和 native 層,framework 層只是對 native 層進行了封裝方便調用,圖上展示的是 native 層的 ServiceManager 啟動過程。
ServiceManager 的啟動是系統在開機時,init 進程解析 init.rc 文件調用 service_manager.c 中的 main() 方法入口啟動的。 native 層有一個 binder.c 封裝了一些與 Binder 驅動交互的方法。
ServiceManager 的啟動分為三步,首先打開驅動創建全局鏈表 binder_procs,然後將自己當前進程資訊保存到 binder_procs 鏈表,最後開啟 loop 不斷的處理共享記憶體中的數據,並處理 BR_xxx 命令(ioctl 的命令,BR 可以理解為 binder reply 驅動處理完的響應)。
ServiceManager本身工作相對簡單,其功能:查詢和註冊服務。 對於Binder IPC通訊過程中,其實更多的情形是BpBinder和BBinder之間的通訊,比如ActivityManagerProxy和ActivityManagerService之間的通訊等。
7. ServiceManager 註冊服務
註冊 MediaPlayerService 服務端,我們通過 ServiceManager 的 addService() 方法來註冊服務。
首先 ServiceManager 向 Binder 驅動發送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解為 binder client 客戶端發過來的請求命令)攜帶 ADD_SERVICE_TRANSACTION 命令,同時註冊服務的執行緒進入等待狀態 waitForResponse()。 Binder 驅動收到請求命令向 ServiceManager 的 todo 隊列裡面添加一條註冊服務的事務。事務的任務就是創建服務端進程 binder_node 資訊並插入到 binder_procs 鏈表中。
事務處理完之後發送 BR_TRANSACTION 命令,ServiceManager 收到命令後向 svcinfo 列表中添加已經註冊的服務。最後發送 BR_REPLY 命令喚醒等待的執行緒,通知註冊成功。
8. ServiceManager 獲取服務
獲取服務的過程與註冊類似,相反的過程。通過 ServiceManager 的 getService() 方法來註冊服務。
首先 ServiceManager 向 Binder 驅動發送 BC_TRANSACTION 命令攜帶 CHECK_SERVICE_TRANSACTION 命令,同時獲取服務的執行緒進入等待狀態 waitForResponse()。
Binder 驅動收到請求命令向 ServiceManager 的發送 BC_TRANSACTION 查詢已註冊的服務,查詢到直接響應 BR_REPLY 喚醒等待的執行緒。若查詢不到將與 binder_procs 鏈表中的服務進行一次通訊再響應。
請求服務(getService)區分請求服務所屬進程情況:
-
當請求服務的進程與服務屬於不同進程,則為請求服務所在進程創建binder_ref對象,指向服務進程中的binder_node;
- 最終readStrongBinder(),返回的是BpBinder對象;
-
當請求服務的進程與服務屬於同一進程,則不再創建新對象,只是引用計數加1,並且修改type為BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER。
-
最終readStrongBinder(),返回的是BBinder對象的真實子類
-
9. 進行一次完整通訊
我們在使用 Binder 時基本都是調用 framework 層封裝好的方法,AIDL 就是 framework 層提供的傻瓜式是使用方式。詳細使用可以參考:Android AIDL 實例與原理分析
首先我們通過 ServiceManager 獲取到服務端的 BinderProxy 代理對象,通過調用 BinderProxy 將參數,方法標識(例如:TRANSACTION_test,AIDL中自動生成)傳給 ServiceManager,同時客戶端執行緒進入等待狀態。
ServiceManager 將用戶空間的參數等請求數據複製到內核空間,並向服務端插入一條執行執行方法的事務。事務執行完通知 ServiceManager 將執行結果從內核空間複製到用戶空間,並喚醒等待的執行緒,響應結果,通訊結束。
參考文章:
一篇文章了解相見恨晚的 Android Binder 進程間通訊機制