Binder進程間通訊(五)----內核虛擬地址空間管理

    上一篇筆記分析到了Binder驅動在調用mmap建立內存映射的過程。可是在分配物理內存的時候,咱們只分配了一個頁面。當咱們須要更多物理內存的時候,Binder驅動纔回去繼續分配物理內存。node

    另外,物理內存的分配是以頁爲單位的,可是在進程中使用內存是卻並非以頁爲單位,因此binder驅動程序使用結構體binder_buffer來描述進程對內存的使用,表示一塊內存。linux

分配內核緩衝區

    當一個進程使用BC_TRANSACTION 或者BC_REPLY向binder驅動發送命令請求和另外一個進程通訊時須要向另外一個進程傳遞數據(經過結構體binder_transaction_data結構體),binder驅動程序就會將這些數據從用戶空間拷貝到內核空間(描述不許確的,應該是從client進程的用戶空間拷貝到service進程的內核空間,由於同一個binder_proc的內核空間和用戶空間映射的是同一片物理內存,理論上並不須要拷貝的),而後再傳遞給目標進程。這時候,binder驅動須要在目標進程的內核虛擬地址空間中分配一塊地址來存放這些數據。算法

    因此這裏涉及到一個關鍵步驟:分配內核虛擬地址空間,這是經過binder_alloc_buf實現的數據結構

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	.......
}

    參數就頗有講究,須要理解一下,proc表示的是目標進程。第二第三個參數就是binder_transaction_data數據結構中的data_size和offsets_size(binder結構體說明),最後一個參數表示請求的內核虛擬地址空間用於同步事物仍是異步事物。異步

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	struct rb_node *n = proc->free_buffers.rb_node;
	struct binder_buffer *buffer;
	size_t buffer_size;
	struct rb_node *best_fit = NULL;
	void *has_page_addr;
	void *end_page_addr;
	size_t size;

	if (proc->vma == NULL) {
		pr_err("%d: binder_alloc_buf, no vma\n",
		       proc->pid);
		return NULL;
	}

    //按void指針大小對齊,size表示要分配的虛擬地址空間的大小
	size = ALIGN(data_size, sizeof(void *)) +
		ALIGN(offsets_size, sizeof(void *));
    //溢出檢測
	if (size < data_size || size < offsets_size) {
		binder_user_error("%d: got transaction with invalid size %zd-%zd\n",
				proc->pid, data_size, offsets_size);
		return NULL;
	}
    //若是使用異步請求,那麼還須要檢測須要使用的內核虛擬地址空間大小 是否大於 剩餘可用於異步操做的地址大小
    //(咱們在binder_mmap的時候有看到,初始設置爲總虛擬地址空間大小的一半)
	if (is_async &&
	    proc->free_async_space < size + sizeof(struct binder_buffer)) {
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: binder_alloc_buf size %zd failed, no async space left\n",
			      proc->pid, size);
		return NULL;
	}

	......
}

    須要注意在咱們計算須要的內核虛擬地址空間總大小的時候,還須要加上一個binder_buffer結構體的大小。上文有提到,咱們會使用binder_buffer來組織內存中的數據。async

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{

	.....
    //使用最佳匹配算法,在空閒的內核虛擬地址空間樹種(proc->buffer_free中)尋找有沒有適合大小的空間可用
	while (n) {
		buffer = rb_entry(n, struct binder_buffer, rb_node);
		BUG_ON(!buffer->free);
        //至關於binder_buffer.data的長度
		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;
		}
	}
    //沒有找到,則直接報錯退出
	if (best_fit == NULL) {
		pr_err("%d: binder_alloc_buf size %zd failed, no address space\n",
			proc->pid, size);
		return NULL;
	}
    //沒有找到大小恰好合適的空間大小,可是找到了一塊較大的地址空間,而後計算地址空間大小,並保存在buffer_size變量中
	if (n == NULL) {
		buffer = rb_entry(best_fit, struct binder_buffer, rb_node);
		buffer_size = binder_buffer_size(proc, buffer);
	}

	......
}

    以上一段代碼的主要做用是尋找空閒的內核虛擬地址空間。函數

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	.......

    //計算buffer(上面找到的空閒內核虛擬地址空間起始地址)結束地址所在的頁 的起始地址
	has_page_addr =
		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);

	if (n == NULL) {
		if (size + sizeof(struct binder_buffer) + 4 >= buffer_size)
			buffer_size = size; /* no room for other buffers */
        // 表示找到的buffer大小相比 須要的buffer大小 大了挺多,直接用浪費裁剪一下。
		else
			buffer_size = size + sizeof(struct binder_buffer);
	}
    
	end_page_addr =
		(void *)PAGE_ALIGN((uintptr_t)buffer->data + buffer_size);
	if (end_page_addr > has_page_addr)
		end_page_addr = has_page_addr;
	if (binder_update_page_range(proc, 1,
	    (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr, NULL))
		return NULL;

	......
}

    上面這段代碼應該說是這個函數,甚至是binder對於內核空間分配和創建物理內存映射最關鍵的幾局代碼了。並且更主要的是理解很是困難!!我花了大概一個下午才弄通其中的邏輯。ui

    在理解以前,先給出一個結論,這在理解這段代碼的時候很是關鍵。spa

    咱們在上文找到了一個binder_buffer類型的對象buffer,表示一個未被使用的虛擬地址空間段,用圖來表示是這樣的.net

                       

    buffer由兩部分組成,binder_buffer結構體,以及後面的data數據空間(建議回顧一下binder_buffer的結構)。

    根據尋找buffer的代碼,咱們分兩種狀況來討論:

