close
八、記憶體
在 Kernel 內管理記憶體的方式與一般應用程式不同,所以驅動程式需要取用記憶體時,必須要知道 kernel 管理記憶體的方式及其特性。
本章會解說 kernel 記憶體的用法,以及驅動程式經常用到的 DMA 功能。



8-1、Linux kernel 的記憶體管理
最近的 OS 都以「paging」的方式來管理記憶體, Linux 也是其中之一。
Paging 是利用 CPU 與 MMU(Memory Management Unit) 的硬體機制實現的。
Paging 可以讓作業系統實現下列功能:
  • Virtual Memory (虛擬記憶體)
  • Swap (置換檔)
  • Demand Paging (按需求分頁;用到時才載入資料)
  • Copy on Write (只有資料有更動時才實際複製)
  • Shared Memory (共享資料)
  • mmap (將磁碟內容映射到記憶體內)

特別是 virtual memory 可以讓 kernel 與一般應用程式在虛擬位址空間內執行。
虛擬位址轉成物理位址(實體位址) 的工作由硬體完成,轉換速度夠快。

Paging 指的是把物理記憶體切割成長度固定的「page」,並以此作為管理單位。
一個 page 的大小隨著硬體架構而不同, IA-32 的 page size 是 4 KB。
物理記憶體固定以 page size 當成管理單位,也就是說,就算只要求分配 1 byte 記憶體,還是會分配到最小單位,也就是 1 page。
系統啟動時,OS 會從 BIOS 得知系統安裝了多少物理記憶體。
OS 以 page 為管理物理記憶體的單位,實作了「buddy system(buddy allocator)」。
而 Buddy system 當然就是以 page size 為單位管理物理記憶體,以 2 的冪次方控制單位數量。
例如,當 OS 內的 module 要求 128 bytes 記憶體時,會回傳 1 page 的記憶體,如果要求 4097 bytes 記憶體時,就會回傳 2 pages 記憶體,因為是以 page 當成管理單位,所以有著出現無謂浪費的缺點。
在分配 2 pages 以上的記憶體時,得到的物理記憶體空間會是連續的,適合拿來進行 DMA 傳送。

Buddy system 的空間使用狀態可以透過以下指令查詢:
cat /proc/buddyinfo
Buddy system 因為只能以 page size 當成單位,所以在驅動程式需備記憶體時,是個不怎麼好用的介面,因此在 buddy system 上層,還準備了「Slab allocator」介面。
slab allocator 是以稱為「cache」的單位來管理記憶體,slab allocator 是比較容易使用的介面,驅動程式常用的 kmalloc() 及 kfree() 就是它所提供的。



8-2、Stack
Stack 是存放暫時性資料的記憶體空間,這邊會說明 kernel 的 stack 管理機制。

8-2.1、Kernel Stack
一般應用程式使用區域變數時,會在 user space 的 stack 範單取用記憶體,而驅動程式也是一樣,使用區域變數、管理返回位址的時候,就需要用到 kernel 內的 stack 範圍。
kernel stack 是透過 thread_info 結構管理的,緊鄰分配於 thread_info 結構(include/linux/sched.h):
union thread_union {
struct thread_info thread_info
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

THREAD_SIZE 是 thread_info 結構與 stack 合在一起的大小,在 IA-32 環境下預設是「8KB」。
kernel stack 是從記憶體的高位址側往低位址側使用的,kernel 內部如果發生 stack overflow 的話,就會破壞 process 結構。
Stack 大小設為 8KB 乍看之下或許很小,但其實已經十分足夠,一般來說 kernel 之內的程式碼,為了抑制記憶體使用量,會極力避免使用 stack 才對。

user process 呼叫 system call,讓驅動程式在 user context 運作的時候,就會使用 user process 定義的 kernel stack。
那 interrupt context 又是怎樣的情況呢?發生中斷時,驅動程式所在的 interrupt context 沒有對應的 user process,這時中斷處理程序會拿發生中斷時「正在執行」的 user process 擁有的 kernel stack 來用。
呼叫函式會使用 kernel stack 空間,因此了函式都是 reentrant(可重進入) 的。

8-2.2、4K Stacks
8KB 的 kernel stack 有個問題:
  • 8192 bytes 的大小無法放入 1 page

所以 kernel 配直選項能讓它變成 4096 bytes,剛好能放進一個 page,稱為「4K Stacks」機制:
  • Kernel hacking -> Use 4Kb for kernel stacks instead of 8Kb

在開發驅動程式時,為了避免因為 stack overflow 而造成 kernel panic,必須特別注意:
  • 盡量不要使用區域變數
  • 不要套疊呼叫太多層函式



8-3、全域變數
驅動程式可以一如往常地宣告、使用全域變數,全域變數放在 RAM 之內,只要不加上 const 就可自由讀寫,需注意的有:
  • 全域變數會在整個驅動程式之內共享
  • 對編譯成 module 的驅動程式而言,其它 module 看不到它的全域變數;但可用 EXPORT_SYMBOL 對外公開
  • 對靜態連結到 kernel 的驅動程式而言,全域變數是整個 kernel 都看得到的

以下面這個驅動程式為例,假設它是 kernel module 類型的驅動程式:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

int g_sample_counter __attribute__ ((unused));
const int g_sample_counter2 __attribute__ ((unused));
static int g_sample_counter3 __attribute__ ((unused));
int g_sample_counter4 __attribute__ ((unused));
EXPORT_SYMBOL (g_sample_counter4);

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed.\n");
return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed\n");
}

