首頁>技術>

本文字數:4661,閱讀時長:8分鐘

應用程式和驅動程式之間傳遞資料時,可以通過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){\tprintf("enter a's value: \\n");\tscanf("%d", &a);\tprintf("a's address = 0x%x, a's value = %d\\n", &a, a);\twhile (1)\t{\t\tsleep(10);\t}\treturn 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含有程式碼段、資料段、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。

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

所以CPU要訪問虛擬地址0x12345678時,實際上訪問的是0x81045678的實體地址

2.2 二級頁表對映過程

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

以下圖為例介紹地址對映過程:

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

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

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

④ 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,你需要得到它的實體地址,這得由你提供。

也很幸運,核心提供有相關函式。

APP裡呼叫mmap時,導致的核心相關函式呼叫過程如下:

3.2 cache和buffer

使用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了)。

關鍵程式碼現場編寫,再完善文件。

可以與韋東山老師交流:

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 軟體開發平臺之爭:NET VS Java,誰是更好的選擇?