cache和主存的映射方式,Binder內存拷貝的本質和變遷

 2023-11-30 阅读 20 评论 0

摘要:說起Binder的內存拷貝,相信大多數人都聽過“一次拷貝”:相較于傳統IPC的兩次拷貝,Binder在數據傳輸時顯得效率更高。 其實不少人在面試時都能回答出上面這句話,但若是追問他更多細節,估計又啞口無言了。 cache和主存的映射方式。其實內存拷

說起Binder的內存拷貝,相信大多數人都聽過“一次拷貝”:相較于傳統IPC的兩次拷貝,Binder在數據傳輸時顯得效率更高。

其實不少人在面試時都能回答出上面這句話,但若是追問他更多細節,估計又啞口無言了。

cache和主存的映射方式。其實內存拷貝的概念既簡單又復雜。簡單是因為它功能單一,而復雜則在于不少人對于虛擬內存,物理內存,用戶空間,內核空間的認識并不充分。所謂地基不穩,高樓難立。

本文嘗試揭示Binder內存拷貝的本質,另外還會介紹新版本中相應實現的一些改動。

1. 內存拷貝概述

在做任何一件事之前,先明確目的。我相信Binder的開發者在最初設計時也一定仔細考慮過這個問題。根據我的理解,Binder數據傳輸的目的可以概括成這句話:

手機主板壞了 數據拷貝。一個進程可以通過自己用戶空間的虛擬地址訪問另一個進程的數據。

要想充分理解這句話,需要在基礎知識上達成一些共識。

1.1 虛擬地址和數據的關系

所有的數據都存儲在物理內存中,而進程訪問內存只能通過虛擬地址。因此,若是想成功訪問必須得有個前提:

電腦的硬盤和內存有什么區別、虛擬地址和物理內存之間建立映射關系

若是這層映射關系不建立,則訪問會出錯。信號11(SIGSEGV)的MAPERR就是專門用來描述這種錯誤的。

虛擬地址和物理地址間建立映射關系通過mmap完成。這里我們不考慮file-back的mapping,只考慮anonymous mapping。當mmap被調用(flag=MAP_ANONYMOUS)時,實際上會做以下兩件事:

  1. 分配一塊連續的虛擬地址空間。
  2. 更新這些虛擬地址對應的PTE(Page Table Entry)。

蘋果拷貝什么意思。mmap做完這兩件事后,就會返回連續虛擬地址空間的起始地址。在mmap調用結束后,其實并不會立即分配物理頁。如果此時不分配物理頁,那么就會有如下兩個問題:

  1. 沒有新的物理頁分配,那么PTE都更新了哪些內容?
  2. 如果后續使用mmap返回的虛擬地址訪問內存,會有什么情況產生呢?

1.1.1 沒有新的物理頁分配,那么PTE都更新了些什么內容呢?

PTE也即頁表的條目,它的內容反映了一個虛擬地址到物理地址之間的映射關系。如果沒有新的物理頁分配,那這些新的虛擬地址都和哪些物理地址之間建立了映射關系呢?答案是所有的虛擬地址都和同一個zero page(頁內容全為0)建立了映射關系。

克隆存儲庫,1.1.2 如果后續使用mmap返回的虛擬地址訪問內存,會有什么情況產生呢?

拿到mmap返回的虛擬地址后,并不會有新的物理頁分配。此時若是直接讀取虛擬地址中的值,則會通過PTE追蹤到剛剛建立映射關系的zero page,因此讀取出來的值都是0。

如果此時往虛擬地址中寫入數據,將會在page fault handler中觸發一個正常的copy-on-write機制。需要寫多少頁,就會新分配多少物理頁。所以我們可以看到,真實的物理頁是符合lazy(on-demand) allocation原則的。這一點,極大地保證了物理資源的合理分配和使用。

1.2 進程間用戶空間/內核空間是否隔離?

快速內存拷貝?先說結論,不同進程間的用戶空間是完全隔離的,內核空間是共享的。

那么“隔離”和“共享”在這個語境下又是什么意思呢?

從實現角度而言,“隔離”的意思是不同進程的頁表不同,“共享”的意思是不同進程的頁表相同,僅此而已。我們知道,頁表反映的是虛擬地址和物理地址的映射關系。那么一張頁表應該管理哪些虛擬地址呢?是整個地址空間的所有虛擬地址么?

當然不是。Linux將虛擬地址空間分為了用戶空間和內核空間,因此管理不同空間虛擬地址的頁表也不一樣。

