內存頁
MMU :內存管理單元,將虛擬內存轉化為物理地址的硬件。
- 因為 MMU 通常以頁為單位處理內存,所以從虛擬內存的角度來說,內核中頁是最小的單位。
- 一般 32 位系統使用4k頁,64 位系統使用8k頁。
- 內核使用 struct page 結構體存放物理頁。page 與物理頁相關,而非虛擬頁,當內存頁被swap后,可能不再和同一個page相關聯。
- 可以通過page_address(page) 函數獲取物理頁page 對應的邏輯地址。
page 的內核代碼
defined in <linux/mm_types.h>
struct page {
unsigned long flags; //內存頁狀態,定義在<linux/page-flags.h>.
atomic_t _count; //引用計數
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; //在虛擬內存中的地址
};
ZONE
由於硬件的原因,內核對於內存中不同物理地址的內存並不一視同仁。由於這種限制,內存將內存頁划分了區(zone)。ZONE的划分是為了管理頁的一種邏輯分組。內存分配不能同時在兩個zone 分配。
有些硬件存在下面兩種缺陷引起的內存尋址問題,所以需要將內存分區。
- 一些硬件只能用特定的內存地址來執行DMA(直接內存訪問)。
- 一些體系結構的內存物理尋址范圍大於虛擬尋址范圍,導致一些內存不能映射到內核空間。
linux 主要有下面 4 種 ZONE
- ZONE_DMA—This zone contains pages that can undergo DMA.
- ZONE_DMA32—Like ZOME_DMA, this zone contains pages that can undergo DMA. Unlike ZONE_DMA, these pages are accessible only by 32-bit devices. On some architectures, this zone is a larger subset of memory.
- ZONE_NORMAL—This zone contains normal, regularly mapped, pages.
- ZONE_HIGHMEM—This zone contains “high memory,” which are pages not permanently mapped into the kernel’s address space.
例如 X86-32 架構,ISA 設備只能訪問物理內存的前16M,高於896M的內存不能直接映射。剩下的就是NORMAL區。如果體系結構沒有限制,那么全部都是NORMAL區。
| Zone | Description | Physical Memory |
|---|---|---|
| ZONE_DMA | DMA-able | pages < 16MB |
| ZONE_NORMAL | Normally | addressable pages 16–896MB |
| ZONE_HIGHMEM | Dynamically | mapped pages > 896MB |
zone 的水線:每一個zone 都有自己的最小值,最低值,最高值三個水線,使用水線設置合適的內存消耗基准,水線隨着空暇內存變化
zone 的內核代碼
defined in <linux/mmzone.h>
struct zone {
unsigned long watermark[NR_WMARK]; //持有該區的最小最低最高的水位值。
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
spinlock_t lock; //自旋鎖
struct free_area free_area[MAX_ORDER]
spinlock_t lru_lock;
struct zone_lru {
struct list_head list;
unsigned long nr_saved_scan;
}lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long pages_scanned;
unsigned long flags;
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
unsigned int inactive_ratio;
wait_queue_head_t *wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
};
獲取頁
內核提供了一些請求內存和釋放內存的底層接口,
| 請求內存函數 | 描述 |
|---|---|
| alloc_page(gfp_mask) | Allocates a single page and returns a pointer to its |
| alloc_pages(gfp_mask, order) | Allocates 2order pages and returns a pointer to the first page’s page structure |
| __get_free_page(gfp_mask) | Allocates a single page and returns a pointer to its logical address |
| __get_free_pages(gfp_mask, order) | Allocates 2order pages and returns a pointer to the first page’s logical address |
| get_zeroed_page(gfp_mask) | Allocates a single page, zero its contents and returns a pointer to its logical address |
釋放內存接口:
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
kmalloc()
和上面獲取頁的接口不同,kmalloc() 主要用於申請字節為單位的內存。kmalloc() 返回一個指向內存塊的指針,至少是 size 大小,分配的內存區在物理上是連續的。
void * kmalloc(size_t size, gfp_t flags)
gfp_mask 標志
- 行為修飾符:分配內存時的動作
- 區修飾符:從哪個 zone 分配內存
- 類型:組合上面兩個
- GFP_ATOMIC 分配內存是不能睡眠,在內存緊缺時容易失敗。
- GFP_KERNEL 可以睡眠,用於安全調度的進程上下文中,成功率高。
| Situation | Solution |
|---|---|
| Process context, can sleep | Use GFP_KERNEL. |
| Process context, cannot sleep | Use GFP_ATOMIC, or perform your allocations with GFP_KERNEL at an earlier or later point when you can sleep |
| Interrupt handler | Use GFP_ATOMIC. |
| Softirq | Use GFP_ATOMIC. |
| Tasklet | Use GFP_ATOMIC. |
| Need DMA-able memory, can | Use (GFP_DMA |
| Need DMA-able memory, cannot | Use (GFP_DMA |
kfree()
kfree() 釋放由kmalloc()申請的內存。
void kfree(const void *ptr)
//例子
char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC); if (!buf)
/* error allocating memory ! */
kfree(buf);
vmalloc()
類似kmalloc,但是物理內存地址可以不連續,虛擬內存地址是連續的。可以睡眠。
一般只有硬件要求得到物理地址連續的內存。軟件可以使用只有虛擬地址連續的內存。很多內核代碼雖然不需要連續的物理內存,但還是使用kmalloc,因為性能好,不需要做邏輯映射。
//declared in <linux/vmalloc.h>
void * vmalloc(unsigned long size)
void vfree(const void *addr)
//例子
char *buf;
buf = vmalloc(16 * PAGE_SIZE); /* get 16 pages */ if (!buf)
vfree(buf);
Slab 層
有很多對象存放在鏈表結構中,在不用的時候空閑鏈表也已經占用內存。內核不能不能控制這些空閑鏈表的回收,尤其是在內存緊缺時。所以引入了 slab 分配器。slab 扮演了一個通用的數據結構緩存角色。
slab 把不同類型的對象放到不同的 caches 中。 一個 slab 由一個或者多個物理上的連續頁組成,一般情況只有一個頁,每一個cache 有多個 slab。

一個 slab 有三個狀態:full, partial, or empty. 先從 partial 開始填充。
cache 用 kmem_cache 結構表示 包含三個鏈表 slabs_full,slabs_partial,slabs_empty,鏈表中包含所有的 slab。
struct slab {
struct list_head list; /* full, partial, or empty list */
unsigned long colouroff; /* offset for the slab coloring */
void *s_mem; /* first object in the slab */
unsigned int inuse; /* allocated objects in the slab */
kmem_bufctl_t free; /* first free object, if any */
};
slab 的創建:
通過 *kmem_getpages()中調用的 _get_free_pages()函數分配內存頁。
slab 是在 cache 的基礎之上,提供給內核一個簡單的接口,通過接口來對 cache 進行分配和撤銷。slab 起一個分配器的作用,可以為具體的 object 分配內存。
//cache 的創建(slab 分配器的接口):
//返回一個指向 cache 的指針。align 是 slab 第一個對象的偏移量,用於內存對齊
struct kmem_cache * kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags, void (*ctor)(void *));
//撤銷 cache
int kmem_cache_destroy(struct kmem_cache *cachep)
//創建 cache 后,獲取對象,沒有空閑 slab 的話,通過上面的*kmem_getpages()獲取新的頁。
void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
task_struct 對象的 slab 和 cache 創建例子
//1.首先創建一個全局變量存放 task_struct 的 cache
struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create(“task_struct”, sizeof(struct task_struct),ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL);
//2.進程調用 fork()時,會創建新的process descriptor:
struct task_struct *tsk;
tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
if (!tsk)
return NULL;
//3.process descriptor 被撤銷
kmem_cache_free(task_struct_cachep, tsk);
//4.task_struct_cachep cache 是不會被撤銷的,因為內核經常要用,非要撤銷的話:
int err;
err = kmem_cache_destroy(task_struct_cachep); if (err)
/* error destroying cache */
其他
Stack 上內存的靜態分配
32位 和 64位 頁的大小為4K 和 8K,一般進程有兩頁的內核棧,也可以設置單頁內核棧。
隨着運行時間的增加,物理內存碎片增加,分配連續的頁越來越難。當單頁棧設置后,中斷程序不再和進程放在同一個棧內,有自己的中斷棧。
棧的溢出會覆蓋緊鄰堆棧末端的內容,溢出后 down 機還好,否則會破壞數據。
高端內存的映射
高端內存的永久映射數量是有限的,不需要時需要解除。通過函數kmap進行映射,可以睡眠。kmap_atomic提供了原子性的臨時映射。不會被阻塞,禁止內核搶斷。
per CPU 新接口
對於 smp 系統,多個 cpu 可以有自己才能訪問的數據,這樣不需要鎖,只需要注意內核搶占的問題即可。
void *percpu_ptr;
unsigned long *foo;
percpu_ptr = alloc_percpu(unsigned long); //為 cpu 動態分配內存,類似 kmalloc()
if (!ptr)
/* error allocating memory .. */
foo = get_cpu_var(percpu_ptr); //獲取當前 CPU 上的指定數據,會禁止內核搶斷
/* manipulate foo .. */
put_cpu_var(percpu_ptr); //開啟內核搶斷
分配內存函數的選擇
| 需求 | 函數 | 特點 |
|---|---|---|
| 連續的物理頁 | kmalloc() | 可以通過 flag 決定是否可以睡眠, |
| 高端內存 | alloc_pages() | 返回一個 page 的指針,而不是邏輯地址,因為可能沒有映射 |
| 獲取真正的指針 | kmap() | 會把高端內存映射到邏輯地址 |
| 不需要內存連續的地址 | vmalloc() | 需要映射,有性能損失 |
| 創建和撤銷數據結構 | slab | 使用 cache 動態分配 |
