非連續記憶體管理
從前面的章節我們知道,把記憶體對映待一組連續的頁框是最好的選擇,這樣會充分利用快取記憶體並獲得較低的平均訪問時間。不過,如果對記憶體區的請求不是很頻繁,那麼,透過連續的線性地址來訪問非連續的頁框這樣的一種分配方式就會顯得很有意義。
這種模式主要是避免了外碎片,而缺點是打亂了核心頁表。顯然,非連續記憶體區的大小必須是4096的倍數。Linux在幾個地方使用非連續記憶體區,例如,為活動的交換區分配資料結構,為模組分配空間(linux驅動模組),或者給某些I/O驅動程式分配快取。
非連續記憶體區的線性地址
見 高階記憶體對映的順序
在實體記憶體對映的末尾與第一個記憶體之間差入一個大小為8MB的安全區,目的是為了捕獲記憶體的越界訪問。出於同樣的理由,插入其他4kb大小的安全區來隔離非連續的記憶體區。
非連續記憶體區的描述符
型別 | 名稱 | 說明 |
---|---|---|
void* | addr | 記憶體區內第一個記憶體單元的線性地址 |
unsigned long | size | 記憶體區的大小 + 4096(記憶體區之間的安全區間的大小) |
unsigned long | flag | 非連續記憶體對映的記憶體的型別 |
strutc page** | pages | 指向 nr_pages陣列的指標,該陣列由指向頁描述符的指標組成 |
unsugned int | nr_pages | 記憶體區填充的頁框個數 |
unsugned long | phys_addr | 該欄位設為0,除非記憶體已被建立來對映一個硬體的I/O共享裝置 |
struct vm_struct* | next | 指向下一個vm_struct結構的指標 |
struct vm_struct {
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
unsigned int nr_pages;
unsigned long phys_addr;
struct vm_struct *next;
};
透過next
欄位,這些描述符被插入到一個簡單的連結串列中,連結串列的第一個元素的地址存放在vm_list變數中。flags欄位標識了非連續區對映的記憶體的型別:VM_ALLOC
表示使用vmalloc()
申請得到的頁,VM_MAP
表示使用vmap()
對映的已分配的頁,而VM_IOREMAP
表示使用ioremap()
對映的硬體裝置版的版上記憶體。
get_vm_area()
函式線上性地址VMALLOC_START
到VMALLOC_END
之間查詢一個空閒區域。
函式原型
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags);
執行步驟如下
- 呼叫
kmalloc()
獲得一個vm_struct
型別的記憶體區描述符。kmalloc
從slab
分配記憶體。 - 為寫得到
vmlist_lock
鎖, 並掃描型別為vm_struct
的描述符表來查詢空閒的線性地址,空閒線性地址大小至少為size + 4096
- 如果存在這樣的空間,函式就初始化描述符的欄位,釋放
vmlist_lock
鎖,並以返回這個非連續記憶體區的其實地址而結束。
初始化描述符包括addr,size,flag - 否則,get_vm_area()釋放先前得到的描述符,釋放vmlist_lock,然後返回NULL。
分配非連續記憶體區
vmalloc() 函式給核心分配一個非連續的記憶體區。
void *vmalloc(unsigned long size)
{
return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
struct vm_struct *area;
struct page **pages;
unsigned int nr_pages, array_size, i;
/* 將size擴充為4096的整數倍 */
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > num_physpages)
return NULL;
/* 建立一個新的描述符 描述符的flag欄位被初始化為 VM_ALLOC */
area = get_vm_area(size, VM_ALLOC);
if (!area)
return NULL;
/* 計算出 page 計數 */
nr_pages = size >> PAGE_SHIFT;
array_size = (nr_pages * sizeof(struct page *));
area->nr_pages = nr_pages;
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE)
pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL);
else
/* __vmalloc 呼叫kmalloc為pages欄位分配記憶體 */
pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM));
area->pages = pages;
/* 若分配失敗,則清除記憶體區描述符,並返回NULL */
if (!area->pages) {
remove_vm_area(area->addr);
kfree(area);
return NULL;
}
/* 將pages頁描述符陣列指標初始化為 0 */
memset(area->pages, 0, array_size);
/* 使用alloc_page 函式逐個申請頁框 頁框很可能不是連續的*/
for (i = 0; i < area->nr_pages; i++) {
area->pages[i] = alloc_page(gfp_mask);
if (unlikely(!area->pages[i])) {
/* Successfully allocated i pages, free them in __vunmap() */
area->nr_pages = i;
goto fail;
}
}
/* 現在已經分配了一組非連續的頁框來對映這些線性地址,最後至關重要的是修改核心使用的頁表項
以此表明分配給非連續記憶體區的每個頁框對應著一個線性地址 */
if (map_vm_area(area, prot, &pages))
goto fail;
return area->addr;
fail:
vfree(area->addr);
return NULL;
}
map_vm_area
函式將非連續頁框對映到連續頁框上
函式定義在mm/vmalloc.c line201
/* 引數
* area:vm_struct 描述符的指標
* prot:已分配頁框的保護位。在vmalloc中始終為 PAGE_KERNEL
* pages:struct page* 陣列的指標,
*/
int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages)
{
/*
* 先將非連續記憶體區的開始和末尾分別賦值給 address end
*/
unsigned long address = (unsigned long) area->addr;
unsigned long end = address + (area->size-PAGE_SIZE);
unsigned long next;
pgd_t *pgd;
int err = 0;
int i;
/*
linux 2.6使用 4級頁表
PGD --> PUD --> PMD --> PT
| | | |
| | | +--> 實際物理頁面 (pages)
| | +------> 中間目錄
| +------------> 上級目錄
+------------------> 全域性目錄
pgd_offset_k 用於獲取pgd條目得指標
pgd_index 用於獲取pgd條目得索引
類似於陣列的下標和指標關係
*/
/* 獲取address所在的全域性頁目錄表起始條目 */
pgd = pgd_offset_k(address);
// 獲取頁表自旋鎖,避免競爭條件
spin_lock(&init_mm.page_table_lock);
/*從起始條目開始遍歷,直到所有得頁框都被對映*/
for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
// 新建以一個條目,用來存放所有的page,起始點為address
pud_t *pud = pud_alloc(&init_mm, pgd, address);
if (!pud) {
err = -ENOMEM;
break;
}
// 計算線性地址的所在的下一個全域性頁目錄項的起始地址
next = (address + PGDIR_SIZE) & PGDIR_MASK;
// 如果下一個起始地址 超出了當表項範圍,說明在當前表項可以對映所有的實體記憶體
if (next < address || next > end)
next = end;
// 然後向下依次新增更第一記的條目
if (map_area_pud(pud, address, next, prot, pages)) {
err = -ENOMEM;
break;
}
/*****************************************************************************************************************
上層目錄對映
The map_area_pud( ) function executes a similar cycle for all the page tables that a Page Upper Directory points to:
*/
do {
pmd_t * pmd = pmd_alloc(&init_mm, pud, address);
if (!pmd)
return -ENOMEM;
if (map_area_pmd(pmd, address, end-address, prot, pages))
return -ENOMEM;
address = (address + PUD_SIZE) & PUD_MASK;
pud++;
} while (address < end);
/* 中間目錄對映
The map_area_pmd( ) function executes a similar cycle for all the Page Tables that a Page Middle Directory points to:
*/
do {
/* 分配一個新的頁表 */
pte_t * pte = pte_alloc_kernel(&init_mm, pmd, address);
if (!pte)
return -ENOMEM;
if (map_area_pte(pte, address, end-address, prot, pages))
return -ENOMEM;
address = (address + PMD_SIZE) & PMD_MASK;
pmd++;
} while (address < end);
/* 頁框對映
The main cycle of map_area_pte( ) is:
*/
do {
struct page * page = **pages;
// 將物理頁的內容以及已分配頁框的保護位 填入該頁表實現對映
set_pte(pte, mk_pte(page, prot));
address += PAGE_SIZE;
pte++;
(*pages)++;
} while (address < end);
/****************************************************************************************************************/
address = next;
pgd++;
}
/* 物理頁對映完成,釋放鎖 並重新整理快取*/
spin_unlock(&init_mm.page_table_lock);
flush_cache_vmap((unsigned long) area->addr, end);
return err;
}
除了vmalloc()
函式外,非來納許記憶體區還能由vmalloc_32()
函式分配,但是它只從ZONE_NORMAL
和ZONE_DMA
記憶體管理區中分配頁框
Linux2.6還特別提供了一個vmap函式與vmalloc很相似,但是他不分配頁框。完成的工作是,在vmalloc虛擬地址空間中找到一個空閒區域,然後將page頁面陣列對應的實體記憶體對映到該區域,最終返回對映的虛擬起始地址。
釋放非連續記憶體區
vfree()
函式釋放vmalloc()
或者vmalloc_32()
建立的非連續記憶體區,而vunmap()
函式釋放vmap()
建立的記憶體區。兩個函式都是用同一個引數————將要釋放的記憶體區的線性地址address
,他們都依賴於__vunmap()
函式來做實質上的工作。該函式執行以下操作:
- 呼叫
remove_vm_area()
函式得到vm_struct
描述符的地址area
,並清除非連續記憶體區中的線性地址對應的核心的頁表項 - 如果
deallocate
被置位,函式掃描指向頁描述符的area->pages
指標陣列,對於陣列的每一個元素,呼叫__free_page()
函式釋放頁框到分割槽頁框分配器。此外,執行kfree(area->pages)
來釋放陣列本身。 - 呼叫
kfree(area)
來釋放vm_struct
描述符。
/* vfree 和 vunmap 透過置位__vunmap函式的deallocate_pages的值來決定是否釋放pages中的頁框,
當然一般是 vmalloc vfree,vmap vunmap成對使用
*/
void vfree(void *addr)
{
BUG_ON(in_interrupt());
__vunmap(addr, 1);
}
void vunmap(void *addr)
{
BUG_ON(in_interrupt());
__vunmap(addr, 0);
}
void __vunmap(void *addr, int deallocate_pages)
{
struct vm_struct *area;
if (!addr)
return;
// 檢查 address是否頁對齊
if ((PAGE_SIZE-1) & (unsigned long)addr) {
printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
WARN_ON(1);
return;
}
// 從全域性核心頁表目錄開始,逐級取消對映
area = remove_vm_area(addr);
if (unlikely(!area)) {
printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n",
addr);
WARN_ON(1);
return;
}
// 根據傳入的標誌決定是否釋放頁框,vfree釋放,vunmap不釋放頁框
if (deallocate_pages) {
int i;
// 釋放歸還頁框
for (i = 0; i < area->nr_pages; i++) {
if (unlikely(!area->pages[i]))
BUG();
__free_page(area->pages[i]);
}
// 然後再釋放為頁框描述符申請的頁框描述符陣列
if (area->nr_pages > PAGE_SIZE/sizeof(struct page *))
vfree(area->pages);
else
kfree(area->pages);
}
kfree(area);
return;
}
```c
struct vm_struct *remove_vm_area(void *addr)
{
struct vm_struct **p, *tmp;
write_lock(&vmlist_lock);
for (p = &vmlist ; (tmp = *p) != NULL ;p = &tmp->next) {
if (tmp->addr == addr)
goto found;
}
write_unlock(&vmlist_lock);
return NULL;
found:
unmap_vm_area(tmp);
*p = tmp->next;
write_unlock(&vmlist_lock);
return tmp;
}