1 記憶體中不連續的頁的分配 根據上文的講述, 我們知道物理上連續的映射對內核是最好的, 但並不總能成功地使用. 在分配一大塊記憶體時, 可能竭盡全力也無法找到連續的記憶體塊. 在用戶空間中這不是問題,因為普通進程設計為使用處理器的分頁機制, 當然這會降低速度並占用TLB. 在內核中也可以使用同樣的技術. ...
1 記憶體中不連續的頁的分配
根據上文的講述, 我們知道物理上連續的映射對內核是最好的, 但並不總能成功地使用. 在分配一大塊記憶體時, 可能竭盡全力也無法找到連續的記憶體塊.
在用戶空間中這不是問題,因為普通進程設計為使用處理器的分頁機制, 當然這會降低速度並占用TLB.
在內核中也可以使用同樣的技術. 內核分配了其內核虛擬地址空間的一部分, 用於建立連續映射.
在IA-32系統中, 前16M劃分給DMA區域, 後面一直到第896M作為NORMAL直接映射區, 緊隨直接映射的前896MB物理記憶體,在插入的8MB安全隙之後, 是一個用於管理不連續記憶體的區域. 這一段具有線性地址空間的所有性質. 分配到其中的頁可能位於物理記憶體中的任何地方. 通過修改負責該區域的內核頁表, 即可做到這一點.
Persistent mappings和Fixmaps地址空間都比較小, 這裡我們忽略它們, 這樣只剩下直接地址映射和VMALLOC區, 這個劃分應該是平衡兩個需求的結果
儘量增加DMA和Normal區大小,也就是直接映射地址空間大小,當前主流平臺的記憶體,基本上都超過了512MB,很多都是標配1GB記憶體,因此註定有一部分記憶體無法進行線性映射。
- 保留一定數量的VMALLOC大小,這個值是應用平臺特定的,如果應用平臺某個驅動需要用vmalloc分配很大的地址空間,那麼最好通過在kernel參數中指定vmalloc大小的方法,預留較多的vmalloc地址空間。
並不是Highmem沒有或者越少越好,這個是我的個人理解,理由如下:高端記憶體就像個垃圾桶和緩衝區,防止來自用戶空間或者vmalloc的映射破壞Normal zone和DMA zone的連續性,使得它們碎片化。當這個垃圾桶較大時,那麼污染Normal 和DMA的機會自然就小了。
通過這種方式, 將內核的內核虛擬地址空間劃分為幾個不同的區域
下麵的圖是VMALLOC地址空間內部劃分情況
2 用vmalloc分配記憶體
vmalloc是一個介面函數, 內核代碼使用它來分配在虛擬記憶體中連續但在物理記憶體中不一定連續的記憶體
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L70
void *vmalloc(unsigned long size);
該函數只需要一個參數, 用於指定所需記憶體區的長度, 與此前討論的函數不同, 其長度單位不是頁而是位元組, 這在用戶空間程式設計中是很普遍的.
使用vmalloc的最著名的實例是內核對模塊的實現. 因為模塊可能在任何時候載入, 如果模塊數據比較多, 那麼無法保證有足夠的連續記憶體可用, 特別是在系統已經運行了比較長時間的情況下.
如果能夠用小塊記憶體拼接出足夠的記憶體, 那麼使用vmalloc可以規避該問題
內核中還有大約400處地方調用了vmalloc, 特別是在設備和聲音驅動程式中.
因為用於vmalloc的記憶體頁總是必須映射在內核地址空間中, 因此使用ZONE_HIGHMEM
記憶體域的頁要優於其他記憶體域. 這使得內核可以節省更寶貴的較低端記憶體域, 而又不會帶來額外的壞處. 因此, vmalloc
等映射函數是內核出於自身的目的(並非因為用戶空間應用程式)使用高端記憶體頁的少數情形之一.
所有有關vmalloc的數據結構和API結構聲明在include/linux/vmalloc.h
聲明頭文件 | NON-MMU實現 | MMU實現 |
---|---|---|
include/linux/vmalloc.h | mm/nommu.c | mm/vmalloc.c |
2.1 數據結構
內核在管理虛擬記憶體中的vmalloc
區域時, 內核必須跟蹤哪些子區域被使用、哪些是空閑的. 為此定義了一個數據結構vm_struct
, 將所有使用的部分保存在一個鏈表中. 該結構提的定義在include/linux/vmalloc.h?v=4.7, line 32
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32
struct vm_struct {
struct vm_struct *next;
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
unsigned int nr_pages;
phys_addr_t phys_addr;
const void *caller;
};
註意, 內核使用了一個重要的數據結構稱之為vm_area_struct, 以管理用戶空間進程的虛擬地址空間內容. 儘管名稱和目的都是類似的, 雖然二者都是做虛擬地址空間映射的, 但不能混淆這兩個結構。
- 前者是內核虛擬地址空間映射,而後者則是應用進程虛擬地址空間映射。
- 前者不會產生page fault,而後者一般不會提前分配頁面,只有當訪問的時候,產生page fault來分配頁面。
對於每個用vmalloc分配的子區域, 都對應於內核記憶體中的一個該結構實例. 該結構各個成員的語義如下
欄位 | 描述 |
---|---|
next | 使得內核可以將vmalloc區域中的所有子區域保存在一個單鏈表上 |
addr | 定義了分配的子區域在虛擬地址空間中的起始地址。size表示該子區域的長度. 可以根據該信息來勾畫出vmalloc區域的完整分配方案 |
flags | 存儲了與該記憶體區關聯的標誌集合, 這幾乎是不可避免的. 它只用於指定記憶體區類型 |
pages | 是一個指針,指向page指針的數組。每個數組成員都表示一個映射到虛擬地址空間中的物理記憶體頁的page實例 |
nr_pages | 指定pages中數組項的數目,即涉及的記憶體頁數目 |
phys_addr | 僅當用ioremap映射了由物理地址描述的物理記憶體區域時才需要。該信息保存在phys_addr中 |
caller |
其中flags只用於指定記憶體區類型, 所有可能的flag標識以巨集的形式定義在include/linux/vmalloc.h?v=4.7, line 14
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14
/* bits in flags of vmalloc's vm_struct below */
#define VM_IOREMAP 0x00000001 /* ioremap() and friends */
#define VM_ALLOC 0x00000002 /* vmalloc() */
#define VM_MAP 0x00000004 /* vmap()ed pages */
#define VM_USERMAP 0x00000008 /* suitable for remap_vmalloc_range */
#define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */
#define VM_NO_GUARD 0x00000040 /* don't add guard page */
#define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */
/* bits [20..32] reserved for arch specific ioremap internals */
flag標識 | 描述 |
---|---|
VM_IOREMAP | 表示將幾乎隨機的物理記憶體區域映射到vmalloc區域中. 這是一個特定於體繫結構的操作 |
VM_ALLOC | 指定由vmalloc產生的子區域 |
VM_MAP 用於表示將現存pages集合映射到連續的虛擬地址空間中
VM_USERMAP |
VM_UNINITIALIZED|
VM_NO_GUARD |
VM_KASAN|
下圖給出了該結構使用方式的一個實例. 其中依次映射了3個(假想的)物理記憶體頁, 在物理記憶體中的位置分別是1 023、725和7 311. 在虛擬的vmalloc區域中, 內核將其看作起始於VMALLOC_START + 100的一個連續記憶體區, 大小為3*PAGE_SIZE的內核地址空間,被映射到物理頁面725, 1023和7311
2.2 創建vm_area
因為大部分體繫結構都支持mmu, 這裡我們只考慮有mmu的情況. 實際上沒有mmu支持時, vmalloc就無法實現非連續物理地址到連續內核地址空間的映射, vmalloc
退化為kmalloc
實現.
2.2.1 vmlist全局鏈表
在創建一個新的虛擬記憶體區之前, 必須找到一個適當的位置. vm_area
實例組成的一個鏈表, 管理著vmalloc區域中已經建立的各個子區域. 定義在mm/vmalloc的全局變數vmlist是表頭. 定義在mm/vmalloc.c?v=4.7, line 1170
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1170
static struct vm_struct *vmlist __initdata;
2.2.2 分配函數
內核在mm/vmalloc中提供了輔助函數get_vm_area
和__get_vm_area
, 它們負責參數準備工作, 而實際的分配工作交給底層函數__get_vm_area_node
來完成, 這些函數定義在mm/vmalloc.c?v=4.7, line 1388
struct vm_struct *__get_vm_area(unsigned long size, unsigned long flags,
unsigned long start, unsigned long end)
{
return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
GFP_KERNEL, __builtin_return_address(0));
}
EXPORT_SYMBOL_GPL(__get_vm_area);
struct vm_struct *__get_vm_area_caller(unsigned long size, unsigned long flags,
unsigned long start, unsigned long end,
const void *caller)
{
return __get_vm_area_node(size, 1, flags, start, end, NUMA_NO_NODE,
GFP_KERNEL, caller);
}
/**
* get_vm_area - reserve a contiguous kernel virtual area
* @size: size of the area
* @flags: %VM_IOREMAP for I/O mappings or VM_ALLOC
*
* Search an area of @size in the kernel virtual mapping area,
* and reserved it for out purposes. Returns the area descriptor
* on success or %NULL on failure.
*/
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags)
{
return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
NUMA_NO_NODE, GFP_KERNEL,
__builtin_return_address(0));
}
struct vm_struct *get_vm_area_caller(unsigned long size, unsigned long flags,
const void *caller)
{
return __get_vm_area_node(size, 1, flags, VMALLOC_START, VMALLOC_END,
NUMA_NO_NODE, GFP_KERNEL, caller);
}
這些函數是負責實際工作的__get_vm_area_node函數的前端. 根據子區域的長度信息, __get_vm_area_node函數試圖在虛擬的vmalloc空間中找到一個適當的位置. 該函數定義在mm/vmalloc.c?v=4.7, line 1354
由於各個vmalloc
子區域之間需要插入1頁(警戒頁)作為安全隙, 內核首先適當提高需要分配的記憶體長度.
static struct vm_struct *__get_vm_area_node(unsigned long size,
unsigned long align, unsigned long flags, unsigned long start,
unsigned long end, int node, gfp_t gfp_mask, const void *caller)
{
struct vmap_area *va;
struct vm_struct *area;
BUG_ON(in_interrupt());
if (flags & VM_IOREMAP)
align = 1ul << clamp_t(int, fls_long(size),
PAGE_SHIFT, IOREMAP_MAX_ORDER);
size = PAGE_ALIGN(size);
if (unlikely(!size))
return NULL;
area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);
if (unlikely(!area))
return NULL;
if (!(flags & VM_NO_GUARD))
size += PAGE_SIZE;
va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
if (IS_ERR(va)) {
kfree(area);
return NULL;
}
setup_vmalloc_vm(area, va, flags, caller);
return area;
}
start和end參數分別由調用者設置, 比如get_vm_area函數和get_vm_area_caller函數傳入VMALLOC_START和VMALLOC_END. 接下來迴圈遍歷vmlist的所有表元素,直至找到一個適當的項
2.2.3 釋放函數
remove_vm_area
函數將一個現存的子區域從vmalloc地址空間刪除.
函數聲明如下, include/linux/vmalloc.h?v=4.7, line 121
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L121
struct vm_struct *remove_vm_area(void *addr);
函數定義在mm/vmalloc.c?v=4.7, line 1454
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1446
/**
* remove_vm_area - find and remove a continuous kernel virtual area
* @addr: base address
*
* Search for the kernel VM area starting at @addr, and remove it.
* This function returns the found VM area, but using it is NOT safe
* on SMP machines, except for its size or flags.
*/
struct vm_struct *remove_vm_area(const void *addr)
{
struct vmap_area *va;
va = find_vmap_area((unsigned long)addr);
if (va && va->flags & VM_VM_AREA) {
struct vm_struct *vm = va->vm;
spin_lock(&vmap_area_lock);
va->vm = NULL;
va->flags &= ~VM_VM_AREA;
spin_unlock(&vmap_area_lock);
vmap_debug_free_range(va->va_start, va->va_end);
kasan_free_shadow(vm);
free_unmap_vmap_area(va);
return vm;
}
return NULL;
}
2.3 vmalloc分配記憶體區
vmalloc
發起對不連續的記憶體區的分配操作. 該函數只是一個前端, 為__vmalloc
提供適當的參數, 後者直接調用__vmalloc_node
.
vmalloc只是__vmalloc_node_flags的前端介面, 複雜向__vmalloc_node_flags傳遞數據, 而__vmalloc_node_flags又是__vmalloc_node的前端介面, 而後者又將實際的工作交給__vmalloc_node_range函數來完成
vmalloc函數定義在mm/vmalloc.c?v=4.7, line 1754, 將實際的工作交給__vmalloc_node_flags函數來完成.
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1754
/**
* vmalloc - allocate virtually contiguous memory
* @size: allocation size
* Allocate enough pages to cover @size from the page level
* allocator and map them into contiguous kernel virtual space.
*
* For tight control over page level allocator and protection flags
* use __vmalloc() instead.
*/
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL | __GFP_HIGHMEM);
}
EXPORT_SYMBOL(vmalloc);
__vmalloc_node_flags
函數定義在mm/vmalloc.c?v=4.7, line 1747, 通過__vmalloc_node來完成實際的工作.
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1747
static inline void *__vmalloc_node_flags(unsigned long size,
int node, gfp_t flags)
{
return __vmalloc_node(size, 1, flags, PAGE_KERNEL,
node, __builtin_return_address(0));
}
__vmalloc_node
函數定義在mm/vmalloc.c?v=4.7, line 1719, 通過__vmalloc_node_range
來完成實際的工作.
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1719
/**
* __vmalloc_node - allocate virtually contiguous memory
* @size: allocation size
* @align: desired alignment
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
* @node: node to use for allocation or NUMA_NO_NODE
* @caller: caller's return address
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
static void *__vmalloc_node(unsigned long size, unsigned long align,
gfp_t gfp_mask, pgprot_t prot,
int node, const void *caller)
{
return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END,
gfp_mask, prot, 0, node, caller);
}
__vmalloc_node_range
最終完成了記憶體區的分配工作
// http://lxr.free-electrons.com/source/mm/vmalloc.c?v=4.7#L1658
/**
* __vmalloc_node_range - allocate virtually contiguous memory
* @size: allocation size
* @align: desired alignment
* @start: vm area range start
* @end: vm area range end
* @gfp_mask: flags for the page level allocator
* @prot: protection mask for the allocated pages
* @vm_flags: additional vm area flags (e.g. %VM_NO_GUARD)
* @node: node to use for allocation or NUMA_NO_NODE
* @caller: caller's return address
*
* Allocate enough pages to cover @size from the page level
* allocator with @gfp_mask flags. Map them into contiguous
* kernel virtual space, using a pagetable protection of @prot.
*/
void *__vmalloc_node_range(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, unsigned long vm_flags, int node,
const void *caller)
{
struct vm_struct *area;
void *addr;
unsigned long real_size = size;
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > totalram_pages)
goto fail;
area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED |
vm_flags, start, end, node, gfp_mask, caller);
if (!area)
goto fail;
addr = __vmalloc_area_node(area, gfp_mask, prot, node);
if (!addr)
return NULL;
/*
* In this function, newly allocated vm_struct has VM_UNINITIALIZED
* flag. It means that vm_struct is not fully initialized.
* Now, it is fully initialized, so remove this flag here.
*/
clear_vm_uninitialized_flag(area);
/*
* A ref_count = 2 is needed because vm_struct allocated in
* __get_vm_area_node() contains a reference to the virtual address of
* the vmalloc'ed block.
*/
kmemleak_alloc(addr, real_size, 2, gfp_mask);
return addr;
fail:
warn_alloc_failed(gfp_mask, 0,
"vmalloc: allocation failure: %lu bytes\n",
real_size);
return NULL;
}
實現分為3部分
- 首先,
get_vm_area
在vmalloc
地址空間中找到一個適當的區域. - 接下來從物理記憶體分配各個頁
- 最後將這些頁連續地映射到vmalloc區域中, 分配虛擬記憶體的工作就完成了.
如果顯式指定了分配頁幀的結點, 則內核調用alloc_pages_node
, 否則,使用alloc_page
從當前結點分配頁幀.
分配的頁從相關結點的伙伴系統移除. 在調用時, vmalloc
將gfp_mask設置為GFP_KERNEL
| __GFP_HIGHMEM
,內核通過該參數指示記憶體管理子系統儘可能從ZONE_HIGHMEM記憶體域分配頁幀. 理由已經在上文給出:低端記憶體域的頁幀更為寶貴,因此不應該浪費到vmalloc的分配中,在此使用高
3 備選映射方法
除了vmalloc
之外,還有其他方法可以創建虛擬連續映射。這些都基於上文討論的__vmalloc
函數或使用非常類似的機制
vmalloc_32
的工作方式與vmalloc相同,但會確保所使用的物理記憶體總是可以用普通32位指針定址。如果某種體繫結構的定址能力超出基於字長計算的範圍, 那麼這種保證就很重要。例如,在啟用了PAE
的IA-32
系統上,就是如此.- vmap使用一個page數組作為起點,來創建虛擬連續記憶體區。與vmalloc相比,該函數所用的物理記憶體位置不是隱式分配的,而需要先行分配好,作為參數傳遞。此類映射可通過vm_map實例中的VM_MAP標誌辨別。
- 不同於上述的所有映射方法, ioremap是一個特定於處理器的函數, 必須在所有體繫結構上實現. 它可以將取自物理地址空間、由系統匯流排用於I/O操作的一個記憶體塊,映射到內核的地址空間中.
該函數在設備驅動程式中使用很多, 可將用於與外設通信的地址區域暴露給內核的其他部分使用(當然也包括其本身).
4 釋放記憶體
有兩個函數用於向內核釋放記憶體, vfree用於釋放vmalloc和vmalloc_32分配的區域,而vunmap用於釋放由vmap或ioremap創建的映射。這兩個函數都會歸結到__vunmap
void __vunmap(void *addr, int deallocate_pages)
addr表示要釋放的區域的起始地址, deallocate_pages指定了是否將與該區域相關的物理記憶體頁返回給伙伴系統. vfree將後一個參數設置為1, 而vunmap設置為0, 因為在這種情況下只刪除映射, 而不將相關的物理記憶體頁返回給伙伴系統. 圖3-40給出了__vunmap的代碼流程圖
不必明確給出需要釋放的區域長度, 長度可以從vmlist中的信息導出. 因此__vunmap的第一個任務是在__remove_vm_area(由remove_vm_area在完成鎖定之後調用)中掃描該鏈表, 以找到 相關項。
unmap_vm_area使用找到的vm_area實例,從頁表刪除不再需要的項。與分配記憶體時類似,該函 數需要操作各級頁表,但這一次需要刪除涉及的項。它還會更新CPU高速緩存。
如果__vunmap的參數deallocate_pages設置為1(在vfree中),內核會遍歷area->pages的所 有元素,即指向所涉及的物理記憶體頁的page實例的指針。然後對每一項調用__free_page,將頁釋放 到伙伴系統。
最後,必須釋放用於管理該記憶體區的內核數據結構。