如上圖所示,A進程的用戶空間使用頁表1,B進程的用戶空間使用頁表2,而A/B進程的內核空間都使用頁表3。A/B中使用相同的用戶空間虛擬地址來訪問內存,由于頁表不同,因此最終映射的物理頁也不同,這就是所謂的“進程隔離”。而由于A/B進程的內核空間使用了同一張頁表,所以只要他們使用相同的虛擬地址(位于內核空間),那么必然訪問到同一個物理頁。

1.3 數據傳輸的兩種方式

1.3.1 共享內存

虛擬地址只是為了進行內存訪問封裝的一層接口,而數據總歸是存在物理內存上的。因此,若是想讓A進程通過(用戶空間)虛擬地址訪問到B進程中的數據,最高效的方式就是修改A/B進程中某些虛擬地址的PTE,使得這些虛擬地址映射到同一片物理區域。這樣就不存在任何拷貝,因此數據在物理空間中也只有一份。

1.3.2 內存拷貝

共享內存雖然高效,但由于物理內存只有一份,因此少不了考慮各種同步機制。讓不同進程考慮數據的同步問題,這對于Android而言是個挑戰。因為作為系統平臺,它必然希望降低開發者的開發難度,最好讓開發者只用管好自己的事情。因此,讓開發者頻繁地考慮跨進程數據同步問題不是一個好的選擇。

取而代之的是內存拷貝的方法。該方法可以保證不同進程都擁有一塊屬于自己的數據區域,該區域不用考慮進程間的數據同步問題。

由于不同進程的內核空間是共享的(只有共享才能完成傳輸,否則只能隔江相望了),因此很自然地考慮到將它作為數據中轉站。常規的做法需要兩次拷貝,一次是由發送進程的用戶空間拷貝到發送進程的內核空間,另一次是由接收進程的內核空間拷貝到接收進程的用戶空間。這兩次拷貝中間有一個隱含的轉換關系,即發送進程的內核空間和接收進程的內核空間是共享的,因此持有相同的虛擬地址就會訪問到同一片物理區域。

兩次拷貝的方法比較符合直覺,但在效率上還有可優化的空間。

既然兩次拷貝都發生在一個進程的用戶空間和內核空間之間,那么其實也就隱含了一個前提:

用戶空間和內核空間的虛擬地址指向不同的物理頁。

正是因為指向不同的物理頁,所以才需要拷貝。那有沒有可能讓二者指向同一個物理頁?如果可以,這樣不就節省了一次拷貝么?

事實上,Binder正是這樣做的。

2. Binder內存拷貝的實現

2.1 早期版本(≤Android P)

為了減少一次拷貝,接收數據的進程必須同時滿足下面三個條件:

  1. 在用戶空間分配一塊連續區域A(僅僅是虛擬地址的分配)。
  2. 在內核空間分配一塊同樣大小的連續區域B(同樣僅僅是虛擬地址的分配)。
  3. 在每次數據通信的時候,根據實際需求分配物理頁,并將該物理頁同時映射到A/B中偏移相同的位置。

條件1、2在進程調用Binder的mmap函數時已經完成,而條件3則在每次數據通信時進行。

下面假設進程1發送數據,進程2接收數據,我們來分析下內存拷貝到底發生在何時(以下執行均發生在進程1中,只不過此時正在執行驅動代碼[陷入內核態])。

  1. 由于進程2之前調用過mmap函數(只會調用一次),因此它擁有用戶空間的區域A和內核空間的區域B(只分配了虛擬地址,并未映射物理頁)。
  2. 得知即將發送的數據大小,并根據該大小分配實際的物理頁。
  3. 將剛剛分配出來的物理頁映射到進程2的A/B區域中(由于進程1處于內核態,因此可以操作進程2的PTE)。
  4. 將用戶空間的發送數據通過copy_from_user拷貝到內核區域B中。
  5. 由于A/B映射到同樣的物理頁,因此B中的數據也可以通過A的地址讀取出來。

整個過程中,只有步驟4發生了一次數據拷貝。

2.2 當前版本(Android Q,R)

從性能角度而言,早期版本的實現幾乎無可挑剔。但是它有一個致命的穩定性缺陷,這是Google工程師們無法忍受的。因此從Android Q開始,Binder內存拷貝的實現有了新的改動。

通過之前的分析可以知道,驅動的mmap函數執行完之后,該進程將會在內核空間分配一塊虛擬地址區域B。對Android應用進程而言,B的默認大小為1M-8K。只要這個進程沒有退出,這1M-8K的虛擬地址就會一直分配給它。

通常對于虛擬地址長時間的占用并不會產生問題,但不幸的是,Binder的這個占用確實產生了問題。

2.2.1 32位機器上Binder內存拷貝的缺陷

32位機器的尋址空間為4G,其中高位的1G用作內核地址空間,低位的3G用作用戶地址空間,這些都是虛擬地址的概念。