module_init(sample_init);
module_exit(sample_exit);

定義了四個全域變數,接著來看外部 module 會看到哪些東西。
因為這些個變數都沒在程式碼內用到,為了不讓 gcc 自動把它們刪除,所以加上「unused」屬性。

Makefile 如下,因為希望建立 link map,所以在 LDFLAGS 環境變數加上 「-Map」選項:
CFLAGS += -Wall
LDFLAGS += -Map $(PWD)/sample.map
CFILES := main.c

obj-m += sample.o
sample-objs := $(CFILES:.c=.o)

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


建構驅動程式後,會產生映射檔(sample.map)。
因為 g_sample_counter3 變數宣告為 static,所以命名範圍限定在 main.c 檔案內,因此配置在「BSS(Block Started by Symbol」區段內的變數只有三個:
cat sample.map | grep g_sample_counter
建構時產生的「Module.symvers」包含被 EXPORT_SYMBOL 的符號清單,這些符號可被其它 module 取用:
cat Module.symvers | grep g_sample_counter
最後把驅動程式掛進 kernel,再看看 kernel 的符號表:
cat /proc/kallsyms | grep sample

全域變數是一個驅動程式的 module 內共用的,如果在許多不同的 context 之內存取的話,就必須作好鎖定才行。



8-4、動態取得記憶體
linux kernel 提供了許多種不同的記憶體管理機制,所以在取用記憶體時,要先弄清楚它們之間的差異。

8-4.1、kmalloc
取用記憶體時,驅動程式最常用的方式應該是 kmalloc(),定義在 linux/slab.h 內,但不必特地 include。
void *kmalloc(size_t size, gfp_t flags);
kmalloc() 會取得 size bytes 的 kernel 記憶體,適合用來取得 128 KB 左右的小 buffer。
取用記憶體失敗時,會傳回 NULL 指標,所以一定要檢查傳回值。
記憶體用完後,一定要呼叫 kfree() 釋放,否則會在 kernel 內引發 memory leak,驅動程式的 memory leak 並不會在卸除驅動程式後由 kernel 代為釋放。
void *kfree(const void *block);

使用 kmalloc() 的範例:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

void *memptr;

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed.\n");
memptr = ptr = kmalloc(2, GFP_KERNEL):
printk("ptr %p\n", ptr);
return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed\n");
kfree(memptr);
}

module_init(sample_init);
module_exit(sample_exit);


8-4.2、直接從 Cache 取用記憶體
kmalloc() 與 kfree() 都是以「slab allocator」這個 cache 機制實作出來的,這邊的「cache」指的是「一塊記憶體」的意思。
slab allocator 的下層是之前提過的 buddy system 記憶體管理機制,slab allocator 利用 buddy system 提供更靈活的記憶體管理功能。

8-4.3、直接從 Cache 取用記憶體
若要直接從 cache 取得記憶體,首先要以 kmem_cache_create() 建立最初的 cache 容器:
struck kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *, struct kmem_cache *, unsigned long));
引數 name 指的是在 /proc/slabinfo 或 /sys/slab 中讓其它人看到的名稱。
引數 size 指的是想取用的記憶體量(bytes)。
其它的引數不常用到,填 0 或 NULL 都無所謂。
呼叫 kmem_cache_create() 後,並不會實際配置記憶體,這個 cache 對 OS 來說還是「空」的,若呼叫失敗時,則會傳回 NULL值。
不用要用到後,需須叫 kmem_cache_destroy() 將之釋放:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

