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

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

那么什么時(shí)候使用 vmalloc 呢?一般來說,如果要申請(qǐng)大塊的內(nèi)存就可以用vmalloc。
vmalloc實(shí)現(xiàn)
可以通過?vmalloc()?函數(shù)向內(nèi)核申請(qǐng)一塊內(nèi)存,其原型如下:
void * vmalloc(unsigned long size);
參數(shù)?size?表示要申請(qǐng)的內(nèi)存塊大小。
我們看看看?vmalloc()?函數(shù)的實(shí)現(xiàn),代碼如下:
static inline void * vmalloc(unsigned long size)
{
return __vmalloc(size, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL);
}
從上面代碼可以看出,vmalloc()?函數(shù)直接調(diào)用了?__vmalloc()?函數(shù),而?__vmalloc()?函數(shù)的實(shí)現(xiàn)如下:
void * __vmalloc(unsigned long size, int gfp_mask, pgprot_t prot)
{
void * addr;
struct vm_struct *area;
size = PAGE_ALIGN(size); // 內(nèi)存對(duì)齊
if (!size || (size >> PAGE_SHIFT) > num_physpages) {
BUG();
return NULL;
}
area = get_vm_area(size, VM_ALLOC); // 申請(qǐng)一個(gè)合法的虛擬地址
if (!area)
return NULL;
addr = area->addr;
// 映射物理內(nèi)存地址
if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) {
vfree(addr);
return NULL;
}
return addr;
}
__vmalloc()?函數(shù)主要工作有兩點(diǎn):
調(diào)用?
get_vm_area()?函數(shù)申請(qǐng)一個(gè)合法的虛擬內(nèi)存地址。調(diào)用?
vmalloc_area_pages()?函數(shù)把虛擬內(nèi)存地址映射到物理內(nèi)存地址。
接下來,我們看看?get_vm_area()?函數(shù)的實(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ù)比較簡(jiǎn)單,首先申請(qǐng)一個(gè)類型為?vm_struct?的結(jié)構(gòu)?area?用于保存申請(qǐng)到的虛擬內(nèi)存地址。然后查找可用的虛擬內(nèi)存地址,如果找到,就把虛擬內(nèi)存到虛擬內(nèi)存地址保存到?area?變量中。最后把?area?連接到?vmalloc?虛擬內(nèi)存地址管理鏈表?vmlist?中。vmlist?鏈表最終結(jié)果如下圖:

申請(qǐng)到虛擬內(nèi)存地址后,__vmalloc()?函數(shù)會(huì)調(diào)用?vmalloc_area_pages()?函數(shù)來對(duì)虛擬內(nèi)存地址與物理內(nèi)存地址進(jìn)行映射。
我們知道,映射過程就是對(duì)進(jìn)程的?頁表?進(jìn)行映射。但每個(gè)進(jìn)程都有一個(gè)獨(dú)立?頁表(內(nèi)核線程除外),并且我們知道內(nèi)核空間是所有進(jìn)程共享的,那么就有個(gè)問題:如果只映射當(dāng)前進(jìn)程?頁表?的內(nèi)核空間,那么怎么同步到其他進(jìn)程的內(nèi)核空間呢?
為了解決內(nèi)核空間同步問題,Linux 并不是直接對(duì)當(dāng)前進(jìn)程的內(nèi)核空間映射的,而是對(duì)?init?進(jìn)程的內(nèi)核空間(init_mm)進(jìn)行映射,我們來看看?vmalloc_area_pages()?函數(shù)的實(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 進(jìn)程對(duì)應(yīng)的頁目錄項(xiàng)
spin_lock(&init_mm.page_table_lock); // 對(duì) 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)) // 對(duì)頁目錄項(xiàng)進(jìn)行映射
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?進(jìn)程的內(nèi)存空間。因?yàn)橛成涞?init?進(jìn)程的內(nèi)存空間,所以當(dāng)前進(jìn)程訪問?vmalloc()?函數(shù)申請(qǐng)的內(nèi)存時(shí),由于沒有對(duì)虛擬內(nèi)存進(jìn)行映射,所以會(huì)發(fā)生?缺頁異常?而觸發(fā)內(nèi)核調(diào)用?do_page_fault()?函數(shù)來修復(fù)。我們看看?do_page_fault()?函數(shù)對(duì)?vmalloc()?申請(qǐng)的內(nèi)存異常處理:
void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...
__asm__("movl %%cr2,%0":"=r" (address)); // 獲取出錯(cuò)的虛擬地址
...
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;
}
}
上面的代碼就是當(dāng)進(jìn)程訪問?vmalloc()?函數(shù)申請(qǐng)到的內(nèi)存時(shí),發(fā)生?缺頁異常?而進(jìn)行的異常修復(fù),主要的修復(fù)過程就是把?init?進(jìn)程的?頁表項(xiàng)?復(fù)制到當(dāng)前進(jìn)程的?頁表項(xiàng)?中,這樣就可以實(shí)現(xiàn)所有進(jìn)程的內(nèi)核內(nèi)存地址空間同步。
