韋東山:Linux驅動程序基石之mmap

應用程序和驅動程序之間傳遞數據時,可以通過read、write函數進行。這涉及在用戶態buffer和內核態buffer之間傳數據,如下圖所示:
在這裏插入圖片描述
應用程序不能直接讀寫驅動程序中的buffer,需要在用戶態buffer和內核態buffer之間進行一次數據拷貝。這種方式在數據量比較小時沒什麼問題;但是數據量比較大時效率就太低了。比如更新LCD顯示時,如果每次都讓APP傳遞一幀數據給內核,假設LCD採用102460032bpp的格式,一幀數據就有102460032/8=2.3MB左右,這無法忍受。

改進的方法就是讓程序可以直接讀寫驅動程序中的buffer,這可以通過mmap實現(memory map),把內核的buffer映射到用戶態,讓APP在用戶態直接讀寫。

1.內存映射現象與數據結構
假設有這樣的程序,名爲test.c:

#include <stdio.h>
#include <unistd.h>

int a;

int main(int argc, char **argv)
{
	printf("enter a's value: \n");
	scanf("%d", &a);
	printf("a's address = 0x%x, a's value = %d\n", &a, a);
	while (1)
	{
		sleep(10);
	}
	return 0;
}

在ubuntu上如下編譯:

gcc  -o test  test.c -static

在2個終端中分別執行test程序,在第3個終端執行ps -a,可以看到這2個程序同時存在,如下圖:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

觀察到這些現象:

① 2個程序同時運行,它們的變量a的地址都是一樣的:0x6d73c0;

② 2個程序同時運行,它們的變量a的值是不一樣的,一個是12,另一個是123。

疑問來了:

① 這2個程序同時在內存中運行,它們在內存中的地址肯定不同,比如變量a的地址肯定不同;

② 但是打印出來的變量a的地址卻是一樣的。

怎麼回事?

這裏要引入虛擬地址的概念:CPU發出的地址是虛擬地址,它經過MMU(Memory Manage Unit,內存管理單元)映射到物理地址上,對於不同進程的同一個虛擬地址,MMU會把它們映射到不同的物理地址。如下圖:

在這裏插入圖片描述

當前運行的是app1時,MMU會把CPU發出的虛擬地址addr映射爲物理地址paddr1,用paddr1去訪問內存。

當前運行的是app2時,MMU會把CPU發出的虛擬地址addr映射爲物理地址paddr2,用paddr2去訪問內存。

MMU負責把虛擬地址映射爲物理地址,虛擬地址映射到哪個物理地址去?映射關係保存在頁表中:

在這裏插入圖片描述

解析如下:

① 每個APP在內核中都有一個task_struct結構體,它用來描述一個進程;

② 每個APP都要佔據內存,在task_struct中用mm_struct來管理進程佔用的內存;

內存在虛擬地址、物理地址,mm_struct中用mmap來描述虛擬地址,用pgd來描述對應的物理地址。

注意:pgd,Page Global Directory,頁目錄。

③ 每個APP都有一系列的VMA:virtual memory

比如APP含有代碼段、數據段、BSS段、棧等等,還有共享庫。這些單元會保存在內存裏,它們的地址空間不同,權限不同(代碼段是隻讀的可運行的、數據段可讀可寫),內核用一系列的vm_area_struct來描述它們。

vm_area_struct中的vm_start、vm_end是虛擬地址。

④ vm_area_struct中虛擬地址如何映射到物理地址去?

每一個APP的虛擬地址可能相同,物理地址不相同,這些對應關係保存在pgd中。

2.ARM架構內存映射簡介
ARM架構支持一級頁表映射,也就是說MMU根據CPU發來的虛擬地址可以找到第1個頁表,從第1個頁表裏就可以知道這個虛擬地址對應的物理地址。一級頁表裏地址映射的最小單位是1M。

