藉助 AIDL 理解 Android Binder 機制——Binder 來龍去脈

guanpj發表於2019-01-08

AIDL 是 Android Interface Definition Language(Android 介面定義語言)的縮寫,它是 Android 程式間通訊的介面語言。由於 Android 系統的核心是 Linux,它採用了程式隔離機制,使得不同的應用程式執行在不同的程式當中,有時候兩個應用之間需要傳遞或者共享某些資料,就需要進行程式間的通訊訊。

Android 程式間通訊的方式有很多種,比如 Messenger、檔案(SharePreference)、AIDL、Socket 和 Content Provider 等,它們當中 Messenge、AIDL 和 Content Provider 的底層都是依賴於 Binder 機制去實現的。除此之外,Android 四大元件的啟動和通訊的核心過程也是通過 Binder 機制去實現的,這裡,我們藉助 AIDL 來了解 Binder 的實現機制。

在瞭解 AIDL 機制和用法之前,首先要了解幾個概念,這對後續的深入理解有較大的幫助。

程式隔離

以下內容來自維基百科

程式隔離是為保護作業系統中程式互不干擾而設計的一組不同硬體和軟體的技術。這個技術是為了避免程式 A 寫入程式 B 的情況發生。 程式的隔離實現,使用了虛擬地址空間。程式 A 的虛擬地址和程式B的虛擬地址不同,這樣就防止程式 A 將資料資訊寫入程式 B。

Linux IPC 原理

由於 Linux 採用了虛擬地址空間技術,作業系統在邏輯上將虛擬記憶體分為使用者空間(User Space)核心空間(Kernel Space),普通應用程式執行在使用者空間,系統核心執行在核心空間,為了控制應用程式的訪問範圍、保證系統安全,使用者空間只能通過系統呼叫的方式去訪問核心空間。當程式執行系統呼叫而陷入核心程式碼的時候,該程式則進入了核心態,相比之下,程式在使用者空間執行自己的程式碼的時候,則是處於使用者態

由於程式 A 和程式 B 的虛擬地址不同,因此它們之間是相互透明的,都以為自己獨享了系統的資源,當然也不能直接跟對方互動。但是,有些情況下有些程式難免會需要跟其他程式進行互動,這個互動過程就叫 IPC(Inter-Process Communication,程式間通訊)。IPC 的實質就是資料的互動,因此我們這裡將進行 IPC 過程中的通訊呼叫方和被呼叫放分別稱為資料傳送方和資料接收方,IPC 通訊的過程如下:

  1. 資料傳送方程式將資料放在記憶體快取區,通過系統呼叫陷入核心態
  2. 核心程式在核心空間開闢一塊核心快取區,通過 copy_from_user 函式將資料從資料傳送方使用者空間的記憶體快取區拷貝到核心空間的核心快取區中
  3. 資料接收方程式在自己的使用者空間開闢一塊記憶體快取區
  4. 核心程式將核心快取區中通過 copy_to_user 函式將資料拷貝到資料接收方程式的記憶體快取區

Linux IPC

通過以上過程,一次 IPC 就完成了,但是這種傳統的 IPC 機制有兩個問題:

  • 效能比較低:整個過程資料的傳遞需要經歷傳送方記憶體快取區——核心快取區——接收方記憶體快取區的過程
  • 接收方程式事先不知道需要開闢多大的記憶體用於存放資料,因此需要開闢儘可能大的空間或者事先呼叫 API 來解決這個問題,這兩種方式不是浪費空間就是浪費時間。

Binder IPC 原理

為了克服 Linux 傳統的 IPC 機制中的不足之處,Android 系統引入了 Binder 機制,從字面上看 Binder 是膠水的意思,在這裡,Binder 的職責是在不同的程式之間扮演一個橋樑的角色,讓它們之間能夠相互通訊。從上一小節內容可以瞭解到,程式間的通訊少不了 Linux 核心的支援,而 Binder 並不屬於核心的一部分,但是,得益於 Linux 的 LKM(Loadable Kernel Module) 機制:

模組是具有獨立功能的程式,它可以被單獨編譯,但不能獨立執行。它在執行時被連結到核心作為核心的一部分在核心空間執行

