說起Binder的內存拷貝,相信大多數人都聽過“一次拷貝”:相較于傳統IPC的兩次拷貝,Binder在數據傳輸時顯得效率更高。
其實不少人在面試時都能回答出上面這句話,但若是追問他更多細節,估計又啞口無言了。
cache和主存的映射方式。其實內存拷貝的概念既簡單又復雜。簡單是因為它功能單一,而復雜則在于不少人對于虛擬內存,物理內存,用戶空間,內核空間的認識并不充分。所謂地基不穩,高樓難立。
本文嘗試揭示Binder內存拷貝的本質,另外還會介紹新版本中相應實現的一些改動。
在做任何一件事之前,先明確目的。我相信Binder的開發者在最初設計時也一定仔細考慮過這個問題。根據我的理解,Binder數據傳輸的目的可以概括成這句話:
手機主板壞了 數據拷貝。一個進程可以通過自己用戶空間的虛擬地址訪問另一個進程的數據。
要想充分理解這句話,需要在基礎知識上達成一些共識。
所有的數據都存儲在物理內存中,而進程訪問內存只能通過虛擬地址。因此,若是想成功訪問必須得有個前提:
電腦的硬盤和內存有什么區別、虛擬地址和物理內存之間建立映射關系
若是這層映射關系不建立,則訪問會出錯。信號11(SIGSEGV)的MAPERR就是專門用來描述這種錯誤的。
虛擬地址和物理地址間建立映射關系通過mmap完成。這里我們不考慮file-back的mapping,只考慮anonymous mapping。當mmap被調用(flag=MAP_ANONYMOUS)時,實際上會做以下兩件事:
蘋果拷貝什么意思。mmap做完這兩件事后,就會返回連續虛擬地址空間的起始地址。在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原則的。這一點,極大地保證了物理資源的合理分配和使用。
快速內存拷貝?先說結論,不同進程間的用戶空間是完全隔離的,內核空間是共享的。
那么“隔離”和“共享”在這個語境下又是什么意思呢?
從實現角度而言,“隔離”的意思是不同進程的頁表不同,“共享”的意思是不同進程的頁表相同,僅此而已。我們知道,頁表反映的是虛擬地址和物理地址的映射關系。那么一張頁表應該管理哪些虛擬地址呢?是整個地址空間的所有虛擬地址么?
當然不是。Linux將虛擬地址空間分為了用戶空間和內核空間,因此管理不同空間虛擬地址的頁表也不一樣。
如上圖所示,A進程的用戶空間使用頁表1,B進程的用戶空間使用頁表2,而A/B進程的內核空間都使用頁表3。A/B中使用相同的用戶空間虛擬地址來訪問內存,由于頁表不同,因此最終映射的物理頁也不同,這就是所謂的“進程隔離”。而由于A/B進程的內核空間使用了同一張頁表,所以只要他們使用相同的虛擬地址(位于內核空間),那么必然訪問到同一個物理頁。
1.3.1 共享內存
虛擬地址只是為了進行內存訪問封裝的一層接口,而數據總歸是存在物理內存上的。因此,若是想讓A進程通過(用戶空間)虛擬地址訪問到B進程中的數據,最高效的方式就是修改A/B進程中某些虛擬地址的PTE,使得這些虛擬地址映射到同一片物理區域。這樣就不存在任何拷貝,因此數據在物理空間中也只有一份。
1.3.2 內存拷貝
共享內存雖然高效,但由于物理內存只有一份,因此少不了考慮各種同步機制。讓不同進程考慮數據的同步問題,這對于Android而言是個挑戰。因為作為系統平臺,它必然希望降低開發者的開發難度,最好讓開發者只用管好自己的事情。因此,讓開發者頻繁地考慮跨進程數據同步問題不是一個好的選擇。
取而代之的是內存拷貝的方法。該方法可以保證不同進程都擁有一塊屬于自己的數據區域,該區域不用考慮進程間的數據同步問題。
由于不同進程的內核空間是共享的(只有共享才能完成傳輸,否則只能隔江相望了),因此很自然地考慮到將它作為數據中轉站。常規的做法需要兩次拷貝,一次是由發送進程的用戶空間拷貝到發送進程的內核空間,另一次是由接收進程的內核空間拷貝到接收進程的用戶空間。這兩次拷貝中間有一個隱含的轉換關系,即發送進程的內核空間和接收進程的內核空間是共享的,因此持有相同的虛擬地址就會訪問到同一片物理區域。
兩次拷貝的方法比較符合直覺,但在效率上還有可優化的空間。
既然兩次拷貝都發生在一個進程的用戶空間和內核空間之間,那么其實也就隱含了一個前提:
用戶空間和內核空間的虛擬地址指向不同的物理頁。
正是因為指向不同的物理頁,所以才需要拷貝。那有沒有可能讓二者指向同一個物理頁?如果可以,這樣不就節省了一次拷貝么?
事實上,Binder正是這樣做的。
為了減少一次拷貝,接收數據的進程必須同時滿足下面三個條件:
條件1、2在進程調用Binder的mmap函數時已經完成,而條件3則在每次數據通信時進行。
下面假設進程1發送數據,進程2接收數據,我們來分析下內存拷貝到底發生在何時(以下執行均發生在進程1中,只不過此時正在執行驅動代碼[陷入內核態])。
整個過程中,只有步驟4發生了一次數據拷貝。
從性能角度而言,早期版本的實現幾乎無可挑剔。但是它有一個致命的穩定性缺陷,這是Google工程師們無法忍受的。因此從Android Q開始,Binder內存拷貝的實現有了新的改動。
通過之前的分析可以知道,驅動的mmap函數執行完之后,該進程將會在內核空間分配一塊虛擬地址區域B。對Android應用進程而言,B的默認大小為1M-8K。只要這個進程沒有退出,這1M-8K的虛擬地址就會一直分配給它。
通常對于虛擬地址長時間的占用并不會產生問題,但不幸的是,Binder的這個占用確實產生了問題。
2.2.1 32位機器上Binder內存拷貝的缺陷
32位機器的尋址空間為4G,其中高位的1G用作內核地址空間,低位的3G用作用戶地址空間,這些都是虛擬地址的概念。
1G的內核地址空間又劃分為四塊不同的區域:
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將分配的內核空間虛擬地址釋放掉。這樣就再也不會發生長時間占用內核空間虛擬地址的情況。
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态