理解 Android Binder 機制(一):驅動篇
Binder的實現是比較複雜的,想要完全弄明白是怎麼一回事,並不是一件容易的事情。
這裡面牽涉到好幾個層次,每一層都有一些模組和機制需要理解。這部分內容預計會分為三篇文章來講解。本文是第一篇,首先會對整個Binder機制做一個架構性的講解,然後會將大部分精力用來講解Binder機制中最核心的部分:Binder驅動的實現。
Binder機制簡介
Binder源自Be Inc公司開發的OpenBinder框架,後來該框架轉移的Palm Inc,由Dianne Hackborn主導開發。OpenBinder的核心部分已經合入Linux Kernel 3.19。
Android Binder是在OpneBinder上的定製實現。原先的OpenBinder框架現在已經不再繼續開發,可以說Android上的Binder讓原先的OpneBinder得到了重生。
Binder是Android系統中大量使用的IPC(Inter-process communication,程式間通訊)機制。無論是應用程式對系統服務的請求,還是應用程式自身提供對外服務,都需要使用到Binder。
因此,Binder機制在Android系統中的地位非常重要,可以說,理解Binder是理解Android系統的絕對必要前提。
在Unix/Linux環境下,傳統的IPC機制包括:
- 管道
- 訊息佇列
- 共享記憶體
- 訊號量
- Socket
等。
由於篇幅所限,本文不會對這些IPC機制做講解,有興趣的讀者可以參閱《UNIX網路程式設計 卷2:程式間通訊》。
Android系統中對於傳統的IPC使用較少(但也有使用,例如:在請求Zygote fork程式的時候使用的是Socket IPC),大部分場景下使用的IPC都是Binder。
Binder相較於傳統IPC來說更適合於Android系統,具體原因的包括如下三點:
- Binder本身是C/S架構的,這一點更符合Android系統的架構
- 效能上更有優勢:管道,訊息佇列,Socket的通訊都需要兩次資料拷貝,而Binder只需要一次。要知道,對於系統底層的IPC形式,少一次資料拷貝,對整體效能的影響是非常之大的
- 安全性更好:傳統IPC形式,無法得到對方的身份標識(UID/GID),而在使用Binder IPC時,這些身份標示是跟隨呼叫過程而自動傳遞的。Server端很容易就可以知道Client端的身份,非常便於做安全檢查
整體架構
Binder整體架構如下所示:
從圖中可以看出,Binder的實現分為這麼幾層:
- Framework層
- Java部分
- JNI部分
- C++部分
- 驅動層
驅動層位於Linux核心中,它提供了最底層的資料傳遞,物件標識,執行緒管理,呼叫過程控制等功能。驅動層是整個Binder機制的核心。
Framework層以驅動層為基礎,提供了應用開發的基礎設施。
Framework層既包含了C++部分的實現,也包含了Java部分的實現。為了能將C++的實現複用到Java端,中間通過JNI進行銜接。
開發者可以在Framework之上利用Binder提供的機制來進行具體的業務邏輯開發。其實不僅僅是第三方開發者,Android系統中本身也包含了很多系統服務都是基於Binder框架開發的。
既然是“程式間”通訊就至少牽涉到兩個程式,Binder框架是典型的C/S架構。在下文中,我們把服務的請求方稱之為Client,服務的實現方稱之為Server。
Client對於Server的請求會經由Binder框架由上至下傳遞到核心的Binder驅動中,請求中包含了Client將要呼叫的命令和引數。請求到了Binder驅動之後,在確定了服務的提供方之後,會再從下至上將請求傳遞給具體的服務。整個呼叫過程如下圖所示:
對網路協議有所瞭解的讀者會發現,這個資料的傳遞過程和網路協議是如此的相似。
初識ServiceManager
前面已經提到,使用Binder框架的既包括系統服務,也包括第三方應用。因此,在同一時刻,系統中會有大量的Server同時存在。那麼,Client在請求Server的時候,是如果確定請求傳送給哪一個Server的呢?
這個問題,就和我們現實生活中如何找到一個公司/商場,如何確定一個人/一輛車一樣,解決的方法就是:每個目標物件都需要一個唯一的標識。並且,需要有一個組織來管理這個唯一的標識。
而Binder框架中負責管理這個標識的就是ServiceManager。ServiceManager對於Binder Server的管理就好比車管所對於車牌號碼的的管理,派出所對於身份證號碼的管理:每個公開對外提供服務的Server都需要註冊到ServiceManager中(通過addService),註冊的時候需要指定一個唯一的id(這個id其實就是一個字串)。
Client要對Server發出請求,就必須知道服務端的id。Client需要先根據Server的id通過ServerManager拿到Server的標示(通過getService),然後通過這個標示與Server進行通訊。
整個過程如下圖所示:
如果上面這些介紹已經讓你一頭霧水,請不要過分擔心,下面會詳細講解這其中的細節。
下文會以自下而上的方式來講解Binder框架。自下而上未必是最好的方法,每個人的思考方式不一樣,如果你更喜歡自上而下的理解,你也按這樣的順序來閱讀。
對於大部分人來說,我們可能需要反覆的查閱才能完全理解。
驅動層
原始碼路徑(這部分程式碼不在AOSP中,而是位於Linux核心程式碼中):
/kernel/drivers/android/binder.c /kernel/include/uapi/linux/android/binder.h
或者
/kernel/drivers/staging/android/binder.c /kernel/drivers/staging/android/uapi/binder.h
Binder機制的實現中,最核心的就是Binder驅動。 Binder是一個miscellaneous型別的驅動,本身不對應任何硬體,所有的操作都在軟體層。 binder_init
函式負責Binder驅動的初始化工作,該函式中大部分程式碼是在通過debugfs_create_dir
和debugfs_create_file
函式建立debugfs對應的檔案。 如果核心在編譯時開啟了debugfs,則通過adb shell
連上裝置之後,可以在裝置的這個路徑找到debugfs對應的檔案:/sys/kernel/debug
。Binder驅動中建立的debug檔案如下所示:
# ls -l /sys/kernel/debug/binder/ total 0 -r--r--r-- 1 root root 0 1970-01-01 00:00 failed_transaction_log drwxr-xr-x 2 root root 0 1970-05-09 01:19 proc -r--r--r-- 1 root root 0 1970-01-01 00:00 state -r--r--r-- 1 root root 0 1970-01-01 00:00 stats -r--r--r-- 1 root root 0 1970-01-01 00:00 transaction_log -r--r--r-- 1 root root 0 1970-01-01 00:00 transactions
這些檔案其實都在記憶體中的,實時的反應了當前Binder的使用情況,在實際的開發過程中,這些資訊可以幫忙分析問題。例如,可以通過檢視/sys/kernel/debug/binder/proc
目錄來確定哪些程式正在使用Binder,通過檢視transaction_log
和transactions
檔案來確定Binder通訊的資料。
binder_init
函式中最主要的工作其實下面這行:
ret = misc_register(&binder_miscdev);
該行程式碼真正向核心中註冊了Binder裝置。binder_miscdev
的定義如下:
static struct miscdevice binder_miscdev = { .minor = MISC_DYNAMIC_MINOR, .name = "binder", .fops = &binder_fops };
這裡指定了Binder裝置的名稱是“binder”。這樣,在使用者空間便可以通過對/dev/binder檔案進行操作來使用Binder。
binder_miscdev同時也指定了該裝置的fops。fops是另外一個結構體,這個結構中包含了一系列的函式指標,其定義如下:
static const struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = binder_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, };
這裡除了owner
之外,每一個欄位都是一個函式指標,這些函式指標對應了使用者空間在使用Binder裝置時的操作。例如:binder_poll
對應了poll
系統呼叫的處理,binder_mmap
對應了mmap
系統呼叫的處理,其他類同。
這其中,有三個函式尤為重要,它們是:binder_open
,binder_mmap
和binder_ioctl
。 這是因為,需要使用Binder的程式,幾乎總是先通過binder_open
開啟Binder裝置,然後通過binder_mmap
進行記憶體對映。
在這之後,通過binder_ioctl
來進行實際的操作。Client對於Server端的請求,以及Server對於Client請求結果的返回,都是通過ioctl完成的。
這裡提到的流程如下圖所示:
主要結構
Binder驅動中包含了很多的結構體。為了便於下文講解,這裡我們先對這些結構體做一些介紹。
驅動中的結構體可以分為兩類:
一類是與使用者空間共用的,這些結構體在Binder通訊協議過程中會用到。因此,這些結構體定義在binder.h
中,包括:
結構體名稱 | 說明 |
---|---|
flat_binder_object | 描述在Binder IPC中傳遞的物件,見下文 |
binder_write_read | 儲存一次讀寫操作的資料 |
binder_version | 儲存Binder的版本號 |
transaction_flags | 描述事務的flag,例如是否是非同步請求,是否支援fd |
binder_transaction_data | 儲存一次事務的資料 |
binder_ptr_cookie | 包含了一個指標和一個cookie |
binder_handle_cookie | 包含了一個控制程式碼和一個cookie |
binder_pri_desc | 暫未用到 |
binder_pri_ptr_cookie | 暫未用到 |
這其中,binder_write_read
和binder_transaction_data
這兩個結構體最為重要,它們儲存了IPC呼叫過程中的資料。關於這一點,我們在下文中會講解。
Binder驅動中,還有一類結構體是僅僅Binder驅動內部實現過程中需要的,它們定義在binder.c
中,包括:
結構體名稱 | 說明 |
---|---|
binder_node | 描述Binder實體節點,即:對應了一個Server |
binder_ref | 描述對於Binder實體的引用 |
binder_buffer | 描述Binder通訊過程中儲存資料的Buffer |
binder_proc | 描述使用Binder的程式 |
binder_thread | 描述使用Binder的執行緒 |
binder_work | 描述通訊過程中的一項任務 |
binder_transaction | 描述一次事務的相關資訊 |
binder_deferred_state | 描述延遲任務 |
binder_ref_death | 描述Binder實體死亡的資訊 |
binder_transaction_log | debugfs日誌 |
binder_transaction_log_entry | debugfs日誌條目 |
這裡需要讀者關注的結構體已經用加粗做了標註。
Binder協議
Binder協議可以分為控制協議和驅動協議兩類。
控制協議是程式通過ioctl(“/dev/binder”) 與Binder裝置進行通訊的協議,該協議包含以下幾種命令:
命令 | 說明 | 引數型別 |
---|---|---|
BINDER_WRITE_READ | 讀寫操作,最常用的命令。IPC過程就是通過這個命令進行資料傳遞 | binder_write_read |
BINDER_SET_MAX_THREADS | 設定程式支援的最大執行緒數量 | size_t |
BINDER_SET_CONTEXT_MGR | 設定自身為ServiceManager | 無 |
BINDER_THREAD_EXIT | 通知驅動Binder執行緒退出 | 無 |
BINDER_VERSION | 獲取Binder驅動的版本號 | binder_version |
BINDER_SET_IDLE_PRIORITY | 暫未用到 | - |
BINDER_SET_IDLE_TIMEOUT | 暫未用到 | - |
Binder的驅動協議描述了對於Binder驅動的具體使用過程。驅動協議又可以分為兩類:
- 一類是
binder_driver_command_protocol
,描述了程式傳送給Binder驅動的命令 - 一類是
binder_driver_return_protocol
,描述了Binder驅動傳送給程式的命令
binder_driver_command_protocol
共包含17個命令,分別是:
命令 | 說明 | 引數型別 |
---|---|---|
BC_TRANSACTION | Binder事務,即:Client對於Server的請求 | binder_transaction_data |
BC_REPLY | 事務的應答,即:Server對於Client的回覆 | binder_transaction_data |
BC_FREE_BUFFER | 通知驅動釋放Buffer | binder_uintptr_t |
BC_ACQUIRE | 強引用計數+1 | __u32 |
BC_RELEASE | 強引用計數-1 | __u32 |
BC_INCREFS | 弱引用計數+1 | __u32 |
BC_DECREFS | 弱引用計數-1 | __u32 |
BC_ACQUIRE_DONE | BR_ACQUIRE的回覆 | binder_ptr_cookie |
BC_INCREFS_DONE | BR_INCREFS的回覆 | binder_ptr_cookie |
BC_ENTER_LOOPER | 通知驅動主執行緒ready | void |
BC_REGISTER_LOOPER | 通知驅動子執行緒ready | void |
BC_EXIT_LOOPER | 通知驅動執行緒已經退出 | void |
BC_REQUEST_DEATH_NOTIFICATION | 請求接收死亡通知 | binder_handle_cookie |
BC_CLEAR_DEATH_NOTIFICATION | 去除接收死亡通知 | binder_handle_cookie |
BC_DEAD_BINDER_DONE | 已經處理完死亡通知 | binder_uintptr_t |
BC_ATTEMPT_ACQUIRE | 暫未實現 | - |
BC_ACQUIRE_RESULT | 暫未實現 | - |
binder_driver_return_protocol
共包含18個命令,分別是:
返回型別 | 說明 | 引數型別 |
---|---|---|
BR_OK | 操作完成 | void |
BR_NOOP | 操作完成 | void |
BR_ERROR | 發生錯誤 | __s32 |
BR_TRANSACTION | 通知程式收到一次Binder請求(Server端) | binder_transaction_data |
BR_REPLY | 通知程式收到Binder請求的回覆(Client) | binder_transaction_data |
BR_TRANSACTION_COMPLETE | 驅動對於接受請求的確認回覆 | void |
BR_FAILED_REPLY | 告知傳送方通訊目標不存在 | void |
BR_SPAWN_LOOPER | 通知Binder程式建立一個新的執行緒 | void |
BR_ACQUIRE | 強引用計數+1請求 | binder_ptr_cookie |
BR_RELEASE | 強引用計數-1請求 | binder_ptr_cookie |
BR_INCREFS | 弱引用計數+1請求 | binder_ptr_cookie |
BR_DECREFS | 若引用計數-1請求 | binder_ptr_cookie |
BR_DEAD_BINDER | 傳送死亡通知 | binder_uintptr_t |
BR_CLEAR_DEATH_NOTIFICATION_DONE | 清理死亡通知完成 | binder_uintptr_t |
BR_DEAD_REPLY | 告知傳送方對方已經死亡 | void |
BR_ACQUIRE_RESULT | 暫未實現 | - |
BR_ATTEMPT_ACQUIRE | 暫未實現 | - |
BR_FINISHED | 暫未實現 | - |
單獨看上面的協議可能很難理解,這裡我們以一次Binder請求過程來詳細看一下Binder協議是如何通訊的,就比較好理解了。
這幅圖的說明如下:
- Binder是C/S架構的,通訊過程牽涉到:Client,Server以及Binder驅動三個角色
- Client對於Server的請求以及Server對於Client回覆都需要通過Binder驅動來中轉資料
- BC_XXX命令是程式傳送給驅動的命令
- BR_XXX命令是驅動傳送給程式的命令
- 整個通訊過程由Binder驅動控制
這裡再補充說明一下,通過上面的Binder協議的說明中我們看到,Binder協議的通訊過程中,不僅僅是傳送請求和接受資料這些命令。同時包括了對於引用計數的管理和對於死亡通知的管理(告知一方,通訊的另外一方已經死亡)等功能。
這些功能的通訊過程和上面這幅圖是類似的:一方傳送BC_XXX,然後由驅動控制通訊過程,接著傳送對應的BR_XXX命令給通訊的另外一方。因為這種相似性,對於這些內容就不再贅述了。
在有了上面這些背景知識介紹之後,我們就可以進入到Binder驅動的內部實現中來一探究竟了。
PS:上面介紹的這些結構體和協議,因為內容較多,初次看完記不住是很正常的,在下文詳細講解的時候,回過頭來對照這些表格來理解是比較有幫助的。
開啟Binder裝置
任何程式在使用Binder之前,都需要先通過open("/dev/binder")
開啟Binder裝置。上文已經提到,使用者空間的open
系統呼叫對應了驅動中的binder_open
函式。在這個函式,Binder驅動會為呼叫的程式做一些初始化工作。binder_open
函式程式碼如下所示:
static int binder_open(struct inode *nodp, struct file *filp) { struct binder_proc *proc; // 建立程式對應的binder_proc物件 proc = kzalloc(sizeof(*proc), GFP_KERNEL); if (proc == NULL) return -ENOMEM; get_task_struct(current); proc->tsk = current; // 初始化binder_proc INIT_LIST_HEAD(&proc->todo); init_waitqueue_head(&proc->wait); proc->default_priority = task_nice(current); // 鎖保護 binder_lock(__func__); binder_stats_created(BINDER_STAT_PROC); // 新增到全域性列表binder_procs中 hlist_add_head(&proc->proc_node, &binder_procs); proc->pid = current->group_leader->pid; INIT_LIST_HEAD(&proc->delivered_death); filp->private_data = proc; binder_unlock(__func__); return 0; }
在Binder驅動中,通過binder_procs
記錄了所有使用Binder的程式。每個初次開啟Binder裝置的程式都會被新增到這個列表中的。
另外,請讀者回顧一下上文介紹的Binder驅動中的幾個關鍵結構體:
- binder_proc
- binder_node
- binder_thread
- binder_ref
- binder_buffer
在實現過程中,為了便於查詢,這些結構體互相之間都留有欄位儲存關聯的結構。
下面這幅圖描述了這裡說到的這些內容:
記憶體對映(mmap)
在開啟Binder裝置之後,程式還會通過mmap進行記憶體對映。mmap的作用有如下兩個:
- 申請一塊記憶體空間,用來接收Binder通訊過程中的資料
- 對這塊記憶體進行地址對映,以便將來訪問
binder_mmap
函式對應了mmap系統呼叫的處理,這個函式也是Binder驅動的精華所在(這裡說的binder_mmap函式也包括其內部呼叫的binder_update_page_range函式,見下文)。
前文我們說到,使用Binder機制,資料只需要經歷一次拷貝就可以了,其原理就在這個函式中。
binder_mmap
這個函式中,會申請一塊實體記憶體,然後在使用者空間和核心空間同時對應到這塊記憶體上。在這之後,當有Client要傳送資料給Server的時候,只需一次,將Client傳送過來的資料拷貝到Server端的核心空間指定的記憶體地址即可,由於這個記憶體地址在服務端已經同時對映到使用者空間,因此無需再做一次複製,Server即可直接訪問,整個過程如下圖所示:
這幅圖的說明如下:
- Server在啟動之後,呼叫對/dev/binder裝置呼叫mmap
- 核心中的binder_mmap函式進行對應的處理:申請一塊實體記憶體,然後在使用者空間和核心空間同時進行對映
- Client通過BINDER_WRITE_READ命令傳送請求,這個請求將先到驅動中,同時需要將資料從Client程式的使用者空間拷貝到核心空間
- 驅動通過BR_TRANSACTION通知Server有人發出請求,Server進行處理。由於這塊記憶體也在使用者空間進行了對映,因此Server程式的程式碼可以直接訪問
瞭解原理之後,我們再來看一下Binder驅動的相關原始碼。這段程式碼有兩個函式:
binder_mmap
函式對應了mmap的系統呼叫的處理binder_update_page_range
函式真正實現了記憶體分配和地址對映
static int binder_mmap(struct file *filp, struct vm_area_struct *vma) { int ret; struct vm_struct *area; struct binder_proc *proc = filp->private_data; const char *failure_string; struct binder_buffer *buffer; ... // 在核心空間獲取一塊地址範圍 area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP); if (area == NULL) { ret = -ENOMEM; failure_string = "get_vm_area"; goto err_get_vm_area_failed; } proc->buffer = area->addr; // 記錄核心空間與使用者空間的地址偏移 proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer; mutex_unlock(&binder_mmap_lock); ... proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL); if (proc->pages == NULL) { ret = -ENOMEM; failure_string = "alloc page array"; goto err_alloc_pages_failed; } proc->buffer_size = vma->vm_end - vma->vm_start; vma->vm_ops = &binder_vm_ops; vma->vm_private_data = proc; /* binder_update_page_range assumes preemption is disabled */ preempt_disable(); // 通過下面這個函式真正完成記憶體的申請和地址的對映 // 初次使用,先申請一個PAGE_SIZE大小的記憶體 ret = binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma); ... } static int binder_update_page_range(struct binder_proc *proc, int allocate, void *start, void *end, struct vm_area_struct *vma) { void *page_addr; unsigned long user_page_addr; struct vm_struct tmp_area; struct page **page; struct mm_struct *mm; ... for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) { int ret; struct page **page_array_ptr; page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE]; BUG_ON(*page); // 真正進行記憶體的分配 *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); if (*page == NULL) { pr_err("%d: binder_alloc_buf failed for page at %p\n", proc->pid, page_addr); goto err_alloc_page_failed; } tmp_area.addr = page_addr; tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */; page_array_ptr = page; // 在核心空間進行記憶體對映 ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr); if (ret) { pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n", proc->pid, page_addr); goto err_map_kernel_failed; } user_page_addr = (uintptr_t)page_addr + proc->user_buffer_offset; // 在使用者空間進行記憶體對映 ret = vm_insert_page(vma, user_page_addr, page[0]); if (ret) { pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n", proc->pid, user_page_addr); goto err_vm_insert_page_failed; } /* vm_insert_page does not seem to increment the refcount */ } if (mm) { up_write(&mm->mmap_sem); mmput(mm); } preempt_disable(); return 0; ...
在開發過程中,我們可以通過procfs看到程式對映的這塊記憶體空間:
- 將Android裝置連線到電腦上之後,通過
adb shell
進入到終端 - 然後選擇一個使用了Binder的程式,例如system_server(這是系統中一個非常重要的程式,下一章我們會專門講解),通過
ps | grep system_server
來確定程式號,例如是1889 - 通過
cat /proc/[pid]/maps | grep "/dev/binder"
過濾出這塊記憶體的地址
在我的Nexus 6P上,控制檯輸出如下:
angler:/ # ps | grep system_server system 1889 526 2353404 140016 SyS_epoll_ 72972eeaf4 S system_server angler:/ # cat /proc/1889/maps | grep "/dev/binder" 7294761000-729485f000 r--p 00000000 00:0c 12593 /dev/binder
PS:grep是通過萬用字元進行匹配過濾的命令,“|”是Unix上的管道命令。即將前一個命令的輸出給下一個命令作為輸入。如果這裡我們不加“ | grep xxx”,那麼將看到前一個命令的完整輸出。
記憶體的管理
上文中,我們看到binder_mmap的時候,會申請一個PAGE_SIZE(通常是4K)的記憶體。而實際使用過程中,一個PAGE_SIZE的大小通常是不夠的。
在驅動中,會根據實際的使用情況進行記憶體的分配。有記憶體的分配,當然也需要記憶體的釋放。這裡我們就來看看Binder驅動中是如何進行記憶體的管理的。
首先,我們還是從一次IPC請求說起。
當一個Client想要對Server發出請求時,它首先將請求傳送到Binder裝置上,由Binder驅動根據請求的資訊找到對應的目標節點,然後將請求資料傳遞過去。
程式通過ioctl系統呼叫來發出請求:ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)
PS:這行程式碼來自於Framework層的IPCThreadState
類。在後文中,我們將看到,IPCThreadState
類專門負責與驅動進行通訊。
這裡的mProcess->mDriverFD
對應了開啟Binder裝置時的fd。BINDER_WRITE_READ
對應了具體要做的操作碼,這個操作碼將由Binder驅動解析。bwr
儲存了請求資料,其型別是binder_write_read
。
binder_write_read
其實是一個相對外層的資料結構,其內部會包含一個binder_transaction_data
結構的資料。binder_transaction_data
包含了發出請求者的標識,請求的目標物件以及請求所需要的引數。它們的關係如下圖所示:
binder_ioctl函式對應了ioctl系統呼叫的處理。這個函式的邏輯比較簡單,就是根據ioctl的命令來確定進一步處理的邏輯,具體如下:
- 如果命令是BINDER_WRITE_READ,並且
- 如果 bwr.write_size > 0,則呼叫binder_thread_write
- 如果 bwr.read_size > 0,則呼叫binder_thread_read
- 如果命令是BINDER_SET_MAX_THREADS,則設定程式的max_threads,即程式支援的最大執行緒數
- 如果命令是BINDER_SET_CONTEXT_MGR,則設定當前程式為ServiceManager,見下文
- 如果命令是BINDER_THREAD_EXIT,則呼叫binder_free_thread,釋放binder_thread
- 如果命令是BINDER_VERSION,則返回當前的Binder版本號
這其中,最關鍵的就是binder_thread_write方法。當Client請求Server的時候,便會傳送一個BINDER_WRITE_READ命令,同時框架會將將實際的資料包裝好。此時,binder_transaction_data中的code將是BC_TRANSACTION,由此便會呼叫到binder_transaction方法,這個方法是對一次Binder事務的處理,這其中會呼叫binder_alloc_buf函式為此次事務申請一個快取。這裡提到到呼叫關係如下:
binder_update_page_range這個函式在上文中,我們已經看到過了。其作用就是:進行記憶體分配並且完成記憶體的對映。而binder_alloc_buf函式,正如其名稱那樣的:完成快取的分配。
在驅動中,通過binder_buffer結構體描述快取。一次Binder事務就會對應一個binder_buffer,其結構如下所示:
struct binder_buffer { struct list_head entry; struct rb_node rb_node; unsigned free:1; unsigned allow_user_free:1; unsigned async_transaction:1; unsigned debug_id:29; struct binder_transaction *transaction; struct binder_node *target_node; size_t data_size; size_t offsets_size; uint8_t data[0]; };
而在binder_proc(描述了使用Binder的程式)中,包含了幾個欄位用來管理程式在Binder IPC過程中快取,如下:
struct binder_proc { ... struct list_head buffers; // 程式擁有的buffer列表 struct rb_root free_buffers; // 空閒buffer列表 struct rb_root allocated_buffers; // 已使用的buffer列表 size_t free_async_space; // 剩餘的非同步呼叫的空間 size_t buffer_size; // 快取的上限 ... };
程式在mmap時,會設定支援的總快取大小的上限(下文會講到)。而程式每當收到BC_TRANSACTION,就會判斷已使用快取加本次申請的和有沒有超過上限。如果沒有,就考慮進行記憶體的分配。
程式的空閒快取記錄在binder_proc的free_buffers中,這是一個以紅黑樹形式儲存的結構。每次嘗試分配快取的時候,會從這裡面按大小順序進行查詢,找到最接近需要的一塊快取。查詢的邏輯如下:
while (n) { buffer = rb_entry(n, struct binder_buffer, rb_node); BUG_ON(!buffer->free); buffer_size = binder_buffer_size(proc, buffer); if (size < buffer_size) { best_fit = n; n = n->rb_left; } else if (size > buffer_size) n = n->rb_right; else { best_fit = n; break; } }
找到之後,還需要對binder_proc中的欄位進行相應的更新:
rb_erase(best_fit, &proc->free_buffers); buffer->free = 0; binder_insert_allocated_buffer(proc, buffer); if (buffer_size != size) { struct binder_buffer *new_buffer = (void *)buffer->data + size; list_add(&new_buffer->entry, &buffer->entry); new_buffer->free = 1; binder_insert_free_buffer(proc, new_buffer); } binder_debug(BINDER_DEBUG_BUFFER_ALLOC, "%d: binder_alloc_buf size %zd got %p\n", proc->pid, size, buffer); buffer->data_size = data_size; buffer->offsets_size = offsets_size; buffer->async_transaction = is_async; if (is_async) { proc->free_async_space -= size + sizeof(struct binder_buffer); binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC, "%d: binder_alloc_buf size %zd async free %zd\n", proc->pid, size, proc->free_async_space); }
下面我們再來看看記憶體的釋放。
BC_FREE_BUFFER
命令是通知驅動進行記憶體的釋放,binder_free_buf
函式是真正實現的邏輯,這個函式與binder_alloc_buf是剛好對應的。在這個函式中,所做的事情包括:
- 重新計算程式的空閒快取大小
- 通過binder_update_page_range釋放記憶體
- 更新binder_proc的buffers,free_buffers,allocated_buffers欄位
Binder中的“物件導向”
Binder機制淡化了程式的邊界,使得跨越程式也能夠呼叫到指定服務的方法,其原因是因為Binder機制在底層處理了在程式間的“物件”傳遞。
在Binder驅動中,並不是真的將物件在程式間來回序列化,而是通過特定的標識來進行物件的傳遞。Binder驅動中,通過flat_binder_object
來描述需要跨越程式傳遞的物件。其定義如下:
struct flat_binder_object { __u32 type; __u32 flags; union { binder_uintptr_t binder; /* local object */ __u32 handle; /* remote object */ }; binder_uintptr_t cookie; };
這其中,type有如下5種型別。
enum { BINDER_TYPE_BINDER = B_PACK_CHARS('s', 'b', '*', B_TYPE_LARGE), BINDER_TYPE_WEAK_BINDER = B_PACK_CHARS('w', 'b', '*', B_TYPE_LARGE), BINDER_TYPE_HANDLE = B_PACK_CHARS('s', 'h', '*', B_TYPE_LARGE), BINDER_TYPE_WEAK_HANDLE = B_PACK_CHARS('w', 'h', '*', B_TYPE_LARGE), BINDER_TYPE_FD = B_PACK_CHARS('f', 'd', '*', B_TYPE_LARGE), };
當物件傳遞到Binder驅動中的時候,由驅動來進行翻譯和解釋,然後傳遞到接收的程式。
例如當Server把Binder實體傳遞給Client時,在傳送資料流中,flat_binder_object中的type是BINDER_TYPE_BINDER,同時binder欄位指向Server程式使用者空間地址。但這個地址對於Client程式是沒有意義的(Linux中,每個程式的地址空間是互相隔離的),驅動必須對資料流中的flat_binder_object做相應的翻譯:將type該成BINDER_TYPE_HANDLE;為這個Binder在接收程式中建立位於核心中的引用並將引用號填入handle中。對於發生資料流中引用型別的Binder也要做同樣轉換。經過處理後接收程式從資料流中取得的Binder引用才是有效的,才可以將其填入資料包binder_transaction_data的target.handle域,向Binder實體傳送請求。
由於每個請求和請求的返回都會經歷核心的翻譯,因此這個過程從程式的角度來看是完全透明的。程式完全不用感知這個過程,就好像物件真的在程式間來回傳遞一樣。
驅動層的執行緒管理
上文多次提到,Binder本身是C/S架構。由Server提供服務,被Client使用。既然是C/S架構,就可能存在多個Client會同時訪問Server的情況。 在這種情況下,如果Server只有一個執行緒處理響應,就會導致客戶端的請求可能需要排隊而導致響應過慢的現象發生。解決這個問題的方法就是引入多執行緒。
Binder機制的設計從最底層–驅動層,就考慮到了對於多執行緒的支援。具體內容如下:
- 使用Binder的程式在啟動之後,通過BINDER_SET_MAX_THREADS告知驅動其支援的最大執行緒數量
- 驅動會對執行緒進行管理。在binder_proc結構中,這些欄位記錄了程式中執行緒的資訊:max_threads,requested_threads,requested_threads_started,ready_threads
- binder_thread結構對應了Binder程式中的執行緒
- 驅動通過BR_SPAWN_LOOPER命令告知程式需要建立一個新的執行緒
- 程式通過BC_ENTER_LOOPER命令告知驅動其主執行緒已經ready
- 程式通過BC_REGISTER_LOOPER命令告知驅動其子執行緒(非主執行緒)已經ready
- 程式通過BC_EXIT_LOOPER命令告知驅動其執行緒將要退出
- 線上程退出之後,通過BINDER_THREAD_EXIT告知Binder驅動。驅動將對應的binder_thread物件銷燬
再聊ServiceManager
上文已經說過,每一個Binder Server在驅動中會有一個binder_node進行對應。同時,Binder驅動會負責在程式間傳遞服務物件,並負責底層的轉換。另外,我們也提到,每一個Binder服務都需要有一個唯一的名稱。由ServiceManager來管理這些服務的註冊和查詢。
而實際上,為了便於使用,ServiceManager本身也實現為一個Server物件。任何程式在使用ServiceManager的時候,都需要先拿到指向它的標識。然後通過這個標識來使用ServiceManager。
這似乎形成了一個互相矛盾的現象:
- 通過ServiceManager我們才能拿到Server的標識
- ServiceManager本身也是一個Server
解決這個矛盾的辦法其實也很簡單:Binder機制為ServiceManager預留了一個特殊的位置。這個位置是預先定好的,任何想要使用ServiceManager的程式只要通過這個特定的位置就可以訪問到ServiceManager了(而不用再通過ServiceManager的介面)。
在Binder驅動中,有一個全域性的變數:
static struct binder_node *binder_context_mgr_node;
這個變數指向的就是ServiceManager。
當有程式通過ioctl並指定命令為BINDER_SET_CONTEXT_MGR的時候,驅動被認定這個程式是ServiceManager,binder_ioctl函式中對應的處理如下:
case BINDER_SET_CONTEXT_MGR: if (binder_context_mgr_node != NULL) { pr_err("BINDER_SET_CONTEXT_MGR already set\n"); ret = -EBUSY; goto err; } ret = security_binder_set_context_mgr(proc->tsk); if (ret < 0) goto err; if (uid_valid(binder_context_mgr_uid)) { if (!uid_eq(binder_context_mgr_uid, current->cred->euid)) { pr_err("BINDER_SET_CONTEXT_MGR bad uid %d != %d\n", from_kuid(&init_user_ns, current->cred->euid), from_kuid(&init_user_ns, binder_context_mgr_uid)); ret = -EPERM; goto err; } } else binder_context_mgr_uid = current->cred->euid; binder_context_mgr_node = binder_new_node(proc, 0, 0); if (binder_context_mgr_node == NULL) { ret = -ENOMEM; goto err; } binder_context_mgr_node->local_weak_refs++; binder_context_mgr_node->local_strong_refs++; binder_context_mgr_node->has_strong_ref = 1; binder_context_mgr_node->has_weak_ref = 1; break;
ServiceManager應當要先於所有Binder Server之前啟動。在它啟動完成並告知Binder驅動之後,驅動便設定好了這個特定的節點。
在這之後,當有其他模組想要使用ServerManager的時候,只要將請求指向ServiceManager所在的位置即可。
在Binder驅動中,通過handle = 0這個位置來訪問ServiceManager。例如,binder_transaction
中,判斷如果target.handler為0,則認為這個請求是傳送給ServiceManager的,相關程式碼如下:
if (tr->target.handle) { struct binder_ref *ref; ref = binder_get_ref(proc, tr->target.handle, true); if (ref == NULL) { binder_user_error("%d:%d got transaction to invalid handle\n", proc->pid, thread->pid); return_error = BR_FAILED_REPLY; goto err_invalid_target_handle; } target_node = ref->node; } else { target_node = binder_context_mgr_node; if (target_node == NULL) { return_error = BR_DEAD_REPLY; goto err_no_context_mgr_node; } }
結束語
本篇文章中,我們對Binder機制做了整體架構和分層的介紹,也詳細講解了Binder機制中的驅動模組。對於驅動之上的模組,會在今後的文章中講解。
下一篇文章中,我們會詳細講解Android Binder機制的Framework層,敬請期待。
相關文章
- 理解 Android Binder 機制(三):Java層AndroidJava
- 理解 Android Binder 機制(二):C++層AndroidC++
- 藉助 AIDL 理解 Android Binder 機制——Binder 來龍去脈AIAndroid
- Android Binder機制淺析Android
- Android的IPC機制BinderAndroid
- Android外掛化原理解析——Hook機制之Binder HookAndroidHook
- Android進階(六)Binder機制Android
- android Binder機制深入淺出Android
- Android Binder機制文章轉載Android
- Binder機制
- Android 外掛化原理解析(3):Hook 機制之 Binder HookAndroidHook
- 圖解Android中的binder機制圖解Android
- [譯]理解 Node.js 事件驅動機制Node.js事件
- Android系統之Binder通訊機制Android
- Binder學習(二)Binder機制解析
- 藉助 AIDL 理解 Android Binder 機制——AIDL 的使用和原理分析AIAndroid
- Binder通訊機制
- Android IPC機制(三):淺談Binder的使用Android
- Android的IPC機制(三)——Binder連線池Android
- 【Android原始碼】Binder機制和AIDL分析Android原始碼AI
- Binder 驅動詳解(下)
- Binder 驅動詳解(上)
- Binder機制分析(1)——Binder結構簡介
- Android Handler機制理解Android
- 理解Android安全機制Android
- 3分鐘帶你看懂android的Binder機制Android
- Android 系統原始碼-2:Binder 通訊機制Android原始碼
- Binder機制之AIDLAI
- 理解 Android 訊息機制Android
- Android-Binder(一)Android
- 全面理解 Android 安全機制(Android Permission)Android
- Android 深入理解 Notification 機制Android
- android-IPC/Binder/D-BUS(Binder/Messager/AIDL)程式間通訊(訊息機制)AndroidAI
- Binder機制的細節補充
- 從AIDL開始談Android程式間Binder通訊機制AIAndroid
- 從Activity的啟動流程理解Binder
- [Android進階]Binder學習(初始篇)Android
- 一個Demo帶你理解Android介面回撥機制Android