狀況 1 

    首先是has_page_addr的計算,buffer->data+buffer_size的結果是這個buffer的結束地址。若是不知道爲何……本身再去看看代碼吧,buffer_size表示的並非整個buffer的長度,而是buffer第二部分data的長度。而後獲取這個地址所在的頁的起始位置,就以下圖(虛線表示分頁的線)

                        

    size + sizeof(struct binder_bufffer)+4 > buffer_size  變換一下 :

                                buffer_size - size < sizeof(struct binder_bufffer) + 4   

   表示去掉實際須要的data區域以後,剩下的空間不夠分配一個新的binder_buffer內存塊的。這種狀況下,讓buffer_size = size;    

    複雜的來了,end_page_addr的計算,首先buffer_data + buffer_size,獲得一個地址,咱們暫且將這個地址命名爲 「z地址」! 

                    

    接下去就要討論end_page_addr和 has_page_addr的關係

                    

                        

    點狀虛線表示頁分割地址,這兩種狀況 z地址 和 buffer結束地址 處於同一頁,那麼end_page_addr >has_pager_addr,而後手動調整成 end_page_addr = has_pager_addr.

                    

                    

    上面這種狀況,直接has_page_addr = end_page_addr,以後的處理就和以前同樣了。

    思考:會出現end_page_addr < has_page_addr嗎?不會,由於若是要出現這種狀況,那麼剩餘空間必定須要超過1頁大小,這就不符合咱們對狀況1的定義。

    最後調用binder_update_page_range方法來進行物理內存分配(這個函數在binder_mmap創建映射關係的時候講解過)。關鍵是傳入的 start 和 end兩個參數。其實地址是buffer->data按頁對齊(將起始地址按頁對齊很好理解,由於物理內存的分配是按頁分配的,因此每一個頁面的起始地址和結束地址確定是按頁對齊的),結束地址是end_page_addr。會出現兩種狀況:

    start < end ,這個時候binder_update_page_range 調用和之前同樣,分配一頁或者多頁(size很大,跨了多頁)的內存。

    對齊後start = end,這個時候binder_update_page_range 並不會分配內存,而是直接返回了。那就很奇怪了,這種狀況咱們不須要分配物理內存嗎?

    仔細看上面幾幅圖,咱們發現,第一圖size佔了三頁,可是咱們最後只分配了一頁,中間那頁,左邊不須要分配嗎?

    思考,咱們在mmap的時候分配了第一頁的內存,而且建立了一個binder_buffer,若是這時候我找到的buffer就是這個binder_buffer呢?那麼第一頁已經被分配,因此左邊不須要分配。

    若是這個buffer不是處於開頭,那麼說明前面的buffer必定在被使用(未被使用的話,這兩個buffer會合並的,在釋放物理內存的地方能夠找到依據),因此前面那也必定是已經被分配了,因此左邊不用再次分配。

    左邊爲何不分配的緣由找到了,再來思考右邊:

    若是buffer處於整個buffers的末尾,咱們須要知道linux規定,內核地址空間的最後必需要留一頁當作內存保護,也就是說,buffer的末尾地址不可能和z地址在同一個頁中,因此不存在右側有空間未分配的狀況

    若是不是處於buffer末尾,那麼右側的buffer必定是已經被使用了的,不然會合並。既然已經被使用,那麼右側的物理內存確定已經被分配了,因此沒問題。

    再如圖三,size僅僅在一個頁面內,可是這個頁面也沒有分配內存,因此它的內存頁必定被分配了嗎?是的,若是左側相鄰的binder_buffer和他在同一頁,那麼這一頁已經被分配,若是不是在同一頁,那麼說明個buffer的binder_buffer結構體跨越了兩個頁,這種狀況下這一頁的物理內存早就被分配好了(見下文)。

    PS : 咱們會發現多了一塊剩餘空間,咱們既沒有使用它,也沒有給他分配物理內存。這表示,不必定全部binder_buffer都是相鄰的,可能中間還會隔着一個大小比較小的空閒區域。

