1. 程式人生 > >一篇文章瞭解相見恨晚的 Android Binder 程序間通訊機制

一篇文章瞭解相見恨晚的 Android Binder 程序間通訊機制

Android-Binder程序間通訊機制

概述

最近在學習Binder機制,在網上查閱了大量的資料,也看了老羅的Binder系列的部落格和Innost的深入理解Binder系列的部落格,都是從底層開始講的,全是C程式碼,雖然之前學過C和C++,然而各種函式之間花式跳轉,看的我都懷疑人生。毫不誇張的講每看一遍都是新的內容,跟沒看過一樣。後來又看到了Gityuan的部落格看到了一些圖解彷彿發現了新大陸。

下面就以圖解的方式介紹下Binder機制,相信你看這篇文章,一定有所收穫。

什麼是 Binder?

Binder是Android系統中程序間通訊(IPC)的一種方式,也是Android系統中最重要的特性之一。Android中的四大元件Activity,Service,Broadcast,ContentProvider,不同的App等都執行在不同的程序中,它是這些程序間通訊的橋樑。正如其名“粘合劑”一樣,它把系統中各個元件粘合到了一起,是各個元件的橋樑。

理解Binder對於理解整個Android系統有著非常重要的作用,如果對Binder不瞭解,就很難對Android系統機制有更深入的理解。

1. Binder 架構

這裡寫圖片描述

  • Binder 通訊採用 C/S 架構,從元件視角來說,包含 Client、 Server、 ServiceManager 以及 Binder 驅動,其中 ServiceManager 用於管理系統中的各種服務。
  • Binder 在 framework 層進行了封裝,通過 JNI 技術呼叫 Native(C/C++)層的 Binder 架構。
  • Binder 在 Native 層以 ioctl 的方式與 Binder 驅動通訊。

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 是 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通訊過程,Binder 使用了一種“硬體”,因此這個模組被稱之為驅動。

熟悉了上面這些概念,我們再來看下上面的圖,使用者空間中 binder_open(), binder_mmap(), binder_ioctl() 這些方法通過 system call 來呼叫核心空間 Binder 驅動中的方法。核心空間與使用者空間共享記憶體通過 copy_from_user(), copy_to_user() 核心方法來完成使用者空間與核心空間記憶體的資料傳輸。 Binder驅動中有一個全域性的 binder_procs 連結串列儲存了服務端的程序資訊。

4. 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 執行緒來處理的。

5. 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 驅動處理完的響應)。

6. 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 命令喚醒等待的執行緒,通知註冊成功。

7. ServiceManager 獲取服務

這裡寫圖片描述

  • 獲取服務的過程與註冊類似,相反的過程。通過 ServiceManager 的 getService() 方法來註冊服務。

  • 首先 ServiceManager 向 Binder 驅動傳送 BC_TRANSACTION 命令攜帶 CHECK_SERVICE_TRANSACTION 命令,同時獲取服務的執行緒進入等待狀態 waitForResponse()。

  • Binder 驅動收到請求命令向 ServiceManager 的傳送 BC_TRANSACTION 查詢已註冊的服務,查詢到直接響應 BR_REPLY 喚醒等待的執行緒。若查詢不到將與 binder_procs 連結串列中的服務進行一次通訊再響應。

8. 進行一次完整通訊

這裡寫圖片描述

  • 我們在使用 Binder 時基本都是呼叫 framework 層封裝好的方法,AIDL 就是 framework 層提供的傻瓜式是使用方式。假設服務已經註冊完,我們來看看客戶端怎麼執行服務端的方法。

  • 首先我們通過 ServiceManager 獲取到服務端的 BinderProxy 代理物件,通過呼叫 BinderProxy 將引數,方法標識(例如:TRANSACTION_test,AIDL中自動生成)傳給 ServiceManager,同時客戶端執行緒進入等待狀態。

  • ServiceManager 將使用者空間的引數等請求資料複製到核心空間,並向服務端插入一條執行執行方法的事務。事務執行完通知 ServiceManager 將執行結果從核心空間複製到使用者空間,並喚醒等待的執行緒,響應結果,通訊結束。

總結

好了,這裡只是從實現邏輯上簡單介紹了下 Binder 機制的工作原理,想要深入理解 Binder 機制,還得自己下功夫,看原始碼,儘管這個過程很痛苦。一遍看不懂就再來一遍,說實話本人理解能力比較差,跟著部落格思路看了不下十遍。努力總會有收穫,好好欣賞 native 層各方法之間花式跳轉的魅力吧。最後你將發現新世界的大門在向你敞開。

參考資料

歡迎長按下圖 -> 識別圖中二維碼
或者 掃一掃 關注我的公眾號

這裡寫圖片描述