引言
在寫這篇文章之前,參考了很多資料,但是依舊不敢下筆(或者說是不知道從何下筆)。怕自己理解有誤差,對大家造成不好的影響,貽笑大方。又擔心自己理解的不夠透徹,無法清晰準確的表達出Binder的設計思想。直到現在還是戰戰兢兢,只能說是給出一點自己對binder機制的淺顯的理解,以此來拋磚引玉。
1. 為什么研究Binder?
從了解四大組件底層的通信機制,到理解系統的各種Service,到AIDL的實現原理這些的背后深入了解下去,都有著Binder的影子。所以了解Binder機制對理解android系統有著重要的作用。
但是Binder機制細節過于復雜,所以這里主要是從宏觀的層面去試著理解Binder中的各種概念和基本的通信過程,側重于Java層的實現,驅動層不做過多的介紹。
2.概念解釋
Android系統是基于Linux內核,先來了解一下一些基礎的概念。
進程隔離
"進程隔離是為保護操作系統中進程互不干擾而設計的一組不同硬件和軟件的技術。這個技術是為了避免進程A寫入進程B的情況發生。 進程的隔離實現,使用了虛擬地址空間。進程A的虛擬地址和進程B的虛擬地址不同,這樣就防止進程A將數據信息寫入進程B。"這段引用自維基百科,正是由于進程隔離的存在,所以需要一種機制才能完成一個進程與另外一個進程的通信。
用戶空間/內核空間
用戶空間訪問內核空間的唯一方式就是系統調用;通過這個統一入口接口,所有的資源訪問都是在內核的控制下執行,以免導致對用戶程序對系統資源的越權訪問,從而保障了系統的安全和穩定。用戶軟件良莠不齊,要是它們亂搞把系統玩壞了怎么辦?因此對于某些特權操作必須交給安全可靠的內核來執行。
當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處于內核運行態(或簡稱為內核態)此時處理器處于特權級最高的(0級)內核代碼中執行。當進程在執行用戶自己的代碼時,則稱其處于用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶代碼中運行。處理器在特權等級高的時候才能執行那些特權CPU指令。
內核模塊/驅動
傳統的Linux通信機制,比如Socket,管道等都有內核支持,Binder不屬于Linux內核的一部分,它是通過Linux的動態可加載內核模塊(Loadable Kernel Module)機制來實現的。模塊是具有獨立功能的程序,它可以被單獨編譯,但不能獨立運行。它在運行時被鏈接到內核作為內核的一部分在內核空間運行。
驅動就是操作硬件的接口,Binder驅動就是這樣一個運行在內核空間的,負責各個用戶進程通信的內核模塊。
內存映射
"即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。"
內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關系建立后,用戶對這塊內存區域的修改可以直接反應到內核空間;反之內核空間對這段區域的修改也能直接反應到用戶空間。
3.為什么是Binder??
傳統的Linux通信方式有Socket、共享內存、管道、消息隊列、信號量等。Android使用的Binder機制不屬于Linux。Android不繼承Linux中原有的IPC方式,而選擇使用Binder,說明Binder具有一定的優勢。
1.從傳輸性能上說,Socket作為一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通信;消息隊列和管道采用存儲-轉發方式,即數據先從發送方拷貝到內存開辟的緩存區中,然后再從內核緩存區拷貝到接收方緩存區,至少有兩次拷貝過程;共享內存雖然無需拷貝,但控制復雜,難以使用;而Binder只需要拷貝一次;
2.從安全性上說,Linux傳統的IPC沒有任何安全措施,完全依賴上層協議來確保,具體有以下兩點表現:第一,傳統IPC的接收方無法獲得對方可靠的UID/PID(用戶ID/進程ID),從而無法鑒別對方身份,使用傳統IPC時只能由用戶在數據包里填入UID/PID,但這樣不可靠,容易被惡意程序利用;第二,傳統IPC的訪問接入點是開放的,無法建立私有通信,只要知道這些接入點的程序都可以和對端建立連接,這樣無法阻止惡意程序通過猜測接收方的地址獲得連接。
4.Binder IPC 實現原理?
Binder IPC 正是基于內存映射(mmap)來實現的,但是 mmap() 通常是用在有物理介質的文件系統上的。比如進程中的用戶區域是不能直接和物理設備打交道的,如果想要把磁盤上的數據讀取到進程的用戶區域,需要兩次拷貝(磁盤-->內核空間-->用戶空間);通常在這種場景下 mmap() 就能發揮作用,通過在物理介質和用戶空間之間建立映射,減少數據的拷貝次數,用內存讀寫取代I/O讀寫,提高文件讀取效率。
而 Binder 并不存在物理介質,因此 Binder 驅動使用 mmap() 并不是為了在物理介質和用戶空間之間建立映射,而是用來在內核空間創建數據接收的緩存空間。
一次完整的 Binder IPC 通信過程通常是這樣:
1.首先 Binder 驅動在內核空間創建一個數據接收緩存區;
2.接著在內核空間開辟一塊內核緩存區,建立內核緩存區和內核中數據接收緩存區之間的映射關系,以及內核中數據接收緩存區和接收進程用戶空間地址的映射關系;
3.發送方進程通過系統調用 copy_from_user() 將數據 copy 到內核中的內核緩存區,由于內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當于把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信(其實與?copy_from_user()相對應的還有一個copy_to_user()函數用于從內核區中讀取數據到用戶區)。
5.Binder的架構
從上圖可以明顯的看出Binder 通信采用 C/S 架構,從組件的視角來看,包括Client、Server、 Service Manager、Binder驅動。Client、Server、Service Manager運行在用戶空間,Binder驅動運行在內核空間。Service Manager和Binder驅動已經由系統提供,我們只需要實現Client、Server端。
6.Binder的通信模型
從前文中得知Binder的通信離不開Client、Server、Service Manager、Binder驅動這四部分。為了解釋他們在通信過程中扮演的角色,網上有人形象的做了類比。如同互聯網中服務器(Server)、客戶端(Client)、DNS域名服務器(ServiceManager)、路由器(Binder驅動)之間的關系。
我們訪問一個網頁的時候,在瀏覽器輸入www.baidu.com,然后通過路由的轉發功能在DNS服務器中把www.baidu.com置換為具體的IP地址115.239.211.112,然后通過這個IP地址訪問到百度對應的服務器。
Android Binder 設計與實現一文中對Client、Server、Service Manager、Binder驅動之間的關系作了很詳細的描述。以下摘錄部分內容來補充對通信過程的解釋。
簡單點總結就是:
1.一個進程使用BINDER_SET_CONTEXT_MGR命令通過Binder驅動將自己注冊成ServiceManager。
2.Server 向 ServiceManager 中注冊 Binder(Server 中的 Binder 實體),表明可以對外提供服務。驅動為這個 Binder 創建位于內核中的實體節點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查找表。
3.Client 通過名字,在 Binder 驅動的幫助下從 ServiceManager 中獲取到對 Binder 實體的引用,通過這個引用就能實現和 Server 進程的通信。
7.Binder 通信中的代理模式:
進程之間通信的數據都會經過在內核空間的驅動,驅動在數據經過的時候做了一些處理,它并不會真正的給Client進程返回一個object對象,而且返回一個看起來跟object一模一樣的的代理objectProxy,這個objectProxy也有對象內的方法,比如方法a(),但是a()方法只是一個傀儡,不具備Server進程里面object對象的a方法的實現,唯一的作用就是包裝參數然后交給驅動層(這里簡化了ServiceManager的流程)。
但是這些對Client進程來說是未知的,Client仍然直接調用objectProxy對象然后調用a()方法,但正如上邊所說的,a()方法直接把參數包裝后轉發給Binder驅動層處理。
驅動層收到這個消息,驗證是objectProxy,然后就查詢自己維護的表單,發現之前是用objectProxy替換了object發送給Client,它真正要訪問的是object對象的add方法;于是Binder驅動通知Server進程,調用你的object對象的add方法,然后把結果返回給我,Server進程收到這個消息,照做之后將結果返回給驅動,驅動然后把結果發給Client進程,整個過程就完成了。
由此可以看出來,Binder跨進程通信并不是真的把一個對象傳輸到了另外一個進程,它在進程里有一個真身,在另外一個進程copy出一個影子(這個影子可以有多個);Client進程通過對影子的操作,然后影子利用Binder驅動讓真身完成操作。
對于Binder的訪問,如果是在同一個進程那么直接返回原始的Binder實體,如果在不同的進程,那么就返回一個代理對象。
8.Java層的使用
queryLocalInterface()方法是查找Binder本地對象,如果找到了就說明Client和Server在同一個進程,那么這個 binder 本身就是 Binder 本地對象,可以直接使用。否則說明是 binder 是個遠程對象,也就是 BinderProxy。需要通過代理對象來實現遠程訪問。
IBinder是遠程對象調用的基本接口,是Binder機制的核心部分。但是它不僅用于遠程調用,也用于進程內的調用。這個接口定義與遠程對象交互的協議(不要直接實現這個接口)。IBinder的最主要的一個API就是transact(),與之對應的是另一個方法Binder.onTransact()。當通過transact()發起調用請求,是onTransact()響應請求。IBinder的API的執行都是同步的,比如transact()方法執行后,會一直等待onTransact()方法調用完成后才返回。不管是在同一個進程還是不同的進程內都是這樣。
系統為每個進程維護了一個存放交互線程的線程池。專門處理從另外一個進程發送來的IPC調用。比如,當進程A發起調用到進程B,A中發出調用的線程就阻塞在transact()中了。進程B中的線程池拿一個線程接收這個調用,調用Binder.onTransact(),處理完后用Parcel包裹下數據返回過去。然后進程A中等待的那個線程在收到返回的Parcel數據后繼續執行。表面上看起來就像在當前進程里的一個線程一樣,但其實不是當前進程創建的。
而且,Binder機制支持進程間的遞歸調用。例如,進程A執行IBinder的transact()方法調用進程B的binder,而進程B在Binder.onTransact()中又用transact()向進程A發起調用,這個時候進程A在等待它之前自己發出的調用返回的同時,還會用自己的Binder.onTransact()來響應進程B的transact()。
備注:flag==>0 ,當為0的時候表示是正常的一次IPC調用模式,否則就是單向的,不返回數據。
從組建的視角總結一下:
Server:一個Binder服務端實際上就是提供一個Binder類的對象,對象創建后,通過線程池的來等待接收Binder驅動發送來的消息,收到消息后執行到Binder對象中的onTransact()函數,按照不同的參數執行不同的代碼。
Binder驅動:任意一個服務端的Binder對象被創建時,其實也會在Binder驅動中創建一個mRemote對象,也是一個Binder類。客戶端訪問服務端其實都是通過mRemote(其實省略掉了Service Manager,transact()調用底層native層,native層到驅動,驅動并不會直接轉發響應請求,而是驅動程序通過喚醒Service Manager來響應)。
Client:客戶端要訪問服務端,必須獲取服務端在Binder對象中對應的mRemote引用。拿到mRemote后,就可以調用其transact()方法,其實在Binder驅動中,mRemote對象也重載了transact()方法。但是給人的感覺是似乎是直接調用遠程服務端對應的Binder,實際上驅動做了一層中轉。