狀況2

    has_page_addr的計算和狀況1相同。

    關鍵剩餘的空間足夠分配下一個binder_buffer,因此須要進行分割。

                

    ps:上圖中buffer_size仍是最開始的buffer_size,不是裁剪後的buffer_size,這個變量的複用真的讓代碼變得很不清晰,萬惡!

    end_page_addr的大小和狀況1不一樣,不只僅包含了實際須要數據的大小size,並且還包含了一個額外空間,這個額外空間大小是binder_buffer結構體的大小。

    而後和上面狀況1同樣分配物理內存。咱們爲何要爲額外空間分配內存?想一想,這個額外空間是什麼?他必定會成爲下一個binder_buffer的binder_buffer的結構體的內存區域,咱們提早爲他所在頁分配了物理內存。    

    上面這段文字可能並非很容易理解,事實上若是不本身思考一些時間,也不可能理解,思考,結合圖片才能帶來有效的成果。

    

static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,
					      size_t data_size,
					      size_t offsets_size, int is_async)
{
	....
    // buffer已經被從新使用,因此從原來空閒列表中去除
	rb_erase(best_fit, &proc->free_buffers);
	buffer->free = 0;
    //插入buffer列表中
	binder_insert_allocated_buffer(proc, buffer);
    //進行了切割纔會出現這個狀況
	if (buffer_size != size) {
        //將不使用的內核虛擬地址空間新建一個binder_buffer對象
		struct binder_buffer *new_buffer = (void *)buffer->data + size;
        //插入到buffer列表中
		list_add(&new_buffer->entry, &buffer->entry);
		new_buffer->free = 1;
        //同時插入到free_buffer列表中
		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的屬性
	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);
	}

	return buffer;
}

    最後剩下的代碼就相對容易理解不少了。

 

釋放內核緩衝區

    當進程處理完Binder驅動發給他的BR_TRANSACTION或者BR_REPLY以後,就會使用BC_FREE_BUFFER來通知Binder驅動程序釋放相應的內核緩衝區,以避免浪費空間。

    釋放內存經過函數binder_free_buf實現

static void binder_free_buf(struct binder_proc *proc,
			    struct binder_buffer *buffer)
{
	size_t size, buffer_size;
    //
	buffer_size = binder_buffer_size(proc, buffer);

	size = ALIGN(buffer->data_size, sizeof(void *)) +
		ALIGN(buffer->offsets_size, sizeof(void *));

	......
    //若是是異步事物,二話不說,先把異步事物空間讓出來再說!
	if (buffer->async_transaction) {
		proc->free_async_space += size + sizeof(struct binder_buffer);

		binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,
			     "%d: binder_free_buf size %zd async free %zd\n",
			      proc->pid, size, proc->free_async_space);
	}
    //釋放當前buffer數據區所佔用的虛擬地址空間所對應的物理內存。
	binder_update_page_range(proc, 0,
		(void *)PAGE_ALIGN((uintptr_t)buffer->data),
		(void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK),
		NULL);
    //將他從正在使用的buffer列表中刪除
	rb_erase(&buffer->rb_node, &proc->allocated_buffers);
	buffer->free = 1;
    //查找該buffer的下一個binder_buffer是否爲空buffer,若是是,那麼須要刪除下一個buffer(和當前buffer合併)
	if (!list_is_last(&buffer->entry, &proc->buffers)) {
		struct binder_buffer *next = list_entry(buffer->entry.next,
						struct binder_buffer, entry);

		if (next->free) {
			rb_erase(&next->rb_node, &proc->free_buffers);
			binder_delete_free_buffer(proc, next);
		}
	}
    //查找該buffer的上一個binder_buffer是否爲空,若是是,那麼刪除當前buffer(合併到上一個buffer)
	if (proc->buffers.next != &buffer->entry) {
		struct binder_buffer *prev = list_entry(buffer->entry.prev,
						struct binder_buffer, entry);

		if (prev->free) {
			binder_delete_free_buffer(proc, buffer);
			rb_erase(&prev->rb_node, &proc->free_buffers);
			buffer = prev;
		}
	}
    //將合併後的buffer插入到空buffer列表中
	binder_insert_free_buffer(proc, buffer);
}

    關於刪除物理內存的地方,若是想要詳細追究,仍是可使用和分配相同的方式去理解。

