摘要:本節主要來講解Android10.0 Binder的設計原理,如何設計一個Binder通信
閱讀本文大約需要花費15分鐘。
文章首發微信公衆號:IngresGe
專注於Android系統級源碼分析,Android的平臺設計,歡迎關注我,謝謝!
[Android取經之路] 的源碼都基於Android-Q(10.0) 進行分析
[Android取經之路] 系列文章:
《系統啓動篇》
- Android系統架構
- Android是怎麼啓動的
- Android 10.0系統啓動之init進程
- Android10.0系統啓動之Zygote進程
- Android 10.0 系統啓動之SystemServer進程
- Android 10.0 系統服務之ActivityMnagerService
- Android10.0系統啓動之Launcher(桌面)啓動流程
- Android10.0應用進程創建過程以及Zygote的fork流程
- Android 10.0 PackageManagerService(一)工作原理及啓動流程
- Android 10.0 PackageManagerService(二)權限掃描
- Android 10.0 PackageManagerService(三)APK掃描
- Android 10.0 PackageManagerService(四)APK安裝流程
《日誌系統篇》
- Android10.0 日誌系統分析(一)-logd、logcat 指令說明、分類和屬性
- Android10.0 日誌系統分析(二)-logd、logcat架構分析及日誌系統初始化
- Android10.0 日誌系統分析(三)-logd、logcat讀寫日誌源碼分析
- Android10.0 日誌系統分析(四)-selinux、kernel日誌在logd中的實現
《Binder通信原理》
- Android10.0 Binder通信原理(一)Binder、HwBinder、VndBinder概要
- Android10.0 Binder通信原理(二)-Binder入門篇
- Android10.0 Binder通信原理(三)-ServiceManager篇
- Android10.0 Binder通信原理(四)-Native-C\C++實例分析
- Android10.0 Binder通信原理(五)-Binder驅動分析
- Android10.0 Binder通信原理(六)-Binder數據如何完成定向打擊
- Android10.0 Binder通信原理(七)-Framework binder示例
- Android10.0 Binder通信原理(八)-Framework層分析
- Android10.0 Binder通信原理(九)-AIDL Binder示例
- Android10.0 Binder通信原理(十)-AIDL原理分析-Proxy-Stub設計模式
- Android10.0 Binder通信原理(十一)-Binder總結
1.概述
在Android應用、Android系統開發的時候,相信很多人都聽過Binder的概念,而且無意間就用到了Binder機制,例如:寫一個應用打開手電筒功能,某個應用啓動服務等。
這些動作都涉及到一個概念-進程間通信。Android 中的每個應用都是獨立的進程,都有自己虛擬內存,兩個進程之間不能互相訪問數據。
在Android中,應用進程間互相訪問數據,常用的通信方式就Binder。
從前一節,我們知道從Android 8.0 開始,Binder機制,被拆分成了Binder(System分區 進程間通信)、HwBinder(支持System/Vendor分區進程間通信)、VndBinder(Vendor分區進程間通信)。
現在我們先單獨分析一下Binder的機制,HwBinder和VndBinder留到後面慢慢分析。
2.Binder通信模型
下圖中涉及到Binder模型的4類角色:Binder驅動,ServiceManager,Server和Client。Binder機制的目的是實現IPC(Inter-Process Communication),即Client和Server之間的通信。
其中Server,Client,ServiceManager運行於用戶空間,Binder驅動運行於內核空間。這四個角色的關係和互聯網類似:Server是服務器,Client是客戶終端,ServiceManager是域名服務器(DNS),驅動是路由器。
3.一些概念的理解
1)IPC (進程間通信-Inter process communication)
IPC屬於通信機制,Android中常用的IPC通信:管道、共享內存、消息隊列、信號量、socket、binder。
2)RPC (遠程過程調用 Remote Procedure call)
RPC屬於通信機制中的調用方法,目的:不同的進程之間,一個進程調用另一個進程的對象。
RPC在調用一個遠程過程後,自己進入等待狀態,傳往遠程過程的參數包括過程參數,返回參數包括執行結果;
當收到包括執行結果的消息後,本地進程從消息中取得結果,調用進程重新開始執行。在服務器一方,有一個程序在等待調用,當有一個調用到達時,服務器進程取得進程參數,計算結果,然後返回結果。
調用可以同步的也可以是異步的;服務器可以創建一個線程來接收用戶請求,也可以自己來接收用戶請求
3)代理模式
爲其他對象提供代理對象,以控制對這個對象的訪問。
由於進程隔離的存在,一個進程內部的對象對另外一個進程來說沒有任何意義。另外如果是代理對象的話,它可以存在各個進程內,就好比咱們的AMS和PMS。
4)進程隔離
進程隔離是爲保護操作系統中進程互不干擾而設計的一組不同硬件和軟件的技術。這個技術是爲了避免進程A寫入進程B的情況發生。進程的隔離實現,使用了虛擬地址空間。進程A的虛擬地址和進程B的虛擬地址不同,這樣就防止進程A將數據信息寫入進程B。
5)內核空間
可以訪問受保護的內存空間,有訪問底層硬件設備的所有權限。
6)用戶空間
相對於內核空間,上層應用程序所運行的空間以及Native層進程運行的空間就是用戶空間,用戶空間訪問內核空間的唯一方式就是系統調用。
7)系統調用/內核態/用戶態
從邏輯上看,內核空間和用戶空間是獨立的,那麼用戶空間總要有辦法調用內核空間,唯一的調用方式就是系統調用(System Call),通過這個統一入口接口,所有的資源訪問都是在內核的控制下執行,以免導致對用戶程序對系統資源的越權訪問,從而保障了系統的安全和穩定。
當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核運行態(或簡稱爲內核態)。
當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。
4.什麼是Binder?
Binder的英文意思是粘結劑,把兩個不相關的進程粘在一起,讓兩個進程可以進行數據交互。 比如我們寫一個應用,打開手電筒功能,那麼要把打開這個動作,發給管理服務,這個發送的過程就是通過Binder機制來實現。
5.爲什麼要用Binder?
Android使用的Linux內核擁有着非常多的跨進程通信機制,比如管道、共享內存、消息隊列、信號量、socket等;爲什麼還需要單獨搞一個Binder出來呢?
主要有兩點,性能和安全。
1)高效
在移動設備上,廣泛地使用跨進程通信肯定對通信機制本身提出了嚴格的要求;Binder相對出傳統的Socket方式,更加高效;
IPC 內存拷貝次數:
Binder只需要一次拷貝就能將A進程用戶空間的數據爲B進程所用
數據從用戶空間拷貝到內核中的時候,是直接拷貝到目標進程的內核空間,這個過程是在請求端線程中處理的,只不過操作對象是目標進程的內核空間。
Binder拷貝方式: 數據發送端(虛擬內存) copy_from_user --> 內核虛擬內存 <--mmap--> 數據接收端(虛擬內存)
內核虛擬內存和數據接收端虛擬內存採用mmap映射到同一塊物理內存,不存在拷貝動作,數據發送端(Client)要把IPC數據 拷貝到內核虛擬內存空間,存在一次拷貝,所以Binder只存在一次內存拷貝
管道、隊列等需要兩次內存拷貝:
發送方緩存區--memcpy-->內核緩存區 --memcpy-->接收方緩存區 ,存在兩次拷貝
共享內存SMD(Shared Memory Driver),雖然無需拷貝,但控制複雜,難以使用。
socket作爲一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通信和本機上進程間的低速通信。
2)安全
傳統的進程通信方式對於通信雙方的身份並沒有做出嚴格的驗證,接收方無法獲得對方進程可靠的UID/PID(用戶ID/進程ID),只有在上層協議上進行架設;
比如Socket通信ip地址是客戶端手動填入的,都可以進行僞造;而Binder機制爲每個進程分配了UID/PID來作爲鑑別身份的標示,從協議本身就支持對通信雙方做身份校檢,因而大大提升了安全性。
3)可以很好的實現Client-Server(CS)架構
對於Android系統,Google想提供一套基於Client-Server的通信方式。當Client需要獲取某Server的服務時,只需要Client向Server發送相應的請求,Server收到請求之後進行處理,處理完畢再將反饋內容發送給Client。
但是,目前Linux支持的"傳統的管道/消息隊列/共享內存/信號量/Socket等"IPC通信手段中,只有Socket是Client-Server的通信方式。但是,Socket主要用於網絡間通信以及本機中進程間的低速通信,它的傳輸效率太低。
6.如何設計一個Binder?
在理解Binder架構前,我們來考慮下,如果是你,該如何設計一個Binder的進程間通信機制。
要實現一個IPC通信那麼需要幾個核心要素:
1)發起端:肯定包括髮起端所從屬的進程,以及實際執行傳輸動作的線程
2)接收端:接收發送端的數據。
3)待傳輸的數據
4)內存映射,內核態
首先先畫一個最簡單的IPC通信圖:
進程Process1和進程Process2 通過IPC通信機制進行通信。
再進行擴展調整,把IPC機制換成Binder機制,那麼就變成如下的圖形:
由於Android存在進程隔離,那麼兩個進程之間是不能直接傳輸數據的,Process1需要得到Process2的代理,Process2需要一個實體。
爲了實現RPC,我們的代理都是提供接口,稱爲“接口代理”,實體需要提供“接口實體”,如下圖所示:
我們把代理改成BpBinder,實體改成BBinder,接口代理改成BpInterface,接口實現體改成BnInterface。
我們都知道兩個進程的數據共享,需要陷入內核態,那就需要一個驅動設備“/dev/binder”,同時需要一個守護進行來進行service管理,我們成爲ServiceManager。
進一步演變爲:
假如我們想要把通過Process1 的微信信息發送給Process2的微信,我們需要做下面幾步:
0)Process2在騰訊服務器中進行註冊(包括微信名稱、當前活動的IP地址等)
1)Process1從朋友列表中中查找到 Process2的名稱,這就是Process2的別名:"service_name"
2)Process1 編寫消息消息內容,點擊發送。 這裏的消息內容就是IPC數據
3)數據會發送到騰訊的服務器,服務器理解爲Binder驅動
4)服務器從數據庫中解析出IPC數據,找到Process2信息,轉到Process2註冊的地址, 數據庫理解爲ServiceManager
5)把數據發給Process2,完成Process1和Process2的通信
我們可以簡單的把上面的順序內容進行轉換:
1)Binder驅動---騰訊服務器
2)數據庫--ServiceManager
3)Service_name: Process2的微信名稱
4)IPC數據:Process1 發送的微信消息
Native C/C++和內核進行通信需要通過系統調用,ServiecManager的主要用來對Service管理,提供了add\find\list等操作。Native進程的數據直接可以通過系統調用陷入內核態,進入圖像轉換,變爲如下:
上面列舉的是Native C/C++空間的進程進行Binder通信機制,那麼JAVA層是如何通信的呢,Native層的Binder提供的是libbinder.so,那麼從JAVA到Native,需要經過JNI、Framework層的封裝,
JNI層的命名通常爲android_util_xxx,我們這裏是binder機制,那麼JNI層的文件爲 android_util_binder,同時Native的BBinder不能直接傳給JAVA層,在JNI裏面轉換了一個JavaBBinder對象。
Framework層給應用層提供時,其實提供的也是一個代理,我們也稱之爲BinderProxy。
在JAVA側要對應一個Binder的實體,稱之爲Binder。
JAVA側的服務進行也需要一個管理者,類似於Native,創建了JAVA的ServiceManager,那麼設計如下:
7.Binder的通信原理
Binder 通信採用 C/S 架構,從組件視角來說,包含 Client、 Server、 ServiceManager 以及 Binder 驅動,其中 ServiceManager 用於管理系統中的各種服務。
Binder 在 framework 層進行了封裝,通過 JNI 技術調用 Native(C/C++)層的 Binder 架構。
Binder 在 Native 層以 ioctl 的方式與 Binder 驅動通訊。
Binder通信流程如下:
1.首先服務端需要向ServiceManager進行服務註冊,ServiceManager有一個全局的service列表svcinfo,用來緩存所有服務的handler和name。
2.客戶端與服務端通信,需要拿到服務端的對象,由於進程隔離,客戶端拿到的其實是服務端的代理,也可以理解爲引用。客戶端通過ServiceManager從svcinfo中查找服務,ServiceManager返回服務的代理。
3.拿到服務對象後,我們需要向服務發送請求,實現我們需要的功能。通過 BinderProxy 將我們的請求參數發送給 內核,通過共享內存的方式使用內核方法 copy_from_user() 將我們的參數先拷貝到內核空間,這時我們的客戶端進入等待狀態。然後 Binder 驅動向服務端的 todo 隊列裏面插入一條事務,執行完之後把執行結果通過 copy_to_user() 將內核的結果拷貝到用戶空間(這裏只是執行了拷貝命令,並沒有拷貝數據,binder只進行一次拷貝),喚醒等待的客戶端並把結果響應回來,這樣就完成了一次通訊。
在這裏其實會存在一個問題,Client和Server之間通信是稱爲進程間通信,使用了Binder機制,那麼Server和ServiceManager之間通信也叫進程間通信,Client和Server之間還會用到ServiceManager,也就是說Binder進程間通信通過Binder進程間通信來完成,這就好比是 孵出雞前提卻是要找只雞來孵蛋,這是怎麼實現的呢?
Binder的實現比較巧妙:預先創造一隻雞來孵蛋:ServiceManager和其它進程同樣採用Binder通信,ServiceManager是Server端,有自己的Binder對象(實體),其它進程都是Client,需要通過這個Binder的引用來實現Binder的註冊,查詢和獲取。
ServiceManager提供的Binder比較特殊,它沒有名字也不需要註冊,當一個進程使用BINDER_SET_CONTEXT_MGR_EXT命令將自己註冊成ServiceManager時Binder驅動會自動爲它創建Binder實體(這就是那隻預先造好的雞)。
其次這個Binder的引用在所有Client中都固定爲0(handle=0)而無須通過其它手段獲得。也就是說,一個Server若要向ServiceManager註冊自己Binder就必須通過0這個引用號和ServiceManager的Binder通信。
類比網絡通信,0號引用就好比域名服務器的地址,你必須預先手工或動態配置好。要注意這裏說的Client是相對ServiceManager而言的,一個應用程序可能是個提供服務的Server,但對ServiceManager來說它仍然是個Client。
圖片來源csdn-jeanboydev
8.Binder的內存管理
ServiceManager啓動後,會通過系統調用mmap向內核空間申請128K的內存,用戶進程會通過mmap向內核申請(1M-8K)的內存空間。
這裏用戶空間mmap (1M-8K)的空間,爲什麼要減去8K,而不是直接用1M?
Android的git commit記錄:
Modify the binder to request 1M - 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space.
大致的意思是:kernel的“backing store”需要一個保護頁,這使得1M用來分配碎片內存時變得很差,所以這裏減去兩頁來提高效率,因爲減去一頁就變成了奇數。
系統定義:BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) = (1M- sysconf(_SC_PAGE_SIZE) * 2)
這裏的8K,其實就是兩個PAGE的SIZE, 物理內存的劃分是按PAGE(頁)來劃分的,一般情況下,一個Page的大小爲4K。
內核會增加一個guard page,再加上內核本身的guard page,正好是兩個page的大小,減去後,就是用戶空間可用的大小。
在內存分配這塊,還要分爲32位和64位,32位的系統很好區分,虛擬內存爲4G,用戶空間從低地址開始佔用3G,內核空間佔用剩餘的1G。
ARM32內存佔用分配:
但隨着現在的硬件發展越來越迅速,應用程序的運算也越來越複雜,佔用空間越來越大,原有的4G虛擬內存已經不能滿足用戶的需求,因此,現在的Android基本都是用64位的內存機制。
理論上講,64位的地址總線可以支持高達16EB(2^64)的內存。AMD64架構支持52位(4PB)的地址總線和48位(256TB)的虛擬地址空間。在linux arm64中,如果頁的大小爲4KB,使用3級頁錶轉換或者4級頁錶轉換,用戶空間和內核空間都支持有39bit(512GB)或者48bit(256TB)大小的虛擬地址空間。
2^64 次方太大了,Linux 內核只採用了 64 bits 的一部分(開啓 CONFIG_ARM64_64K_PAGES 時使用 42 bits,頁大小是 4K 時使用 39 bits),該文假設使用的頁大小是 4K(VA_BITS = 39)
ARM64 有足夠的虛擬地址,用戶空間和內核空間可以有各自的 2^39 = 512GB 的虛擬地址。
ARM64內存佔用分配:
用戶地址空間(服務端-數據接收端)和內核地址空間都映射到同一塊物理地址空間。
Client(數據發送端)先從自己的用戶進程空間把IPC數據通過copy_from_user()拷貝到內核空間。而Server端(數據接收端)與內核共享數據(mmap到同一塊物理內存),不再需要拷貝數據,而是通過內存地址空間的偏移量,即可獲悉內存地址,整個過程只發生一次內存拷貝。
圖片來源於Gityuan
9.Binder中各角色之間關係
下圖展示了Binder中各個角色之間的關係:
圖片來源:universus
1)Binder實體
Binder實體,是各個Server以及ServiceManager在內核中的存在形式。
Binder實體實際上是內核中binder_node結構體的對象,它的作用是在內核中保存Server和ServiceManager的信息(例如,Binder實體中保存了Server對象在用戶空間的地址)。簡言之,Binder實體是Server在Binder驅動中的存在形式,內核通過Binder實體可以找到用戶空間的Server對象。
在上圖中,Server和ServiceManager在Binder驅動中都對應的存在一個Binder實體。
2)Binder引用\代理
說到Binder實體,就不得不說"Binder引用"。所謂Binder引用,實際上是內核中binder_ref結構體的對象,它的作用是在表示"Binder實體"的引用。換句話說,每一個Binder引用都是某一個Binder實體的引用,通過Binder引用可以在內核中找到它對應的Binder實體。
如果將Server看作是Binder實體的話,那麼Client就好比Binder引用。Client要和Server通信,它就是通過保存一個Server對象的Binder引用,再通過該Binder引用在內核中找到對應的Binder實體,進而找到Server對象,然後將通信內容發送給Server對象。
Binder實體和Binder引用都是內核(Binder驅動)中的數據結構。每一個Server在內核中就表現爲一個Binder實體,而每一個Client則表現爲一個Binder引用。這樣,每個Binder引用都對應一個Binder實體,而每個Binder實體則可以多個Binder引用。
3)遠程服務
Server都是以服務的形式註冊到ServiceManager中進行管理的。如果將Server本身看作是"本地服務"的話,那麼Client中的"遠程服務"就是本地服務的代理。遠程服務就是本地服務的一個代理,通過該遠程服務Client就能和Server進行通信。
4)ServiceManager守護進程
ServiceManager是用戶空間的一個守護進程。當該應用程序啓動時,它會和Binder驅動進行通信,告訴Binder驅動它是服務管理者;對Binder驅動而言,它則會新建ServiceManager對應的Binder實體,並將該Binder實體設爲全局變量。
10.源碼路徑:
1)binder驅動
/kernel/msm-4.9/drivers/android/*
2)servicemanager
/frameworks/native/cmds/servicemanager/*
3)libbinder
/frameworks/native/libs/binder/*
4)JAVA層
/frameworks/base/core/java/android/os/*
源碼下載:
https://github.com/LineageOS
我的微信公衆號:IngresGe
參考: