IPC 之 Binder 初识
概述
最近在看Android 的 IPC 機制,想要系統的研究一下,然后就走到了 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 通信類比到 互聯網 ,Server是服務器,Client是客戶終端,Service_Manager是域名服務器(DNS),Binder 驅動是路由器。Client 通過某種 ”地址“ 去訪問 Server ,而訪問的過程中,需要Service_Manager 這個 DNS 去解析 ”地址“ ,然后通過解析的結果在Service_Manager的一個服務表( svcInfo)中去查找 Server ,找到后執行相應的操作。此時 Client 處于等待狀態,然后等到 Server 處理完請求之后,由 Binder 喚醒 Client 然后將結果返回。 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 Definition; 簡單理解如下:
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 線程來處理的。
③ binder_proc 結構內:
struct binder_proc { //binder_proc下會掛四個紅黑樹 struct hlist_node proc_node; struct rb_root threads; // threads樹用來保存binder_proc進程內用于處理用戶請求的線程,它的最大數量由max_threads來決定struct rb_root nodes; // node樹成用來保存binder_proc進程內的Binder實體struct rb_root refs_by_desc; //refs_by_desc樹和refs_by_node樹用來保存binder_proc進程內的Binder引用,即引用的其它進程的Binder實體,它分別用兩種方式來組織紅黑樹,
// 一種是以句柄作來key值來組織,一種是以引用的實體節點的地址值作來key值來組織,它們都是表示同一樣東西,只不過是為了內部查找方便而用兩個紅黑樹來表示。
struct rb_root refs_by_node; int pid; struct vm_area_struct *vma; struct task_struct *tsk; struct files_struct *files; struct hlist_node deferred_work_node; int deferred_work; void *buffer; ptrdiff_t user_buffer_offset; struct list_head buffers; struct rb_root free_buffers; struct rb_root allocated_buffers; size_t free_async_space; struct page **pages; size_t buffer_size; uint32_t buffer_free; struct list_head todo; wait_queue_head_t wait; struct binder_stats stats; struct list_head delivered_death; int max_threads; int requested_threads; int requested_threads_started; int ready_threads; long default_priority; };
④ binder_thread 結構體:
struct binder_thread { struct binder_proc *proc; // proc表示這個線程所屬的進程struct rb_node rb_node; //用來鏈入 threads 這棵紅黑樹的節點int pid; int looper; struct binder_transaction *transaction_stack; struct list_head todo; uint32_t return_error; /* Write failed, return error code in read buf */ uint32_t return_error2; /* Write failed, return error code in read */ /* buffer. Used when sending a reply to a dead process that */ /* we are also waiting on */ wait_queue_head_t wait; struct binder_stats stats; };?
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進程間通信機制的精髓所在,同一個物理頁面,一方映射到進程虛擬地址空間,一方面映射到內核虛擬地址空間,這樣,進程和內核之間就可以減少一次內存拷貝了,提到了進程間通信效率。舉個例子如,Client要將一塊內存數據傳遞給Server,一般的做法是,Client將這塊數據從它的進程空間拷貝到內核空間中,然后內核再將這個數據從內核空間拷貝到Server的進程空間,這樣,Server就可以訪問這個數據了。但是在這種方法中,執行了兩次內存拷貝操作,而采用我們上面提到的方法,只需要把Client進程空間的數據拷貝一次到內核空間,然后Server與內核共享這個數據就可以了,整個過程只需要執行一次內存拷貝,提高了效率。
總結
網上資料比較好的如下:
1. Bander設計與實現
2. 老羅的 Android進程間通信(IPC)機制Binder簡要介紹和學習計劃 系列
3. Innost的 深入理解Binder 系列
4. Gityuan的 Binder系列 (基于 Android 6.0)
5. Binder學習指南
6. Android-Binder進程間通訊機制
轉載于:https://www.cnblogs.com/aimqqroad-13/p/8952775.html
總結
以上是生活随笔為你收集整理的IPC 之 Binder 初识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数组,集合相关小结
- 下一篇: c++学习4 -- 输入输出