static void *buffer_start_page(struct binder_buffer *buffer)
{
	return (void *)((uintptr_t)buffer & PAGE_MASK);
}

static void *buffer_end_page(struct binder_buffer *buffer)
{
	return (void *)(((uintptr_t)(buffer + 1) - 1) & PAGE_MASK);
}

    這裏有兩個方法,分別用來計算buffer起始位置所在的頁的地址和buffer結束位置所在的頁的地址。

    這個函數就是上面用來刪除空閒buffer的,在調用這個函數以前咱們已經保證了它不是proc得第一個buffer,並且它前面的binder_buffer必定是空的。

static void binder_delete_free_buffer(struct binder_proc *proc,
				      struct binder_buffer *buffer)
{
	struct binder_buffer *prev, *next = NULL;
	int free_page_end = 1;
	int free_page_start = 1;

	BUG_ON(proc->buffers.next == &buffer->entry);
	prev = list_entry(buffer->entry.prev, struct binder_buffer, entry);
	BUG_ON(!prev->free);
    //若是前一個buffer的結束地址和當前buffer的開始地址在同一個頁面上(兩個buffer不必定相鄰的)
	if (buffer_end_page(prev) == buffer_start_page(buffer)) {
		free_page_start = 0;
        //若是前一個buffer的結束地址和當前頁面的結束地址在同一個頁面上
		if (buffer_end_page(prev) == buffer_end_page(buffer))
			free_page_end = 0;
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: merge free, buffer %p share page with %p\n",
			      proc->pid, buffer, prev);
	}
    //若是當前buffer不是最後一個buffer
	if (!list_is_last(&buffer->entry, &proc->buffers)) {
		next = list_entry(buffer->entry.next,
				  struct binder_buffer, entry);
        //若是下一個buffer的起始地址和當前buffer的結束地址在同一個頁面上
		if (buffer_start_page(next) == buffer_end_page(buffer)) {
			free_page_end = 0;
            //若是下一個buffer起始地址和當前buffer的起始地址在同一個buffer上
			if (buffer_start_page(next) ==
			    buffer_start_page(buffer))
				free_page_start = 0;
			binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
				     "%d: merge free, buffer %p share page with %p\n",
				      proc->pid, buffer, prev);
		}
	}
    //將當前buffer從buffer列表中刪除
	list_del(&buffer->entry);
	if (free_page_start || free_page_end) {
		binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
			     "%d: merge free, buffer %p do not share page%s%s with %p or %p\n",
			     proc->pid, buffer, free_page_start ? "" : " end",
			     free_page_end ? "" : " start", prev, next);
		binder_update_page_range(proc, 0, free_page_start ?
			buffer_start_page(buffer) : buffer_end_page(buffer),
			(free_page_end ? buffer_end_page(buffer) :
			buffer_start_page(buffer)) + PAGE_SIZE, NULL);
	}
}

    這一段在羅老師的書中仍是介紹的比較詳細的,說到底這裏進行那麼多if的判斷都是防止內存的錯誤釋放,只有當前一整頁的內存都再也不被使用,纔回去釋放物理內存,不然不會釋放物理內存。

    狀況雖然不少,可是經過畫圖和代碼仍是比較容易理解的,這裏就不分析了。

查詢內存緩衝區

    做爲一個用戶進程,在通知Binder驅動程序表示我要釋放內存的時候,帶上了一個地址參數,可是這個地址參數確定是用戶空間的虛擬地址空間的地址,不多是內核空間的地址,因此,驅動程序還須要經過用戶空間地址來尋找內核空間對應的地址,經過函數binder_buffer_lookup

// user_ptr就是用戶進程傳給binder驅動程序的參數
// 表示用戶空間binder_buffer的data所指向的地址。
static struct binder_buffer *binder_buffer_lookup(struct binder_proc *proc,
						  uintptr_t user_ptr)
{
	struct rb_node *n = proc->allocated_buffers.rb_node;
	struct binder_buffer *buffer;
	struct binder_buffer *kern_ptr;
    
    //計算內核緩衝區中對應的binder_buffer的地址
	kern_ptr = (struct binder_buffer *)(user_ptr - proc->user_buffer_offset
		- offsetof(struct binder_buffer, data));

    //循環,經過這個buffer地址,找到對應的buffer節點。
	while (n) {
		buffer = rb_entry(n, struct binder_buffer, rb_node);
		BUG_ON(buffer->free);

		if (kern_ptr < buffer)
			n = n->rb_left;
		else if (kern_ptr > buffer)
			n = n->rb_right;
		else
			return buffer;
	}
	return NULL;
}
相關文章
相關標籤/搜索