如果要在 cache 內配置記憶體,可以呼叫 kmem_cache_alloc():
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
函式呼叫成功的話,代表成功配置到 kmem_cache_create() 要求的記憶體空間,並傳回指向這塊記憶體開頭的指標,若呼叫失敗時,則會傳回 NULL 。
要釋放記憶體,可以呼叫 kmem_cache_free() :
void kmem_cache_free(struct kmem_cache *cachep, void *disp);

使用 kmem_cache_alloc() 的範例:


8-4.4、Buddy System
Linux 記憶體管理的最低層是「buddy system」,這是 slab allocator 的下層。
驅動程式在需要取用剛好等於 page size 的記憶體時,可以直接使用這個介面。
唯 buddy system 只能以 page size 為單位管理記憶體,並以 order 作為 page 的計量單位,取得 2 的 order 次方個 page。
要從 buddy system 取用記憶體時,是呼叫 __get_free_pages() :
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
order 引數是要取用的數量,若 order 為 3,則會取用 2 的 3 次方,也就是 8 pages 的記憶體,page size 隨系統架構而不同。
函式呼叫成功的話,會傳回記憶體指標,傳回值的型別是 unsigned long,必須明確轉成「void *」或「char *」之類的型別。
釋放記憶體時,則是呼叫 free_pages() :
void free_pages(unsigned long addr, unsigned int order);

使用 __get_free_pages() 的範例:


8-4.5、vmalloc
slab allocator 保證取到的記憶體範圍是連續的物理記憶體,可以用來進行 DMA 傳送工作。
但在某些情況下,並不需要連續的物理記憶體,只要 kernel 虛擬記憶體空間內的位址連續就好,此時可以使用 vmalloc() 及 vfree() 函式:
void *vmalloc(unsigned long size);
void vfree(void *addr)




8-5、檢查 Slab Allocator 的洩漏情形




8-6、kmalloc 檢查偵測 patch




8-7、DMA
驅動程式在裝置與記憶體之間轉移資料時,經常用到 DMA(Direct Memory Access) 的機制,它代表:
  • 在裝置與物理記憶體之間傳送資料時,不必 CPU 插手
的機制。
也就是說,只要用了 DMA, CPU 就可以去做其它的事,可以提昇系統效能。

8-7.1、DMA Buffer 的限制
因為裝置需要直接讀寫系統記憶體,所以一般來說 DMA Buffer 的限制有:
  • DMA buffer 的記憶體範圍必須是連續的
  • DMA buffer 的物理開頭位址必須位於 page 的邊界起點
  • DMA buffer 的記憶體空間必須在開頭 4GB 之內
  • DMA buffer 的記憶體不能被 swap


8-7.2、取得 DMA Buffer 的 Kernel 函式




8-8、快取一致性
執行 DMA 傳送工作時,有時會遇到「cache coherency(快取一致性)」的問題。
一般來說,電腦架構為了提高記憶體讀寫效率,會利用 CPU 內的 L1/L2 cache,所以驅動程式對記憶體寫入資料時,不見得會立刻反應到記憶體內容上,而是會放在 CUP 的 cache 裡,等到適當時機才寫入記憶體。
相同的情況下,由裝置寫入資料到記憶體時,CPU 的 cache 也未必能及時更新,而可能導致驅動程式讀取 cache 時,讀到舊的資料。

linux kernel 為了應用這種問題,提供了控制 cache 的函式:
void dma_cache_sync(struct device *dev, void *vaddr, size_t size, enum dma_data_direction direction);
引數 vaddr 是 DMA buffer 的指標(kernel 虛擬位址)。
引數 size 是想 flush/purge 的 DMA buffer 大小。
dma_data_direction 是列舉常數,指定 DMA_TO_DEVICE 代表「要把資料從記憶體傳給裝置」,也就是執行 flush cache 的動作。
指定 DMA_FROM_DEVICE 代表「要讀取裝置傳到記憶體的資料」,也就是執行 purge cache 的動作。

IA-32 在硬體層及保証 cache coherency,所以驅動程式不必特別控制 cache,但若要移植到其它平台時,則需特別注意。



8-9、結語
開發驅動程式要應對各種不同的記憶體管理機制,依用途選最適合的方式。
而 DMA buffer 有諸多限制,開發時需注意。
arrow
arrow
    全站熱搜

    silverfoxkkk 發表在 痞客邦 留言(0) 人氣()