Android Binder機制 -- Binder驅動
Binder驅動分析
由於時間和精力有限,驅動的分析可能會存在不準確或者不全的地方。。。
準備從 Binder 記憶體管理、多執行緒和引用計數三個方面介紹驅動。
進而回答之前留下的問題。
資料結構介紹
我們即將見到Binder
驅動的第一個資料結構!!!!!!!有請,,,,
struct binder_alloc
每個程序對應一個binder_alloc
,通過它,我們就能管理Binder程序的記憶體了。。。
定義
struct binder_alloc { struct mutex mutex; struct vm_area_struct *vma; struct mm_struct *vma_vm_mm; 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 binder_lru_page *pages; size_t buffer_size; uint32_t buffer_free; int pid; size_t pages_high; };
-
vma
表示
mmap
對映的那一段使用者空間的虛擬地址空間。 -
buffer
mmap
的系統呼叫實現會為程序分配一段虛擬地址(vma
),然後會呼叫到binder_mmap
,由binder完成記憶體對映工作。binder的做法就是將建立一段和vma
大小一樣的核心虛擬地址和vma
關聯。buffer
記錄的就是核心虛擬地址的起始地址。//為核心分配虛擬地址空間 area = get_vm_area(vma->vm_end - vma->vm_start, VM_ALLOC); alloc->buffer = area->addr; //儲存起始地址 alloc->buffer_size = vma->vm_end - vma->vm_start; // 我們對映的大小
-
user_buffer_offset
vma
和vma_vm_mm
的固定偏移。// 負數?? alloc->user_buffer_offset = vma->vm_start - (uintptr_t)alloc->buffer;
-
pages
mmap
時,先為對映的虛擬地址段分配好頁面。alloc->pages = kzalloc(sizeof(alloc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE),GFP_KERNEL);
雖然建立了虛擬地址的對映,但是還是沒有看到為我們mmap
binder_alloc
。她就是struct binder_buffer
。
struct binder_buffer
後面,我們傳輸資料時,一次傳輸的資料就對應一個binder_buffer
。
定義如下:
struct binder_buffer {
struct list_head entry; /* free and allocated entries by address */
struct rb_node rb_node; /* free entry by size or allocated entry */
/* by address */
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;
size_t extra_buffers_size;
void *data;
};
-
entry
和rb_node
回到
binder_alloc
://`binder_alloc` struct list_head buffers; // 當前程序所有的 binder_buffer 連結串列 struct rb_root free_buffers; // 空閒狀態的 binder_buffer 紅黑樹 struct rb_root allocated_buffers; //正在使用的 binder_buffer 紅黑樹
binder_alloc::buffers
就是是一個連結串列,當前程序所有的binder_buffer
都會掛載到這個連結串列(通過entry
)。此外,
binder_buffer
一旦建立,就不會被輕易回收記憶體,所以,他有兩種狀態,被使用和空閒。根據其狀態,binder驅動將其掛載到binder_alloc
的兩棵紅黑樹中(通過rb_node
)。 -
其餘的先不說,說多了很懵逼。
我們再回到binder_mmap
中,前面Binder驅動已經完成了 核心和使用者空間虛擬地址的對映,並且建立了一堆page。接下來,就是先建立一個binder_buffer
,然後將其新增到binde_alloc::buffers
和binder_alloc::free_buffers
中。
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma) {
...
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
buffer->data = alloc->buffer;
//新增到連結串列中
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1; //空閒buffer
//新增到空閒buffer紅黑樹中
binder_insert_free_buffer(alloc, buffer);
alloc->free_async_space = alloc->buffer_size / 2;
...
}
到這裡,mmap
就結束了,除了為這些資料結構分配記憶體,沒有看到為mmap
的虛擬地址分配實體記憶體的操作。
記憶體管理
32bit系統
這一節我們會知道,Binder一次傳輸的資料最大是多少!Binder的一次記憶體拷貝發生在什麼時候。
mmap
我們知道,程序在使用binder時需要用到ProcessState
類,其物件在構造的時候就會開啟binder確定並執行mmap
。
//#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
if (mVMStart == MAP_FAILED) {
close(mDriverFD);
mDriverFD = -1;
}
-
PROT_READ
,只讀對映。簡單的理解,就是
mmap
出來的虛擬地址空間是作為 資料接收緩衝區用的 -
mmap
對映的虛擬地址空間大小是1M - 8K
。
對於由Zygote
孵化的程序,都預設建立了ProcessState
物件,所以其傳輸的最大資料就是1M-8K
,當然,這只是理論值。
為了區分要傳送的資料,還有一些輔助資料結構,這些也是要佔用大小的。
此外,還要記憶體碎片問題。。。。
那是不是我們自己修改ProcessState
中mmap
的對映大小就能夠傳輸大資料呢?
是滴,但是驅動對此也做了限制,最大為4M。
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
...
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
...
}
對於大資料的傳輸,我們可以使用
Ashmem
,然後使用Binder傳輸 檔案描述符實現。
對於mmap
具體做了哪些工作,我們用下面的一張圖來總結。
-
在核心中申請一段連續的 虛擬地址空間。將其起始地址儲存到
binder_alloc->buffer
,將地址空間大小儲存到binder_alloc->pages
。 -
將使用者空間的虛擬地址和核心虛擬地址之間的偏移儲存到
binder_alloc->use_buffer_offset
。這個偏移是固定的,通過這個偏移,binder驅動能輕易的根據核心虛擬地址計算出 使用者空間虛擬地址。反之亦然。
-
將核心虛擬地址分段,並將頁面資訊儲存到
binder_alloc->pages
中。這裡說明一下,雖然名為
pages
,但是實際上就是binder驅動 前面拿到的虛擬地址分成了若干段,然後每一段的大小都是一個page的大小。一開始
SEG
是不會關聯page的,用到的時候才會關聯page。 -
建立一個空的
binder_buffer
,並將其加入到binder_alloc->free_buffers
和binder_alloc->buffers
中。這個在圖中沒有體現出來,binder驅動的記憶體管理更多的就是對
binder_buffer
的管理。後面會詳細介紹。
binder_buffer
該結構體用於快取每次 transaction 時的資料,也就是我們通過IPCThreadState::transact
傳送的資料(用結構體struct binder_transaction_data
表示)。
建立binder_buffer
通過函式binder_alloc_new_buf
能夠獲取到一個滿足我們需求的binder_buffer
。函式原型如下:
struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async);
data_size
: 同binder_transaction_data.data_size
,對應的是我們實際傳送資料(使用者空間的有效資料)的大小。
offsets_size
: 同binder_transaction_data.offsets_size
,對應的是實際傳送資料中binder物件的偏移陣列的大小。
extra_buffers_size
: 0
is_aync
: 本次申請的binder_buffer
是否用於非同步傳輸。
基本流程:
-
計算需要的buffer大小。就是
data_size
和data_offset_size
四位元組對齊後之和。data_offsets_size = ALIGN(data_size, sizeof(void *)) + ALIGN(offsets_size, sizeof(void *)); if (data_offsets_size < data_size || data_offsets_size < offsets_size) { return ERR_PTR(-EINVAL); } //extra_buffers_size == 0 size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *)); if (size < data_offsets_size || size < extra_buffers_size) { return ERR_PTR(-EINVAL); } if (is_async && alloc->free_async_space < size + sizeof(struct binder_buffer)) { return ERR_PTR(-ENOSPC); } /* Pad 0-size buffers so they get assigned unique addresses */ size = max(size, sizeof(void *));
-
在
binder_alloc->free_buffers
中找一個合適的buffer。mmap
中,我們建立了一個空的binder_buffer
,將其加入到了free_buffers
中。程式碼如下:buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); buffer->data = alloc->buffer; list_add(&buffer->entry, &alloc->buffers); buffer->free = 1; binder_insert_free_buffer(alloc, buffer); alloc->free_async_space = alloc->buffer_size / 2;
補充一下,在這裡可以看到,用於非同步傳輸的緩衝區最大值是我們mmap的size的一半。
此時的記憶體關係大概如下:
mmap
時,建立的第一個binder_buffer
,其data
指向binder_alloc->buffer
,也就是當前程序對映的核心虛擬地址空間的首地址。binder驅動計算
binder_buffer
大小並不會根據其傳輸的實際資料來計算的,而是根據前後兩個binder_buffer
的距離來計算的。static size_t binder_alloc_buffer_size(struct binder_alloc *alloc, struct binder_buffer *buffer) { //如果是buffer_alloc->buffers中的最後一個buffer,其大小就是 最後一個buffer起始地址到 虛擬地址空間末尾。 if (list_is_last(&buffer->entry, &alloc->buffers)) return (u8 *)alloc->buffer + alloc->buffer_size - (u8 *)buffer->data; // 後一個buffer的起始地址減去當前buffer的起始地址。 return (u8 *)binder_buffer_next(buffer)->data - (u8 *)buffer->data; }
我們假設,我們是第一次建立
binder_buffer
。我們從binder_alloc->free_buffers
中拿到的free_buffer
就是mmap
中建立的。其大小就是binder_alloc->buffer_size
(通常就是1M - 8K
)。很明顯,這個free_buffer
太大了。 -
分配實體記憶體。
分配實體記憶體的基本單位是page。 這裡不是按照
binder_buffer
的大小來分配,而是按照實際傳輸的資料大小來分配。分配工作由
binder_update_page_range
完成。// free_buffer的 最後一個 page的首地址 has_page_addr = (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK); //我們實際需要的 資料 對應的 page的首地址 end_page_addr = (void *)PAGE_ALIGN((uintptr_t)buffer->data + size); // binder_buffer size > actual size if (end_page_addr > has_page_addr) end_page_addr = has_page_addr; ret = binder_update_page_range(alloc, 1, (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr);
binder_update_page_range
的主要工作就是通過alloc_page
申請page
,然後建立page
和核心虛擬地址空間及使用者虛擬地址空間之間的聯絡。這裡才是一次記憶體拷貝的關鍵,通過 使用者空間虛擬地址和核心的虛擬地址訪問到的是同一片實體記憶體。
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) { int ret; bool on_lru; size_t index; index = (page_addr - alloc->buffer) / PAGE_SIZE; page = &alloc->pages[index]; if (page->page_ptr) { //直接複用之前申請過的 page on_lru = list_lru_del(&binder_alloc_lru, &page->lru); continue; } //申請 page page->page_ptr = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO); page->alloc = alloc; INIT_LIST_HEAD(&page->lru); // 建立核心虛擬地址和page的對映 ret = map_kernel_range_noflush((unsigned long)page_addr, PAGE_SIZE, PAGE_KERNEL, &page->page_ptr); flush_cache_vmap((unsigned long)page_addr, (unsigned long)page_addr + PAGE_SIZE); // 建立 使用者空間和page的對映 user_page_addr = (uintptr_t)page_addr + alloc->user_buffer_offset; ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr); if (index + 1 > alloc->pages_high) alloc->pages_high = index + 1; }
-
裁剪一個新的
free binder_buffer
既然前面的
free_buffer
過大,我們就可以將其裁剪為兩個,一個夠用,另一個就是剩下的全部,然後將其加入到binder_alloc->free_buffer
.// buffer_size 就是前面提到的`1M - 8K` // size 就是我們需要的大小。 if (buffer_size != size) { struct binder_buffer *new_buffer; new_buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); new_buffer->data = (u8 *)buffer->data + size; //將剩餘空間都給這個新的free buffer list_add(&new_buffer->entry, &buffer->entry); new_buffer->free = 1; binder_insert_free_buffer(alloc, new_buffer); }
-
將申請到的
binder_buffer
加入到binder_alloc->allocated
中。通過函式
binder_insert_allocated_buffer_locked
完成。
歸還binder_buffer
當binder_buffer
完成了傳輸任務後,就需要將其歸還,以便其他傳輸任務使用。歸還操作對應的函式是binder_free_buf_locked
。
歸還操作如下:
-
歸還 申請的
page
, 也是通過binder_update_page_range
完成。歸還操作程式碼大致如下:for (page_addr = end - PAGE_SIZE; page_addr >= start; page_addr -= PAGE_SIZE) { bool ret; size_t index; index = (page_addr - alloc->buffer) / PAGE_SIZE; page = &alloc->pages[index]; ret = list_lru_add(&binder_alloc_lru, &page->lru); }
如上,將
binder_buffer
佔用的page
頁新增到binder_alloc_lru
中,值得注意的時,歸還page
並不是將其歸還給作業系統,而是歸還給binder_alloc
,其還是佔據實際的實體記憶體的,這樣做的目的是避免頻繁的alloc_page
和free_page
的呼叫。再來看一下呼叫程式碼:
binder_update_page_range(alloc, 0, (void *)PAGE_ALIGN((uintptr_t)buffer->data), (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
PAGE_ALIGN
:如果(uintptr_t)buffer->data
的地址不是page
的其實地址,其返回值就是下一個page的其實地址。(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK)
,實際上就是向下取整。用圖片來解釋一下。由於
PAGE1
和PAGE3
同時被其他的binder_buffer
所引用,我們能回收的只有PAGE2
。圖片畫的可能有點歧義,橙色部分應該 binder_buffer中資料佔用佔用的記憶體對應的page。
-
判斷是否需要合併
binder_buffer
。如上圖所示,
curr
表示我們當前需要歸還的binder_buffer
,它的下一個指向next
,也是free
狀態的,所以要將這兩個binder_buffer
合併。同時,它的上一個
prev
也是free
狀態,所以也要合併,最終這三個binder_buffer
會合併成一個。// 後面還有一個空閒 binder_bufer, 刪除它 if (!list_is_last(&buffer->entry, &alloc->buffers)) { struct binder_buffer *next = binder_buffer_next(buffer); if (next->free) { rb_erase(&next->rb_node, &alloc->free_buffers); // binder_delete_free_buffer(alloc, next); } } // 前面還有一個空閒 binder_buffer, 刪除當前buffer if (alloc->buffers.next != &buffer->entry) { struct binder_buffer *prev = binder_buffer_prev(buffer); if (prev->free) { binder_delete_free_buffer(alloc, buffer); rb_erase(&prev->rb_node, &alloc->free_buffers); buffer = prev; } }
合併操作的主要工作由
binder_delete_free_buffer
完成。按理說只要釋放掉binder_buffer
的記憶體就可以了,反正page
前面已經被binder_alloc
處理了。但有一種特殊情況。就是兩個binder_buffer
處於同一個page
。static void binder_delete_free_buffer(struct binder_alloc *alloc, struct binder_buffer *buffer) { struct binder_buffer *prev, *next = NULL; bool to_free = true; BUG_ON(alloc->buffers.next == &buffer->entry); prev = binder_buffer_prev(buffer); BUG_ON(!prev->free); // buffer->prev 的尾部 和 buffer的首部 在同一個 page中。 if (prev_buffer_end_page(prev) == buffer_start_page(buffer)) { // 那就不能回收當前page to_free = false; } if (!list_is_last(&buffer->entry, &alloc->buffers)) { next = binder_buffer_next(buffer); // buffer->next 的首部和 buffer 的尾部 在同一個page中 if (buffer_start_page(next) == buffer_start_page(buffer)) { // 那也能回收當前page to_free = false; } } // buffer的首剛好是page 的首地址 if (PAGE_ALIGNED(buffer->data)) { // 也不能回收當前page to_free = false; } if (to_free) { // 為什麼只回收一個page呢? binder_update_page_range(alloc, 0, buffer_start_page(buffer), buffer_start_page(buffer) + PAGE_SIZE); } list_del(&buffer->entry); //釋放 binder_buffer佔用的記憶體 kfree(buffer); }
為什麼
PAGE_ALIGNED(buffer->data)
成立是,不用回收當前的page?為什麼後面只回收只用回收一個PAGE
呢?回到我們剛剛的圖片。我們回收中間的B(
binder_buffer
)後,PAGE2
已經被回收(新增到binder_alloc_lru
中)了(此時B已經是空閒binder_buffer
)。由於A
和C
的存在,我們沒有合併操作。現在我們要回收A
。首先執行的是下面的程式碼://binder_free_buf_locked binder_update_page_range(alloc, 0, (void *)PAGE_ALIGN((uintptr_t)buffer->data), (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK));
在回收
binder_buffer A
時,以這段程式碼而言,PAGE_ALIGN((uintptr_t)buffer->data)
的值是PAGE2
的起始地址,(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK)
的值是PAGE1
的起始地址。所以這段程式碼不會執行回收操作。然後執行的是
binder_delete_free_buffer
。由於A
後面的B
是空閒狀態,所以要合併A
和B
。合併後的狀態如下:在回到
binder_delete_free_buffer
中,按照該函式的流程走下來,我們只要回收PAGE1
就行。這也是為什麼binder_delete_free_buffer
中的binder_update_page_range
只會回收一個頁面。同理,如果
A
剛好是在PAGE1
的首地址,那麼在進入binder_free_buf_locked
的第一個binder_update_page_range
時,PAGE1
就會被回收。所以binder_delete_free_buffer
也就不需要回收PAGE
。最後一步兩個
binder_buffer
合併後,多出一個binder_buffer
,binder驅動要會後這個資料結構佔用的記憶體。
總結
關於Binder驅動的記憶體管理就介紹到這裡,回顧一下重點。
-
一次記憶體拷貝的原理。
mmap
在使用者空間和核心空間 各分配了一個連續的虛擬地址空間。當我們建立binder_buffer
時,驅動會將這兩個虛擬地址(大小就是傳輸資料的實際大小)對映到相同的實體記憶體上。就避免了 一次從核心往使用者空間拷貝的操作。 -
binder一次傳輸最大的size是多少
-
zygote孵化的程序理論上最大是
1M-8K
(有ProcessState
限制),對於非同步傳輸是1M-8K
的一半 -
對於一般的native程序,最大是
4M
,由Binder驅動限制。 -
由前面的分析可以知道,當前能夠傳輸的資料最大size取決於
binder_alloc->free_buffers
中最大的空閒buffer的size
.
-
資料傳輸原理
之前已經介紹過,資料從程序A到程序B的 native 層的實現,這一章節主要就是介紹Binder
驅動是如何傳輸資料的。
不同於TCP的流式傳輸,Binder
更像UDP的報文傳輸,binder驅動需要明確知道傳輸資料的size(從前面的記憶體管理一節也可以看出這一點)。
首先看一下資料傳輸的基本呼叫流程(BC_TRANSACTION
):
ioctl(fd, BINDER_WRITE_READ, &bwr);
---------進入binder驅動----------
binder_ioctl
binder_ioctl_write_read
binder_thread_write
binder_transaction //(分析重點)
binder_thread_read //(分析重點)
前面我們雖然提到了一次記憶體拷貝的本質,但是這個一次記憶體拷貝發生在什麼時候???? 還有 傳輸 binder 物件和檔案描述符的時候,binder驅動做了什麼事情??
大多數情況下,binder資料傳輸都是類似於C/S
模式,有客戶端發起請求,服務端響應客戶端的請求。接下來我們也以著重分析請求和響應兩個過程。
資料從Client到Service
當我們通過mRemote()->transaction
傳送資料時,最終執行的是ioctl(fd, BINDER_WRITE_READ, &bwr)
。該呼叫會導致當前程序進入核心態。最終呼叫到binder驅動的binder_ioctl
函式。