Android 通訊篇 -- 深入剖析Binder原理
Binder 概述
Binder 是一種程序間通訊機制,基於開源的 OpenBinder 實現;OpenBinder 起初由 Be Inc. 開發,後由 Plam Inc. 接手。從字面上來解釋 Binder 有膠水、粘合劑的意思,顧名思義就是粘和不同的程序,使之實現通訊。
為什麼要理解 Binder?
一般Android應用開發很少直接用到跨程序信通訊(IPC),但如果你想知道:
? App是如何啟動並初始化的? ? Activity的啟動過程是怎樣的? ? 程序間是如何通訊的? ? AIDL的具體原理是什麼? ? 眾多外掛化框架的設計原理 等等
這些問題的背後都與 Binder 有莫大的關係,要弄懂上面這些問題理解 Bidner 通訊機制是必須的。
我們知道 Android 應用程式是由 Activity、Service、Broadcast Receiver 和 Content Provide 四大元件中的一個或者多個組成的。有時這些元件執行在同一程序,有時執行在不同的程序。這些程序間的通訊就依賴於 Binder IPC 機制。不僅如此,Android 系統對應用層提供的各種服務如:ActivityManagerService、PackageManagerService 等都是基於 Binder IPC 機制來實現的。Binder 機制在 Android 中的位置非常重要,毫不誇張的說理解 Binder 是邁向 Android 高階工程的第一步。
為什麼是 Binder ?
Android 系統是基於 Linux 核心的,Linux 已經提供了 管道、訊息佇列、共享記憶體 和 Socket 等 IPC 機制。那為什麼 Android 還要提供 Binder 來實現 IPC 呢?主要是基於 效能、穩定性 和 安全性 幾方面的原因!
效能
首先說說效能上的優勢。
Socket:作為一款通用介面,其傳輸效率低,開銷大,主要用在跨網路的程序間通訊和本機上程序間的低速通訊。訊息佇列和管道:採用儲存-轉發方式,即資料先從傳送方快取區拷貝到核心開闢的快取區中,然後再從核心快取區拷貝到接收方快取區,至少有兩次拷貝過程。共享記憶體:雖然無需拷貝,但控制複雜,難以使用。Binder
IPC機制 | 資料拷貝次數 |
---|---|
共享記憶體 | 0 |
Binder | 1 |
管道、訊息佇列、Socket | 2 |
穩定性
再說說穩定性,Binder 基於 C/S 架構,客戶端(Client)有什麼需求就丟給服務端(Server)去完成,架構清晰、職責明確又相互獨立,自然穩定性更好。共享記憶體雖然無需拷貝,但是控制負責,難以使用。從穩定性的角度講,Binder 機制是優於共享記憶體的。
安全性
Android 作為一個開放性的平臺,市場上有各類海量的應用供使用者選擇安裝,因此安全性對於 Android 平臺而言極其重要。作為使用者當然不希望我們下載的 APP 偷偷讀取我的通訊錄,上傳我的隱私資料,後臺偷跑流量、消耗手機電量。傳統的 IPC 沒有任何安全措施,完全依賴上層協議來確保。首先傳統的 IPC 接收方無法獲得對方可靠的程序使用者ID/程序ID(UID/PID),從而無法鑑別對方身份。Android 為每個安裝好的 APP 分配了自己的 UID,故而程序的 UID 是鑑別程序身份的重要標誌。傳統的 IPC 只能由使用者在資料包中填入 UID/PID,但這樣不可靠,容易被惡意程式利用。可靠的身份標識只有由 IPC 機制在核心中新增。其次傳統的 IPC 訪問接入點是開放的,只要知道這些接入點的程式都可以和對端建立連線,不管怎樣都無法阻止惡意程式通過猜測接收方地址獲得連線。同時 Binder 既支援實名 Binder,又支援匿名 Binder,安全性高。
傳統IPC通訊原理
瞭解 Linux IPC 相關的概念和原理有助於我們理解 Binder 通訊原理。因此,在介紹 Binder 跨程序通訊原理之前,我們先聊聊 Linux 系統下傳統的程序間通訊是如何實現。
基礎概念
程序隔離
簡單的說就是作業系統中,程序與程序間記憶體是不共享的。兩個程序就像兩個平行的世界,A 程序沒法直接訪問 B 程序的資料,這就是程序隔離的通俗解釋。A 程序和 B 程序之間要進行資料互動就得采用特殊的通訊機制:程序間通訊(IPC)。
程序空間
現在作業系統都是採用的虛擬儲存器,對於 32 位系統而言,它的定址空間(虛擬儲存空間)就是 2 的 32 次方,也就是 4GB。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也可以訪問底層硬體裝置的許可權。為了保護使用者程序不能直接操作核心,保證核心的安全,作業系統從邏輯上將虛擬空間劃分為使用者空間(User Space)和核心空間(Kernel Space)。針對 Linux 作業系統而言,將最高的 1GB 位元組供核心使用,稱為核心空間;較低的 3GB 位元組供各程序使用,稱為使用者空間。
系統呼叫
雖然從邏輯上進行了使用者空間和核心空間的劃分,但不可避免的使用者空間需要訪問核心資源,比如檔案操作、訪問網路等等。為了突破隔離限制,就需要藉助系統呼叫來實現。系統呼叫是使用者空間訪問核心空間的唯一方式,保證了所有的資源訪問都是在核心的控制下進行的,避免了使用者程式對系統資源的越權訪問,提升了系統安全性和穩定性。
Linux 使用兩級保護機制:0 級供系統核心使用,3 級供使用者程式使用。
當一個任務(程序)執行系統呼叫而陷入核心程式碼中執行時,稱程序處於核心執行態(核心態)。此時處理器處於特權級最高的(0級)核心程式碼中執行。當程序處於核心態時,執行的核心程式碼會使用當前程序的核心棧。每個程序都有自己的核心棧。
當程序在執行使用者自己的程式碼的時候,我們稱其處於使用者執行態(使用者態)。此時處理器在特權級最低的(3級)使用者程式碼中執行。
IPC通訊原理
理解了上面的幾個概念,我們再來看看傳統的 IPC 方式中,程序之間是如何實現通訊的。
通常的做法是訊息傳送方將要傳送的資料存放在記憶體快取區中,通過系統呼叫進入核心態。然後核心程式在核心空間分配記憶體,開闢一塊核心快取區,呼叫 copyfromuser() 函式將資料從使用者空間的記憶體快取區拷貝到核心空間的核心快取區中。同樣的,接收方程序在接收資料時在自己的使用者空間開闢一塊記憶體快取區,然後核心程式呼叫 copytouser() 函式將資料從核心快取區拷貝到接收程序的記憶體快取區。這樣資料傳送方程序和資料接收方程序就完成了一次資料傳輸,我們稱完成了一次程序間通訊
我們來看下原理圖:
這種傳統的 IPC 通訊方式有兩個問題:
✨ 1、效能低下,一次資料傳遞需要經歷:記憶體快取區 --> 核心快取區 --> 記憶體快取區,需要 2 次資料拷貝; ✨ 2、接收資料的快取區由資料接收程序提供,但是接收程序並不知道需要多大的空間來存放將要傳遞過來的資料,因此只能開闢儘可能大的記憶體空間或者先呼叫 API 接收訊息頭來獲取訊息體的大小,這兩種做法不是浪費空間就是浪費時間。
Binder跨程序通訊原理
理解了 Linux IPC 相關概念和通訊原理,接下來我們正式介紹下 Binder IPC 的原理。
動態核心可載入模組
正如前面所說,跨程序通訊是需要核心空間做支援的。傳統的 IPC 機制如管道、Socket 都是核心的一部分,因此通過核心支援來實現程序間通訊自然是沒問題的。但是 Binder 並不是 Linux 系統核心的一部分,那怎麼辦呢?這就得益於 Linux 的 動態核心可載入模組(Loadable Kernel Module,LKM)的機制;模組是具有獨立功能的程式,它可以被單獨編譯,但是不能獨立執行。它在執行時被連結到核心作為核心的一部分執行。這樣,Android 系統就可以通過動態新增一個核心模組執行在核心空間,使用者程序之間通過這個核心模組作為橋樑來實現通訊。
在 Android 系統中,這個執行在核心空間,負責各個使用者程序通過 Binder 實現通訊的核心模組就叫 Binder 驅動(Binder Dirver)。
那麼在 Android 系統中使用者程序之間是如何通過這個核心模組(Binder 驅動)來實現通訊的呢?難道是和前面說的傳統 IPC 機制一樣,先將資料從傳送方程序拷貝到核心快取區,然後再將資料從核心快取區拷貝到接收方程序,通過兩次拷貝來實現嗎?顯然不是,否則也不會有開篇所說的 Binder 在效能方面的優勢了。這就涉及到 記憶體對映 的概念了。
記憶體對映
Binder IPC 機制中涉及到的記憶體對映通過 mmap() 來實現,mmap() 是作業系統中一種記憶體對映的方法。記憶體對映簡單的講就是將使用者空間的一塊記憶體區域對映到核心空間。對映關係建立後,使用者對這塊記憶體區域的修改可以直接反應到核心空間;反之核心空間對這段區域的修改也能直接反應到使用者空間。
記憶體對映能減少資料拷貝次數,實現使用者空間和核心空間的高效互動。兩個空間各自的修改能直接反映在對映的記憶體區域,從而被對方空間及時感知。也正因為如此,記憶體對映能夠提供對程序間通訊的支援。
Binder IPC 實現原理
Binder IPC 正是基於記憶體對映(mmap)來實現的,但是 mmap() 通常是用在有物理介質的檔案系統上的。
比如程序中的使用者區域是不能直接和物理裝置打交道的,如果想要把磁碟上的資料讀取到程序的使用者區域,需要兩次拷貝(磁碟-->核心空間-->使用者空間);通常在這種場景下 mmap() 就能發揮作用,通過在物理介質和使用者空間之間建立對映,減少資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高檔案讀取效率。
而 Binder 並不存在物理介質,因此 Binder 驅動使用 mmap() 並不是為了在物理介質和使用者空間之間建立對映,而是用來在核心空間建立資料接收的快取空間。
一次完整的 Binder IPC 通訊過程通常是這樣:
✨ 1、首先 Binder 驅動在核心空間建立一個 資料接收快取區 ; ✨ 2、接著在核心空間開闢一塊核心快取區,建立 核心快取區 和 核心中資料接收快取區 之間的對映關係,以及 核心中資料接收快取區 和 接收程序使用者空間地址 的對映關係; ✨ 3、傳送方程序通過系統呼叫 copyfromuser() 將資料 copy 到核心中的核心快取區,由於核心快取區和接收程序的使用者空間存在記憶體對映,因此也就相當於把資料傳送到了接收程序的使用者空間,這樣便完成了一次程序間的通訊。
我們來看下原理圖:
Binder 通訊模型
介紹完 Binder IPC 的底層通訊原理,接下來我們看看實現層面是如何設計的。
一次完整的程序間通訊必然至少包含兩個程序,通常我們稱通訊的雙方分別為 客戶端程序 (Client)和 服務端程序 (Server),由於程序隔離機制的存在,通訊雙方必然需要藉助 Binder 來實現。
Client/Server/ServiceManager/驅動
前面我們介紹過,Binder 是基於 C/S 架構的。由一系列的元件組成,包括 Client、Server、ServiceManager、Binder 驅動。 ✨ Client、Server、Service Manager 執行在使用者空間,Binder 驅動執行在核心空間。 ✨ Service Manager 和 Binder 驅動由系統提供,而 Client、Server 由應用程式來實現。 ✨ Client、Server 和 ServiceManager 均是通過系統呼叫 open、mmap 和 ioctl 來訪問裝置檔案 /dev/binder,從而實現與 Binder 驅動的互動來間接的實現跨程序通訊。
如下原理圖:
Client、Server、ServiceManager、Binder 驅動這幾個元件在通訊過程中扮演的角色就如同網際網路中伺服器(Server)、客戶端(Client)、DNS域名伺服器(ServiceManager)以及路由器(Binder 驅動)之前的關係。
通常我們訪問一個網頁的步驟是這樣的:首先在瀏覽器輸入一個地址,如 https://www.google.com 然後按下回車鍵。但是並沒有辦法通過域名地址直接找到我們要訪問的伺服器,因此需要首先訪問 DNS 域名伺服器,域名伺服器中儲存了 https://www.google.com 對應的 ip 地址 10.249.23.13,然後通過這個 ip 地址才能找到 https://www.google.com 對應的伺服器。
如下圖所示:
Android Binder 設計與實現一文中對 Client、Server、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 比較特殊,它沒有名字也不需要註冊。當一個程序使用 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 中一個物件有多個引用一樣。
Binder 通訊過程
至此,我們大致能總結出 Binder 通訊過程:
✨ 1、首先,一個程序使用 BINDERSETCONTEXT_MGR 命令通過 Binder 驅動將自己註冊成為 ServiceManager; ✨ 2、Server 通過驅動向 ServiceManager 中註冊 Binder(Server 中的 Binder 實體),表明可以對外提供服務。驅動為這個 Binder 建立位於核心中的實體節點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查詢表。 ✨ 3、Client 通過名字,在 Binder 驅動的幫助下從 ServiceManager 中獲取到對 Binder 實體的引用,通過這個引用就能實現和 Server 程序的通訊。我們看到整個通訊過程都需要 Binder 驅動的接入。下圖能更加直觀的展現整個通訊過程(為了進一步抽象通訊過程以及呈現上的方便,下圖我們忽略了 Binder 實體及其引用的概念):
原理圖:
Binder 通訊中的代理模式
我們已經解釋清楚 Client、Server 藉助 Binder 驅動完成跨程序通訊的實現機制了,但是還有個問題會讓我們困惑。A 程序想要 B 程序中某個物件(object)是如何實現的呢?畢竟它們分屬不同的程序,A 程序沒法直接使用 B 程序中的 object。
前面我們介紹過跨程序通訊的過程都有 Binder 驅動的參與,因此在資料流經 Binder 驅動的時候驅動會對資料做一層轉換。當 A 程序想要獲取 B 程序中的 object 時,驅動並不會真的把 object 返回給 A,而是返回了一個跟 object 看起來一模一樣的代理物件 objectProxy,這個 objectProxy 具有和 object 一模一樣的方法,但是這些方法並沒有 B 程序中 object 物件那些方法的能力,這些方法只需要把請求引數交給驅動即可。對於 A 程序來說和直接呼叫 object 中的方法是一樣的。
當 Binder 驅動接收到 A 程序的訊息後,發現這是個 objectProxy 就去查詢自己維護的表單,一查發現這是 B 程序 object 的代理物件。於是就會去通知 B 程序呼叫 object 的方法,並要求 B 程序把返回結果發給自己。當驅動拿到 B 程序的返回結果後就會轉發給 A 程序,一次通訊就完成了。
原理圖:
Binder 的完整定義
現在我們可以對 Binder 做個更加全面的定義了:
✨ 從程序間通訊的角度看,Binder 是一種程序間通訊的機制; ✨ 從 Server 程序的角度看,Binder 指的是 Server 中的 Binder 實體物件; ✨ 從 Client 程序的角度看,Binder 指的是對 Binder 代理物件,是 Binder 實體物件的一個遠端代理 ✨ 從傳輸過程的角度看,Binder 是一個可以跨程序傳輸的物件;Binder 驅動會對這個跨越程序邊界的物件對一點點特殊處理,自動完成代理物件和本地物件之間的轉換。