1G的內核地址空間又劃分為四塊不同的區域:

  • 直接映射區(Direct memory region),該區域的虛擬地址和物理地址上的低端內存直接映射,因此虛擬地址和物理地址之間永遠差一個固定的偏移。kmalloc分配的地址就位于此塊區域。
  • vmalloc區,該區域的虛擬地址可以映射到物理地址上的高端內存。由于高端內存的地址范圍遠大于vmalloc區域的地址范圍,因此二者之間的映射不能采用線性映射,只能是動態映射。vmalloc分配的地址就位于此塊區域。
  • 臨時映射區(Kmap region),4M的固定大小,主要用于先有物理頁而后需要為其分配內核空間地址的情況。調用一次kmap只能映射一頁,常用于短時間映射的場景。
  • 固定映射區(Fixed mapping region)

vmalloc區的大小隨著Kernel版本的不同也發生過變化。從Kernel 3.13開始,vmalloc區域由128M增加到240M。240M看似是個不小的數字,但在應用啟動過多的手機上將會出問題。此話怎講?

隨著Android Treble項目(Android O引入)的啟動,hardware binder正式進入大眾視野。一方面越來越多的HAL service使用hwbinder進行跨進程通信,另一方面原先只需分配1M-8K的應用進程現在需要多分配一塊區域用于hwbinder通信。因此,binder驅動對于內核空間vmalloc區域的占用成倍地上升。當應用啟動過多時,vmalloc區域的虛擬地址將有可能被耗盡。注意,這里指的是虛擬地址被耗盡,而不是物理地址被耗盡。

當vmalloc區域的虛擬地址被耗盡時,內核中某些使用vmalloc和vmap的代碼將會報錯,因為他們此時分配不出新的虛擬地址。

為了緩解這個問題,一個簡單的想法自然就是增大vmalloc區域。但是1G的內核空間是固定的,厚此必定薄彼。vmalloc區域增大,意味著直接映射區減少。而直接映射區一個最大的好處就是高效(因為采用了線性映射),所以不能被無限制縮小。因此增大vmalloc區域的做法只能算是緩兵之計,絕非最佳策略。

2.2.2 新的實現

讓我們回到最初的目的,仔細思考內核空間虛擬地址存在的意義。

其實,它只是內核空間中我們為物理頁找的訪問入口而已,它既沒有一直存在的必要,也不會有后續使用的價值。一旦數據傳輸完畢,這個入口也就失去了意義。

既然如此,我們何不采用一種更加動態的方式,在每次傳輸之前分配這個入口,傳輸完成后再釋放這個入口?

事實上新版本的Binder就是這么做的。

?

?

?

上圖右邊的文字展示了一次完整數據傳輸所經歷的過程。早期版本的Binder通過一次copy_from_user將數據整體拷貝完成,新版本的Binder則通過循環調用copy_from_user將數據一頁一頁的拷貝完成。以下是核心代碼差異的展示:

Android version ≤ P:

/drivers/staging/android/binder.c

1501	if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
1502			   tr->data.ptr.buffer, tr->data_size)) {
1503		binder_user_error("%d:%d got transaction with invalid data ptr\n",
1504				proc->pid, thread->pid);
1505		return_error = BR_FAILED_REPLY;
1506		goto err_copy_data_failed;
1507	}
1508	if (copy_from_user(offp, (const void __user *)(uintptr_t)
1509			   tr->data.ptr.offsets, tr->offsets_size)) {
1510		binder_user_error("%d:%d got transaction with invalid offsets ptr\n",
1511				proc->pid, thread->pid);
1512		return_error = BR_FAILED_REPLY;
1513		goto err_copy_data_failed;
1514	}
復制代碼

Android version ≥ Q:

/drivers/android/binder_alloc.c

1108 	while (bytes) {
1109 		unsigned long size;
1110 		unsigned long ret;
1111 		struct page *page;
1112 		pgoff_t pgoff;
1113 		void *kptr;
1114 
1115 		page = binder_alloc_get_page(alloc, buffer,
1116 					     buffer_offset, &pgoff);
1117 		size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
1118 		kptr = kmap(page) + pgoff;
1119 		ret = copy_from_user(kptr, from, size);
1120 		kunmap(page);
1121 		if (ret)
1122 			return bytes - size + ret;
1123 		bytes -= size;
1124 		from += size;
1125 		buffer_offset += size;
1126 	}
復制代碼

可以看到在新版本的實現中,每拷貝一頁的內容就調用一次kunmap將分配的內核空間虛擬地址釋放掉。這樣就再也不會發生長時間占用內核空間虛擬地址的情況。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/185519.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息