vmalloc原理與實現(xiàn)
在 Linux 系統(tǒng)中的每個進程都有獨立 4GB 內存空間,而 Linux 把這 4GB 內存空間劃分為用戶內存空間(0 ~ 3GB)和內核內存空間(3GB ~ 4GB),而內核內存空間由劃分為直接內存映射區(qū)和動態(tài)內存映射區(qū)(vmalloc區(qū))。
直接內存映射區(qū)從?3GB?開始到?3GB+896MB?處結束,直接內存映射區(qū)的特點就是物理地址與虛擬地址的關系為:虛擬地址 = 物理地址 + 3GB。而動態(tài)內存映射區(qū)不能通過這種簡單的關系關聯(lián),而是需要訪問動態(tài)內存映射區(qū)時,由內核動態(tài)申請物理內存并且映射到動態(tài)內存映射區(qū)中。下圖是動態(tài)內存映射區(qū)在內存空間的位置:

為什么需要vmalloc區(qū)
由于直接內存映射區(qū)(3GB ~ 3GB+896MB)是直接映射到物理地址(0 ~ 896MB)的,所以內核不能通過直接內存映射區(qū)使用到超過 896MB 之外的物理內存。這時候就需要提供一個機制能夠讓內核使用 896MB 之外的物理內存,所以 Linux 就實現(xiàn)了一個 vmalloc 機制。vmalloc 機制的目的是在內核內存空間提供一個內存區(qū),能夠讓這個內存區(qū)映射到 896MB 之外的物理內存。如下圖:

那么什么時候使用 vmalloc 呢?一般來說,如果要申請大塊的內存就可以用vmalloc。
vmalloc實現(xiàn)
可以通過?vmalloc()?函數(shù)向內核申請一塊內存,其原型如下:
void * vmalloc(unsigned long size);
參數(shù)?size?表示要申請的內存塊大小。
我們看看看?vmalloc()?函數(shù)的實現(xiàn),代碼如下:
static inline void * vmalloc(unsigned long size)
{
return __vmalloc(size, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL);
}
從上面代碼可以看出,vmalloc()?函數(shù)直接調用了?__vmalloc()?函數(shù),而?__vmalloc()?函數(shù)的實現(xiàn)如下:
void * __vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
void * addr;
struct vm_struct *area;
size = PAGE_ALIGN(size); // 內存對齊
if (!size || (size >> PAGE_SHIFT) > num_physpages) {
BUG();
return NULL;
}
area = get_vm_area(size, VM_ALLOC); // 申請一個合法的虛擬地址
if (!area)
return NULL;
addr = area->addr;
// 映射物理內存地址
if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) {
vfree(addr);
return NULL;
}
return addr;
}
__vmalloc()?函數(shù)主要工作有兩點:
調用?
get_vm_area()?函數(shù)申請一個合法的虛擬內存地址。調用?
vmalloc_area_pages()?函數(shù)把虛擬內存地址映射到物理內存地址。
接下來,我們看看?get_vm_area()?函數(shù)的實現(xiàn),代碼如下:
struct vm_struct * get_vm_area(unsigned long size, unsigned long flags)
{
unsigned long addr;
struct vm_struct **p, *tmp, *area;
area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL);
if (!area)
return NULL;
size += PAGE_SIZE;
addr = VMALLOC_START;
write_lock(&vmlist_lock);
for (p = &vmlist; (tmp = *p) ; p = &tmp->next) {
if ((size + addr) < addr)
goto out;
if (size + addr <= (unsigned long) tmp->addr)
break;
addr = tmp->size + (unsigned long) tmp->addr;
if (addr > VMALLOC_END-size)
goto out;
}
area->flags = flags;
area->addr = (void *)addr;
area->size = size;
area->next = *p;
*p = area;
write_unlock(&vmlist_lock);
return area;
out:
write_unlock(&vmlist_lock);
kfree(area);
return NULL;
}
get_vm_area()?函數(shù)比較簡單,首先申請一個類型為?vm_struct?的結構?area?用于保存申請到的虛擬內存地址。然后查找可用的虛擬內存地址,如果找到,就把虛擬內存到虛擬內存地址保存到?area?變量中。最后把?area?連接到?vmalloc?虛擬內存地址管理鏈表?vmlist?中。vmlist?鏈表最終結果如下圖:

申請到虛擬內存地址后,__vmalloc()?函數(shù)會調用?vmalloc_area_pages()?函數(shù)來對虛擬內存地址與物理內存地址進行映射。
我們知道,映射過程就是對進程的?頁表?進行映射。但每個進程都有一個獨立?頁表(內核線程除外),并且我們知道內核空間是所有進程共享的,那么就有個問題:如果只映射當前進程?頁表?的內核空間,那么怎么同步到其他進程的內核空間呢?
為了解決內核空間同步問題,Linux 并不是直接對當前進程的內核空間映射的,而是對?init?進程的內核空間(init_mm)進行映射,我們來看看?vmalloc_area_pages()?函數(shù)的實現(xiàn):
inline int vmalloc_area_pages (unsigned long address, unsigned long size,
int gfp_mask, pgprot_t prot)
{
pgd_t * dir;
unsigned long end = address + size;
int ret;
dir = pgd_offset_k(address); // 獲取 address 地址在 init 進程對應的頁目錄項
spin_lock(&init_mm.page_table_lock); // 對 init_mm 上鎖
do {
pmd_t *pmd;
pmd = pmd_alloc(&init_mm, dir, address);
ret = -ENOMEM;
if (!pmd)
break;
ret = -ENOMEM;
if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) // 對頁目錄項進行映射
break;
address = (address + PGDIR_SIZE) & PGDIR_MASK;
dir++;
ret = 0;
} while (address && (address < end));
spin_unlock(&init_mm.page_table_lock);
return ret;
}
從上面代碼可以看出,vmalloc_area_pages()?函數(shù)映射的主體是?init?進程的內存空間。因為映射的?init?進程的內存空間,所以當前進程訪問?vmalloc()?函數(shù)申請的內存時,由于沒有對虛擬內存進行映射,所以會發(fā)生?缺頁異常?而觸發(fā)內核調用?do_page_fault()?函數(shù)來修復。我們看看?do_page_fault()?函數(shù)對?vmalloc()?申請的內存異常處理:
void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...
__asm__("movl %%cr2,%0":"=r" (address)); // 獲取出錯的虛擬地址
...
if (address >= TASK_SIZE && !(error_code & 5))
goto vmalloc_fault;
...
vmalloc_fault:
{
int offset = __pgd_offset(address);
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pte_t *pte_k;
asm("movl %%cr3,%0":"=r" (pgd));
pgd = offset + (pgd_t *)__va(pgd);
pgd_k = init_mm.pgd + offset;
if (!pgd_present(*pgd_k))
goto no_context;
set_pgd(pgd, *pgd_k);
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
if (!pmd_present(*pmd_k))
goto no_context;
set_pmd(pmd, *pmd_k);
pte_k = pte_offset(pmd_k, address);
if (!pte_present(*pte_k))
goto no_context;
return;
}
}
上面的代碼就是當進程訪問?vmalloc()?函數(shù)申請到的內存時,發(fā)生?缺頁異常?而進行的異常修復,主要的修復過程就是把?init?進程的?頁表項?復制到當前進程的?頁表項?中,這樣就可以實現(xiàn)所有進程的內核內存地址空間同步。
