1 什么是Binder?
- 從IPC角度來說, Binder 是 Android 中特有的一種跨進程的通信方式
- Binder 可以理解成一種虛擬的物理設備,設備驅動是/dev/binder
- 從 Android Framework 角度來說,Binder 是 ServiceManager 連接各種 Manager(ActivityManager、WindowManager等) 和 ManagerService 的橋梁
- Android系統基本上可以看作是一個基于Binder通信的C/S架構
Binder 形象類比圖
通常我們訪問一個網頁的步驟是這樣的:首先在瀏覽器輸入一個地址,如 www.google.com 然后按下回車鍵。但是并沒有辦法通過域名地址直接找到我們要訪問的服務器,因此需要首先訪問 DNS 域名服務器,域名服務器中保存了 www.google.com 對應的 ip 地址 10.249.23.13,然后通過這個 ip 地址才能放到到 www.google.com 對應的服務器。
Client、Server、ServiceManager、Binder 驅動這幾個組件在通信過程中扮演的角色就如同互聯網中服務器(Server)、客戶端(Client)、DNS域名服務器(ServiceManager)以及路由器(Binder 驅動)之間的關系。
Binder 驅動
Binder 驅動就如同路由器一樣,是整個通信的核心;驅動負責進程之間 Binder 通信的建立,Binder 在進程之間的傳遞,Binder 引用計數管理,數據包在進程之間的傳遞和交互等一系列底層支持。
ServiceManager 與實名 Binder
ServiceManager 和 DNS 類似,作用是將字符形式的 Binder 名字轉化成 Client 中對該 Binder 的引用,使得 Client 能夠通過 Binder 的名字獲得對 Binder 實體的引用。注冊了名字的 Binder 叫實名 Binder,就像網站除了有 IP 地址外還有自己的網址一樣。Server 創建了 Binder,并為它起一個字符形式,可讀易記得名字,將這個 Binder 實體連同名字一起以數據包的形式通過 Binder 驅動發送給 ServiceManager ,通知 ServiceManager 注冊一個名為“張三”的 Binder,它位于某個 Server 中。驅動為這個穿越進程邊界的 Binder 創建位于內核中的實體節點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager。ServiceManger 收到數據后從中取出名字和引用填入查找表。
ServierManager 是一個進程,Server 是另一個進程,Server 向 ServiceManager 中注冊 Binder 必然涉及到進程間通信。當前實現進程間通信又要用到進程間通信,這就好像蛋可以孵出雞的前提卻是要先找只雞下蛋!Binder 的實現比較巧妙,就是預先創造一只雞來下蛋。ServiceManager 和其他進程同樣采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 實體,其他進程都是 Client,需要通過這個 Binder 的引用來實現 Binder 的注冊,查詢和獲取。ServiceManager 提供的 Binder 比較特殊,它沒有名字也不需要注冊。當一個進程使用 BINDER_SET_CONTEXT_MGR 命令將自己注冊成 ServiceManager 時 Binder 驅動會自動為它創建 Binder 實體(這就是那只預先造好的那只雞)。其次這個 Binder 實體的引用在所有 Client 中都固定為 0 而無需通過其它手段獲得。也就是說,一個 Server 想要向 ServiceManager 注冊自己的 Binder 就必須通過這個 0 號引用和 ServiceManager 的 Binder 通信。類比互聯網,0 號引用就好比是域名服務器的地址,你必須預先動態或者手工配置好。要注意的是,這里說的 Client 是相對于 ServiceManager 而言的,一個進程或者應用程序可能是提供服務的 Server,但對于 ServiceManager 來說它仍然是個 Client。
Client 獲得實名 Binder 的引用
Server 向 ServiceManager 中注冊了 Binder 以后, Client 就能通過名字獲得 Binder 的引用了。Client 也利用保留的 0 號引用向 ServiceManager 請求訪問某個 Binder: 我申請訪問名字叫張三的 Binder 引用。ServiceManager 收到這個請求后從請求數據包中取出 Binder 名稱,在查找表里找到對應的條目,取出對應的 Binder 引用作為回復發送給發起請求的 Client。從面向對象的角度看,Server 中的 Binder 實體現在有兩個引用:一個位于 ServiceManager 中,一個位于發起請求的 Client 中。如果接下來有更多的 Client 請求該 Binder,系統中就會有更多的引用指向該 Binder ,就像 Java 中一個對象有多個引用一樣。
2 Binder由來
Android 系統是基于 Linux 內核的,Linux 已經提供了管道、消息隊列、共享內存和 Socket 等 IPC 機制。那為什么 Android 還要提供 Binder 來實現 IPC 呢?主要是基于性能、穩定性和安全性幾方面的原因。
2.1 性能
- Socket 作為一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通信和本機上進程間的低速通信。
- 消息隊列和管道采用存儲-轉發方式,即數據先從發送方緩存區拷貝到內核開辟的緩存區中,然后再從內核緩存區拷貝到接收方緩存區,至少有兩次拷貝過程。
- 共享內存雖然無需拷貝,但控制復雜,難以使用。
- Binder 只需要一次數據拷貝,性能上僅次于共享內存。
2.2 穩定性
- Binder 基于 C/S 架構,客戶端(Client)有什么需求就丟給服務端(Server)去完成,架構清晰、職責明確又相互獨立,自然穩定性更好。
- 共享內存雖然無需拷貝,但是控制負責,難以使用。
2.3 安全性
Android 作為一個開放性的平臺,其上運行這海量的應用程序,其安全性是不言而喻的。
傳統的 IPC 沒有任何安全措施,完全依賴上層協議來確保。
- 傳統的 IPC 接收方無法獲得對方可靠的進程用戶ID/進程ID(UID/PID),從而無法鑒別對方身份。Android 為每個安裝好的 APP 分配了自己的 UID,故而進程的 UID 是鑒別進程身份的重要標志。
- 傳統的 IPC 只能由用戶在數據包中填入 UID/PID,但這樣不可靠,容易被惡意程序利用。可靠的身份標識只有由 IPC 機制在內核中添加。
- 傳統的 IPC 訪問接入點是開放的,只要知道這些接入點的程序都可以和對端建立連接,不管怎樣都無法阻止惡意程序通過猜測接收方地址獲得連接。
Binder 既支持實名 Binder,又支持匿名 Binder,安全性高。
各種IPC方式數據拷貝次數
IPC | 數據拷貝次數 |
---|---|
共享內存 | 0 |
Binder | 1 |
Socket | 2 |
管道 | 2 |
消息隊列 | 2 |
基于上述原因,Android 需要建立一套新的 IPC 機制來滿足系統對穩定性、傳輸性能和安全性方面的要求,這套新的 IPC 機制就是 Binder。
3 Linux 傳統的 IPC 原理
為了更好的理解 Binder 通信原理,我們先來了解下Linux 傳統的進程間通信原理
我們先來了解下 Liunx 中跨進程通信涉及到的一些基本概念
3.1 用戶空間/內核空間
Kernel space 是 Linux 內核的運行空間,User space 是用戶程序的運行空間。 為了安全,它們是隔離的,即使用戶的程序崩潰了,內核也不受影響。
Kernel space 可以執行任意命令,調用系統的一切資源; User space 只能執行簡單的運算,不能直接調用系統資源,必須通過系統接口(又稱 system call)調用系統資源。
3.2 系統調用/內核態/用戶態
系統調用:用戶空間訪問內核空間的唯一方式就是系統調用;通過這個統一入口接口,所有的資源訪問都是在內核的控制下執行,以免導致對用戶程序對系統資源的越權訪問,從而保障了系統的安全和穩定。
Linux 使用兩級保護機制:0 級供系統內核使用,3 級供用戶程序使用。
內核態:當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處于內核運行態(或簡稱為內核態)此時處理器處于特權級最高的(0級)內核代碼中執行。
用戶態:當進程在執行用戶自己的代碼時,則稱其處于用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶代碼中運行。
系統調用主要通過如下兩個函數來實現
copy_from_user() //將數據從用戶空間拷貝到內核空間
copy_to_user() //將數據從內核空間拷貝到用戶空間
3.3 進程隔離
簡單的說就是操作系統中,進程與進程間內存是不共享的。兩個進程就像兩個平行的世界,A 進程沒法直接訪問 B 進程的數據,這就是進程隔離的通俗解釋。A 進程和 B 進程之間要進行數據交互就得采用特殊的通信機制:進程間通信(IPC)。
3.4 Linux 傳統 IPC 通信原理
理解了上面的幾個概念,我們再來看看傳統的 IPC 方式中,進程之間是如何實現通信的。
通常的做法是消息發送方將要發送的數據存放在內存緩存區中,通過系統調用進入內核態。然后內核程序在內核空間分配內存,開辟一塊內核緩存區,調用 copy_from_user() 函數將數據從用戶空間的內存緩存區拷貝到內核空間的內核緩存區中。同樣的,接收方進程在接收數據時在自己的用戶空間開辟一塊內存緩存區,然后內核程序調用 copy_to_user() 函數將數據從內核緩存區拷貝到接收進程的內存緩存區。這樣數據發送方進程和數據接收方進程就完成了一次數據傳輸,我們稱完成了一次進程間通信。
這種傳統的 IPC 通信方式有兩個問題:
- 一次數據傳遞需要經歷:內存緩存區 --> 內核緩存區 --> 內存緩存區,需要 2 次數據拷貝,性能低。
- 接收數據的緩存區由數據接收進程提供,但是接收進程并不知道需要多大的空間來存放將要傳遞過來的數據,因此只能開辟盡可能大的內存空間或者先調用 API 接收消息頭來獲取消息體的大小,這兩種做法不是浪費空間就是浪費時間。
4 Binder架構
- Binder 通信采用 C/S 架構,從組件視角來說,包含 Client、 Server、 ServiceManager 以及 Binder 驅動,其中 ServiceManager 用于管理系統中的各種服務。
- Binder 在 framework 層進行了封裝,通過 JNI 技術與 Native(C/C++)層的 Binder 架構通信。
- Binder 在 Native 層以 ioctl 的方式與 Binder 驅動通信。
5 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() 將內核的結果通過內存映射關系映射到用戶空間,喚醒等待的客戶端并把結果響應回來,這樣就完成了一次通訊。
6 Binder驅動
我們先來看下用戶空間與內核空間的交互
通過系統調用,用戶空間可以訪問內核空間,那么如果一個用戶空間想與另外一個用戶空間進行通信怎么辦呢?很自然想到的是讓操作系統內核添加支持;傳統的 Linux 通信機制,比如 Socket,管道等都是內核支持的;但是 Binder 并不是 Linux 內核的一部分,它是怎么做到訪問內核空間的呢? Linux 的動態可加載內核模塊(Loadable Kernel Module,LKM)機制解決了這個問題;模塊是具有獨立功能的程序,它可以被單獨編譯,但不能獨立運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。這樣,Android系統可以通過添加一個內核模塊運行在內核空間,用戶進程之間的通過這個模塊作為橋梁,就可以完成通信了。
在 Android 系統中,這個運行在內核空間的,負責各個用戶進程通過 Binder 通信的內核模塊叫做 Binder 驅動;
熟悉了上面這些概念,我們再來看下上面的圖,用戶空間中 binder_open(), binder_mmap(), binder_ioctl() 這些方法通過 system call 來調用內核空間 Binder 驅動中的方法。內核空間與用戶空間共享內存通過 copy_from_user(), copy_to_user() 內核方法來完成用戶空間與內核空間內存的數據傳輸。 Binder驅動中有一個全局的 binder_procs 鏈表保存了服務端的進程信息。
那么在 Android 系統中用戶進程之間是如何通過這個內核模塊(Binder 驅動)來實現通信的呢?難道是和前面說的傳統 IPC 機制一樣,先將數據從發送方進程拷貝到內核緩存區,然后再將數據從內核緩存區拷貝到接收方進程,通過兩次拷貝來實現嗎?顯然不是,否則也不會有開篇所說的 Binder 在性能方面的優勢了。
Linux 下的另一個概念:內存映射
Binder IPC 機制中涉及到的內存映射通過 mmap() 來實現,mmap() 是操作系統中一種內存映射的方法,該函數經過系統調用最終會調用到binder驅動的binder_mmap() 函數。內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關系建立后,用戶對這塊內存區域的修改可以直接反應到內核空間;反之內核空間對這段區域的修改也能直接反應到用戶空間。內存映射能減少數據拷貝次數,實現用戶空間和內核空間的高效互動。兩個空間各自的修改能直接反映在映射的內存區域,從而被對方空間及時感知。
7 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 線程來處理的。
8 ServiceManager 啟動
ServiceManager提供了向Binder 驅動查詢服務和注冊服務的功能。
- 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 驅動處理完的響應)。
9 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 命令喚醒等待的線程,通知注冊成功。
10 ServiceManager 獲取服務
- 獲取服務的過程與注冊類似,相反的過程。通過 ServiceManager 的 getService() 方法來注冊服務。
- 首先 ServiceManager 向 Binder 驅動發送 BC_TRANSACTION 命令攜帶 CHECK_SERVICE_TRANSACTION 命令,同時獲取服務的線程進入等待狀態 waitForResponse()。
- Binder 驅動收到請求命令向 ServiceManager 的發送 BR_TRANSACTION 查詢已注冊的服務,查詢到直接響應 BR_REPLY 喚醒等待的線程。若查詢不到將與 binder_procs 鏈表中的服務進行一次通訊再響應。
11 進行一次完整通訊
我們在使用 Binder 時基本都是調用 framework 層封裝好的方法,AIDL 就是 framework 層提供的傻瓜式是使用方式。假設服務已經注冊完,我們來看看客戶端怎么執行服務端的方法。
- 首先我們通過 ServiceManager 獲取到服務端的 BinderProxy 代理對象,通過調用 BinderProxy 將參數,方法標識(例如:TRANSACTION_test,AIDL中自動生成)傳給 ServiceManager,同時客戶端線程進入等待狀態。
- ServiceManager 將用戶空間的參數等請求數據復制到內核空間,并向服務端插入一條執行執行方法的事務。事務執行完通知 ServiceManager 將執行結果從內核空間復制到用戶空間,并喚醒等待的線程,響應結果。
參考鏈接:
一篇文章了解相見恨晚的 Android Binder 進程間通訊機制
寫給 Android 應用工程師的 Binder 原理剖析
Android Bander設計與實現 - 設計篇