ARM架構還支持二級頁表映射,也就是說MMU根據CPU發來的虛擬地址先找到第1個頁表,從第1個頁表裏就可以知道第2級頁表在哪裏;再取出第2級頁表,從第2個頁表裏才能確定這個虛擬地址對應的物理地址。二級頁表地址旺射的最小單位有4K、1K,Linux使用4K。

一級頁表項裏的內容,決定了它是指向一塊物理內存,還是指問二級頁表,如下圖:

在這裏插入圖片描述

2.1, 一級頁表映射過程
一線頁表中每一個表項用來設置1M的空間,對於32位的系統,虛擬地址空間有4G,4G/1M=4096。所以一級頁表要映射整個4G空間的話,需要4096個頁表項。

第0個頁表項用來表示虛擬地址第0個1M(虛擬地址爲0~0x1FFFFF)對應哪一塊物理內存,並且有一些權限設置;

第1個頁表項用來表示虛擬地址第1個1M(虛擬地址爲0x100000~0x2FFFFF)對應哪一塊物理內存,並且有一些權限設置;

依次類推。

使用一級頁表時,先在內存裏設置好各個頁表項,然後把頁表基地址告訴MMU,就可以加動MMU了。

以下圖爲例介紹地址映射過程:

① CPU發出虛擬地址vaddr,假設爲0x12345678

② MMU根據vaddr[31:20]找到一級頁表項:

虛擬地址0x12345678是虛擬地址空間裏第0x123個1M,所以找到頁表裏第0x123項,根據此項內容知道它是一個段頁表項。

段內偏移是0x45678。

③ 從這個表項裏取出物理基地址:Section Base Address,假設是0x81000000

④ 物理基地址加上段內偏移得到:0x81045678

所以CPU要訪問虛擬地址0x12345678時,實際上訪問的是0x81045678的物理地址
在這裏插入圖片描述

2.2, 二級頁表映射過程

首先設置好一級頁表、二級頁表,並且把一級頁表的首地址告訴MMU。

以下圖爲例介紹地址映射過程:

① CPU發出虛擬地址vaddr,假設爲0x12345678

② MMU根據vaddr[31:20]找到一級頁表項:

虛擬地址0x12345678是虛擬地址空間裏第0x123個1M,所以找到頁表裏第0x123項。根據此項內容知道它是一個二級頁表項。

③ 從這個表項裏取出地址,假設是address,這表示的是二級頁表項的物理地址;

④ vaddr[19:12]表示的是二級頁表項中的索引index即0x45,在二級頁表項中找到第0x45項;

⑤ 二級頁表項中含有頁基地址page base addr,假設是0x81889000:

它跟vaddr[11:0]組合得到物理地址:0x81889000 + 0x678 = 0x81889678。

所以CPU要訪問虛擬地址0x12345678時,實際上訪問的是0x81889678的物理地址
在這裏插入圖片描述

3, 怎麼給APP新建一塊內存映射
3.1, mmap調用過程

從上面內存映射的過程可以知道,要給APP端新開劈一塊虛擬內存,並且讓它指向某塊內核buffer,我們要做這些事:

① 得到一個vm_area_struct,它表示APP的一塊虛擬內存空間;

很幸運,APP調用mmap系統函數時,內核就幫我們構造了一個vm_area_stuct結構體。裏面含有虛擬地址的地址範圍、權限。

② 確定物理地址:

你想映射某個內核buffer,你需要得到它的物理地址,這得由你提供。

③ 給vm_area_struct和物理地址建立映射關係:

也很幸運,內核提供有相關函數。

APP裏調用mmap時,導致的內核相關函數調用過程如下:

在這裏插入圖片描述

3.2 cache和buffer
本小節參考:

ARM的cache和寫緩衝器(write buffer)
https://blog.csdn.net/gameit/article/details/13169445

使用mmap時,需要有cache、buffer的知識。下圖是CPU和內存之間的關係,有cache、buffer(寫緩衝器)。Cache是一塊高速內存;寫緩衝器相當於一個FIFO,可以把多個寫操作集合起來一次寫入內存。

在這裏插入圖片描述