因此,Binder 作為這種模組存在於核心之中,也稱為 Binder 驅動。回顧上一小節的內容,傳統 Linux IPC 的過程需要經歷兩次資料拷貝,Binder 藉助 Linux 的另一個特性,只用一次資料拷貝,就能實現 IPC 過程,這就是記憶體對映

Binder IPC 機制中涉及到的記憶體對映通過 mmap() 來實現,mmap() 是作業系統中一種記憶體對映的方法。記憶體對映簡單的講就是將使用者空間的一塊記憶體區域對映到核心空間,對映關係建立後,使用者對這塊記憶體區域的修改可以直接反應到核心空間;反之核心空間對這段區域的修改也能直接反應到使用者空間。

記憶體對映能減少資料拷貝次數,實現使用者空間和核心空間的高效互動。兩個空間各自的修改能直接反映在對映的記憶體區域,從而被對方空間及時感知。也正因為如此,記憶體對映能夠提供對程式間通訊的支援。

Binder IPC 通訊過程如下:

  1. Binder 驅動在核心空間建立一個資料接收快取區
  2. 然後在核心空間開闢一塊記憶體快取區並與資料接收快取區建立對映關係,同時,建立資料接收快取區資料接收方的記憶體快取區的對映關係
  3. 資料傳送方通過系統呼叫 copy_from_user 函式將資料從記憶體快取區拷貝到核心快取區,由於核心快取區通過資料接收快取區跟資料接收方的記憶體快取區存在間接的對映關係,相當於將資料直接拷貝到了接收方的使用者空間,這樣便完成了一次 IPC 的過程。

Android IPC

Binder 通訊模型和通訊過程

在進行 Binder IPC 的時候,實際情況比上面介紹的要複雜,Binder 通訊模型是基於 C/S 架構的,通訊呼叫方程式稱為 Client 程式,被呼叫方稱為 Server 程式,除此之外還需要 ServiceManager 和 Binder 驅動的參與,它們都是通過 open/mmap/iotl 等系統呼叫來訪問裝置檔案 dev/binder 來實現 IPC 過程的。

Binder IPC module

其中,Client、Server 和 ServiceManager 執行在使用者空間,Binder Driver 執行在核心空間,Client 和 Server 需由使用者自己實現,ServiceManager 和 Binder Driver 則由系統提供。

Android Binder 設計與實現 文章中對 Client 和 Server 等角色有詳細的描述:

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 比較特殊,它沒有名字也不需要註冊。當一個程式使用 BINDERSETCONTEXT_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 中一個物件有多個引用一樣。

因此,ServiceManager 是 Binder IPC 通訊過程的核心,是上下文的管理者,Binder 服務端必須先向 ServerManager 註冊才能夠為客戶端提供服務,Binder 客戶端在與服務端通訊之前需要從 ServerManager 中查詢並獲取 Binder 服務端的引用。Binder IPC 過程可以總結成以下步驟:

  1. 某個程式使用 BINDER_SET_CONTEXT_MGR 命令通過 Binder 驅動將自己註冊成 ServiceManager,負責管理所有的 Service
  2. 各個 Server 通過 Binder 驅動向 ServiceManager 註冊 Binder 實體,表明自己可以對外提供服務,這時 Binder 驅動會為這個 Binder 建立位於核心中的實體節點以及 ServiceManager 對該節點的引用,並將名字和該引用打包給 ServiceManager,ServiceManager 接收到資料包後將資料包中的名字和引用填入查詢表中
  3. Client 通過上面 Server 的名字在 Binder 驅動的幫助下從 ServiceManager 中獲取到該 Server 對應的 Binder 引用物件,由於該引用物件同樣具有 Server 的能力,因此 Client 可以通過這個引用與真實的 Server 進行互動

還是universus 老師的圖:

Binder Role

總結

程式隔離雖然使作業系統的安全性和應用程式的穩定性得到了提升,但同時也給 IPC 帶來了一定的難度,Android 系統巧妙地應用了 Binder 機制,使得系統得於在儲存空間和硬體效能等有限的移動裝置上能夠流暢地執行。關於 Binder 在應用層的使用和分析,請看下一篇文章內容:藉助 AIDL 理解 Android Binder 機制——AIDL 的使用和原理分析

參考文章

寫給 Android 應用工程師的 Binder 原理剖析

Binder學習指南

如果你對文章內容有疑問或者有不同意見,歡迎留言,我們一同探討。

相關文章