1. 程式人生 > 實用技巧 >Android Binder機制 -- Binder驅動

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

    vmavma_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;
};
  • entryrb_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::buffersbinder_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;
}
  1. PROT_READ,只讀對映。

    簡單的理解,就是 mmap出來的虛擬地址空間是作為 資料接收緩衝區用的

  2. mmap對映的虛擬地址空間大小是1M - 8K

對於由Zygote孵化的程序,都預設建立了ProcessState物件,所以其傳輸的最大資料就是1M-8K,當然,這只是理論值。

為了區分要傳送的資料,還有一些輔助資料結構,這些也是要佔用大小的。

此外,還要記憶體碎片問題。。。。

那是不是我們自己修改ProcessStatemmap的對映大小就能夠傳輸大資料呢?

是滴,但是驅動對此也做了限制,最大為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具體做了哪些工作,我們用下面的一張圖來總結。

  1. 在核心中申請一段連續的 虛擬地址空間。將其起始地址儲存到binder_alloc->buffer,將地址空間大小儲存到binder_alloc->pages

  2. 將使用者空間的虛擬地址和核心虛擬地址之間的偏移儲存到binder_alloc->use_buffer_offset

    這個偏移是固定的,通過這個偏移,binder驅動能輕易的根據核心虛擬地址計算出 使用者空間虛擬地址。反之亦然。

  3. 將核心虛擬地址分段,並將頁面資訊儲存到binder_alloc->pages中。

    這裡說明一下,雖然名為pages,但是實際上就是binder驅動 前面拿到的虛擬地址分成了若干段,然後每一段的大小都是一個page的大小。

    一開始 SEG是不會關聯page的,用到的時候才會關聯page。

  4. 建立一個空的binder_buffer,並將其加入到binder_alloc->free_buffersbinder_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是否用於非同步傳輸。

基本流程:

  1. 計算需要的buffer大小。就是data_sizedata_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 *));
    
  2. 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太大了。

  3. 分配實體記憶體。

    分配實體記憶體的基本單位是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;
    	}
    
  4. 裁剪一個新的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);
    }
    
  5. 將申請到的binder_buffer加入到binder_alloc->allocated中。

    通過函式binder_insert_allocated_buffer_locked完成。

歸還binder_buffer

binder_buffer完成了傳輸任務後,就需要將其歸還,以便其他傳輸任務使用。歸還操作對應的函式是binder_free_buf_locked

歸還操作如下:

  1. 歸還 申請的 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_pagefree_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),實際上就是向下取整。用圖片來解釋一下。

    由於PAGE1PAGE3同時被其他的binder_buffer所引用,我們能回收的只有PAGE2

    圖片畫的可能有點歧義,橙色部分應該 binder_buffer中資料佔用佔用的記憶體對應的page。

  2. 判斷是否需要合併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)。由於AC的存在,我們沒有合併操作。現在我們要回收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是空閒狀態,所以要合併AB。合併後的狀態如下:

    在回到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驅動的記憶體管理就介紹到這裡,回顧一下重點。

  1. 一次記憶體拷貝的原理。

    mmap在使用者空間和核心空間 各分配了一個連續的虛擬地址空間。當我們建立binder_buffer時,驅動會將這兩個虛擬地址(大小就是傳輸資料的實際大小)對映到相同的實體記憶體上。就避免了 一次從核心往使用者空間拷貝的操作。

  2. 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函式。

資料從Service到Client