程序運行時有“局部性原理”,這又分爲時間局部性、空間局部性。

① 時間局部性:

在某個時間點訪問了存儲器的特定位置,很可能在一小段時間裏,會反覆地訪問這個位置。

② 空間局部性:

訪問了存儲器的特定位置,很可能在不久的將來訪問它附近的位置。

而CPU的速度非常快,內存的速度相對來說很慢。CPU要讀寫比較慢的內存時,怎樣可以加快速度?根據“局部性原理”,可以引入cache。

① 讀取內存addr處的數據時:

先看看cache中有沒有addr的數據,如果有就直接從cache裏返回數據:這被稱爲cache命中。

如果cache中沒有addr的數據,則從內存裏把數據讀入,注意:它不是僅僅讀入一個數據,而是讀入一行數據(cache line)。

而CPU很可能會再次用到這個addr的數據,或是會用到它附近的數據,這時就可以快速地從cache中獲得數據。

② 寫數據:

CPU要寫數據時,可以直接寫內存,這很慢;也可以先把數據寫入cache,這很快。

但是cache中的數據終究是要寫入內存的啊,這有2種寫策略:

a. 寫通(write through):

數據要同時寫入cache和內存,所以cache和內存中的數據保持一致,但是它的效率很低。能改進嗎?可以!使用“寫緩衝器”:cache大哥,你把數據給我就可以了,我來慢慢寫,保證幫你寫完。

有些寫緩衝器有“寫合併”的功能,比如CPU執行了4條寫指令:寫第0、1、2、3個字節,每次寫1字節;寫緩衝器會把這4個寫操作合併成一個寫操作:寫word。對於內存來說,這沒什麼差別,但是對於硬件寄存器,這就有可能導致問題。

所以對於寄存器操作,不會啓動buffer功能;對於內存操作,比如LCD的顯存,可以啓用buffer功能。

b. 寫回(write back):

新數據只是寫入cache,不會立刻寫入內存,cache和內存中的數據並不一致。

新數據寫入cache時,這一行cache被標爲“髒”(dirty);當cache不夠用時,才需要把髒的數據寫入內存。

使用寫回功能,可以大幅提高效率。但是要注意cache和內存中的數據很可能不一致。這在很多時間要小心處理:比如CPU產生了新數據,DMA把數據從內存搬到網卡,這時候就要CPU執行命令先把新數據從cache刷到內存。反過來也是一樣的,DMA從網卡得過了新數據存在內存裏,CPU讀數據之前先把cache中的數據丟棄。

是否使用cache、是否使用buffer,就有4種組合(Linux內核文件arch\arm\include\asm\pgtable-2level.h):
在這裏插入圖片描述

第1種是不使用cache也不使用buffer,讀寫時都直達硬件,這適合寄存器的讀寫。

第2種是不使用cache但是使用buffer,寫數據時會用buffer進行優化,可能會有“寫合併”,這適合顯存的操作。因爲對顯存很少有讀操作,基本都是寫操作,而寫操作即使被“合併”也沒有關係。

第3種是使用cache不使用buffer,就是“write through”,適用於只讀設備:在讀數據時用cache加速,基本不需要寫。

第4種是既使用cache又使用buffer,適合一般的內存讀寫。

3.3, 驅動程序要做的事
驅動程序要做的事情有3點:

① 確定物理地址

② 確定屬性:是否使用cache、buffer

③ 建立映射關係

參考Linux源文件,示例代碼如下:
在這裏插入圖片描述

還有一個更簡單的函數:
在這裏插入圖片描述

4,驅動編程

我們在驅動程序中申請一個8K的buffer,讓APP通過mmap能直接訪問。

① 使用哪一個函數分配內存?

我們應該使用kmalloc或kzalloc,這樣得到的內存物理地址是連續的,在mmap時後APP纔可以使用同一個基地址去訪問這塊內存。(如果物理地址不連續,就要執行多次mmap了)。

關鍵代碼現場編寫,再完善文檔。

可以加羣與韋東山老師交流:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章