了解了 Linux IPC 相關的概念和通信原理, 下面正式介紹下 Binder IPC 的原理.
(如有侵權, 請聯系刪除)
1. 動態內核可加載模塊 && 內存映射
正如上一章所說, 跨進程通信是需要內核空間做支持的. 傳統的 IPC 機制如 管道, Socket, 都是內核的一部分, 因此通過內核支持來實現進程間通信自然是沒問題的.
但是 Binder 并不是 Linux 系統內核的一部分, 那怎么辦呢, 這得益于 Linux 的動態內核可加載模塊 (Loadable Kernel Module, LKM)的機制
1.1 動態內核可加載模塊 (Loadable Kernel Module, LKM)
動態內核可加載模塊 (Loadable Kernel Module, LKM)
?
模塊是具有獨立功能的程序, 它可以被單獨編譯, 但是不能獨立運行. 它在運行時被鏈接到內核作為內核的一部分運行.
這樣 Android 系統就可以通過動態添加一個內核模塊運行在內核空間, 用戶進程進程之間通過這個內核模塊作為橋梁來實現通信.
在 Android 中, 這個運行在內核空間, 負責各個用戶進程通過 Binder 實現通信的內核模塊就叫做 Binder 驅動 (Binder Driver)
那么在 Android 系統中用戶進程之間是如何通過這個內核模塊 (Binder Driver)來實現通信的呢? 顯然不是和上一章的傳統 IPC 通信一樣,進行兩次 copy 了, 不然Binder 也不有在性能方面的優勢了.
1.2 內存映射
Binder IPC 機制中設計到的內存映射通過 mmap()
來實現, mmap()
是操作系統中一種內存映射的方法.
內存映射
?
簡單的說就是將用戶空間的一塊內存區域映射到內核空間.
映射關系建立后, 用戶對這塊內存區域的修改可以直接反應到內核空間.
反之,內核空間對這段區域的修改也能直接反應到用戶空間.
內存映射能減少數據 copy 的次數, 實現用戶空間和內核空間的高效互動. 兩個空間各自的修改也能直接反應在映射的內存區域, 從而被對方空間及時感知. 也正因為如此, 內存映射能夠提供對進程間通信的支持.
2. Binder IPC 實現原理
Binder IPC 正是基于內存映射(mmap()
) 來實現的, 但是mmap()
通常是用在有物理介質的文件系統上的.
比如進程中的用戶區域是不能直接和物理設備打交道的, 如果想要把磁盤上的數據讀取到進程的用戶區域, 需要兩次 copy (磁盤 -> 內核空間 -> 用戶空間). 通常在這種場景下 mmap()
就能發揮作用, 通過在物理介質和用戶空間之間建立映射, 減少數據的 copy 次數, 用內存讀寫代替 I/O 讀寫, 提高文件讀取效率.
而 Binder 并不存在物理介質, 因此 Binder 驅動使用 mmap()
并不是為了在物理介質和用戶空間之間映射, 而是用來在內核空間創建數據接收的緩存空間.
一次完整的 Binder IPC 通信過程通常是這樣:
- 首先 Binder 驅動在內核空間創建一個數據接收緩存區
- 接著在內核空間開辟一塊內核緩存區.
- 建立內核緩存區 和 內核中數據接收緩存區 之間的映射關系.
- 建立內核中數據接收緩存區 與 數據接收進程用戶空間 的地址的映射關系
- 發送方進程通過系統調用
copy_from_user()
將數據 copy 到內核中的內核緩存區, 由于內核緩存區 與 內核數據接收緩存區 存在映射關系, 而內核數據接收緩存區 又與 數據接收進程用戶空間 存在映射關系, 所以相當于 內核緩存區 與 數據接收進程用戶空間存在映射關系. 因此也就相當于把數據發送到了數據接收進程的用戶空間.
這樣就完成了一次進程間通信
如下圖:
3. Binder 通信模型
介紹完 Binder IPC 的底層通信原理, 接下來我們看看實現層面是如何設計的
一次完成的進程間通信必然至少包含兩個進程, 通常我們稱通信的雙方分別為客戶端進程(Client) 和服務端進程(Server), 由于進程隔離機制的存在, 通信雙方必然需要借助 Binder 來實現.
3.1 Client/Server/ServiceManager/Binder驅動
BInder 是基于 C/S 架構. 是由一些列組件組成. 包括 Client, Server, ServiceManager, Binder 驅動.
- Client, Server, ServiceManager 運行在用戶空間
- Binder 驅動運行在內核空間.
- ServiceManager, Binder 驅動由系統提供.
- Client, Service 由應用程序來實現
Client, Server, ServiceManager 都是通過系統調用
open, mmap,
和ioctl
來訪問設備文件/dev/binder
, 從而實現與 Binder 驅動的交互來間接的實現跨進程通信.
3.1.1 Binder 驅動
Binder 驅動就如如同路由器一樣, 是整個通信的核心. 驅動負責進程之間 Binder 通信的建立 / 傳遞, Binder 引用計數管理, 數據包在進程之間的傳遞和交互等一系列底層支持.
3.1.2 ServiceManager 與 實名 Binder
ServiceManager 作用是將字符形式的 Binder 名字轉化成 Client 中對該 Binder 的引用, 使得 Client 能夠通過 Binder 的名字獲得對 Binder 實體的引用.
注冊了名字的 Binder 叫實名 Binder, 就像網站一樣除了 IP 地址以外還有自己的網址.
Server 創建了 Binder, 并為它起一個字符形式, 可讀易記的名字, 將這個 BInder 實體連同名字一起以數據包的形式通過 Binder 驅動 發送給 ServiceManager, 通知 ServiceManager 注冊一個名字為 "張三"的 Binder, 它位于某個 Server 中, 驅動為這個穿越進程邊界的 BInder 創建位于內核中的實體節點以及 ServiceManager 對實體的引用, 將名字以及新建的引用打包傳給 ServiceManager, ServiceManager 收到數據后從中取出名字和引用填入查找表.
ServiceManager 是一個進程, Server 又是一個另外的進程, Server 向 ServiceManager 中注冊 BInder 必然涉及到進程間通信. 當實現進程間通信又要用到進程間通信, 這就好像蛋可以孵出雞的前提確實要先找只雞下蛋! Binder 的實現比較巧妙, 就是預先創造一只雞來下蛋. ServiceManager 和其他進程同樣采用 Binder 通信, ServiceManager 是 Server 端, 有自己的 Binder 實體, 其他進程都是 Client, 需要通過這個 Binder 的引用來實現 Binder 的注冊, 查詢和獲取. ServiceManager 提供的 Binder 比較特殊, 它沒有名字也不需要注冊. 當一個進程使用 BINDERSETCONTEXT_MGR 命令將自己注冊成 ServiceManager 時 Binder 驅動會自動為它創建 Binder 實體(這就是那只預先造好的那只雞). 其實這個 Binder 實體的引用在所有 Client 中都固定為 0 , 而無需通過其他手段獲得. 也就是說, 一個 Server 想要向 ServiceManager 注冊自己的 Binder 就必須通過這個 0 號引用和 ServiceManager 的 Binder 通信. 這里說的 Client 是相對于 ServiceManager 而言的, 一個進程或者應用程序可能是提供服務的 Server, 但是對于 ServiceManager 來說它仍然是個 Client.
Server 向 ServiceManager 中注冊了 Binder 以后, Client 就能通過名字獲得 Binder 的引用. Client 也利用保留的 0 號引用向 ServiceManager 請求訪問某個 Binder. 比如,Client 申請訪問名字叫"張三"的 Binder 引用. ServiceManager 收到這個請求后從請求數據包中取出 Binder 名稱, 在查找表里找到對應的條目, 取出對應的 Binder 引用, 作為回復發送給發起請求的 Client. 從面相對象的角度看, Server 中的 Binder 實體現在有兩個引用: 一個位于 ServiceManager 中, 一個位于發起請求的 Client 中. 如果后面會有更多的 Client 請求該 Binder, 系統中就會有更多的引用指向這個 Binder, 就像 Java 中一個對象有多個引用一樣.
4. Binder 通信中的代理模式
我們已經解釋清楚 Client, Server 借助 Binder 驅動完成跨進程通信的實現機制了, 但是還有個問題需要弄清楚, 比如 A 進程想要 B 進程中的某個對象(object) 是如何實現的呢, 畢竟它們屬于不同的進程, A 進程沒辦法直接使用 B 進程中的 object.
前面我們說過跨進程通信的過程都有 Binder 驅動的參與, 因此在數據流經 Binder 驅動的時候 Binder 驅動會對數據做一層轉換.
我們在 Client端,向 ServiceManager 獲取具體的 Server 端的 Binder 引用的時候,會首先進過 Binder 驅動,Binder 驅動它并不會把真正的 Server 的 Binder 引用返回給 Client 端,而是返回一個代理的 java 對象,該對象具有跟 Server 端的 Binder 引用相同的方法簽名,這個對象為 ProxyObject,他具有跟 Server 的 Binder 實例一樣的方法,只是這些方法并沒有 Server 端的能力,這些方法只需要把請求參數交給 Binder 驅動即可. 對于 Client 端來說和直接調用 Server 中的方法是一樣的.
5. Binder 通信過程
了解了上面之后, 我們大致可以推算出 Binder 的通信過程
1. 注冊 ServiceManager
一個進程使用 BINDERSETCONTEXT_MGR 命令通過 Binder 驅動將自己注冊成為 ServiceManager.
2. 注冊 Server
Server 通過 Binder 驅動向 ServiceManager 中注冊 Binder (Server 中 Binder 實體), 表明可以對外提供服務.
Binder 驅動為這個 Binder 創建位于內核中的實體節點以及 ServiceManager 對實體的引用, 然后將名字及新建的引用打包傳給 ServiceManager, 最后 ServiceManager 將其填入到查找表.
3. Client 獲取 Server 的 Binder 引用
Client 請求獲得 Server 的 Binder 引用, Binder 驅動在接收到 Client 的請求的時候, 就去 ServiceManager 中查詢, 查詢到后會創建一個 Server 端 Binder 對象的 ProxyObject (Binder 的代理對象), 并將該 ProxyObject 返回給 Client.
4. Client 與 Server 通信
Client 收到 Binder 驅動返回的 ProxyObject 后, 通過 ProxyObject 調用其中的方法. ProxyObject 的方法再去調用 Binder 驅動, Binder 驅動會去查詢自己維護的表單, 發現這是某個 Server 端 Binder 的代理對象, 就會通知 Server 端調用具體的方法,(會把 Client 端調用 ProxyObject 中方法的參數傳過去. ), 并要求 Server 把結果返回給自己. 當 Binder 驅動拿到結果后, 就會轉發給 Client 端.
6. Binder 的完整定義
- 從進程間通信的角度看, Binder 是一種進程間通信的機制
- 從 Server 進程的角度看, Binder 指的是 Server 中的 Binder 實體對象.
- 從 Client 進程的角度看, Binder 指的是對 Binder 代理對象, 是 Binder 實體對象的一個遠程代理.
- 從傳輸過程的角度看, Binder 是一個可以跨進程通信的對象. Binder 驅動會對這個跨越進程邊界的對象