天天看點

swapper_pg_dir 以及 kmalloc() 與 vmalloc() 介紹

作者:Linux碼農

swapper_pg_dir 作用

核心維持着一組自己使用的頁表,也即主核心頁全局目錄。當核心在初始化完成後,其存放在 swapper_pg_dir 中,swapper_pg_dir 其實就是一個頁目錄的指針,頁目錄指針在 x86 中是要被加載到 cr3 寄存器的,每個程序都有一個頁目錄指針,這個指針訓示這個程序的記憶體映射資訊,每當切換到一個程序時,該程序的頁目錄指針就被加載到了 cr3,然後直到切換到别的程序的時候才更改。

swapper_pg_dir 隻是在核心初始化的時候被載入到 cr3 訓示記憶體映射資訊,之後在 init 程序啟動後就成了 idle 核心線程的頁目錄指針。

/sbin/init 由一個叫做 init 的核心線程 exec 而成,而 init 核心線程是原始的核心也就是後來的 idle 線程 do_fork 而成的,而在 do_fork 中會為新生的程序重新開機配置設定一個頁目錄指針,由此可見 swapper_pg_dir 隻是在 idle 和核心線程中被使用。

可是它的作用卻不隻是為 idle 程序訓示記憶體映射資訊,更多的,它作為一個核心空間的記憶體映射模闆而存在,在 linux 中,任何程序在核心空間就不分彼此了,所有的程序都會共用一份核心空間的記憶體映射,是以,核心空間是所有程序共享的,每當一個新的程序建立的時候,都會将 swapper_pg_dir 的 768 項以後的資訊全部複制到新程序頁目錄的 768 項以後,代表核心空間。另外在操作 3G+896M 以上的虛拟記憶體時,隻會更改 swapper_pg_dir 的映射資訊(因為虛拟記憶體中 3G~3G+896M 區域和實體記憶體的 0~896M 進行直接映射,已經映射好了,不需要修改),當别的程序通路到這些頁面的時候會發生缺頁,在缺頁進行中會與 swapper_pg_dir 同步。

do_fork -> copy_mm -> mm_init -> pgd_alloc

pgd_t *pgd_alloc(struct mm_struct *mm)
{
pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL);

if (pgd) {
memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
memcpy(pgd + USER_PTRS_PER_PGD, 
swapper_pg_dir + USER_PTRS_PER_PGD, 
(PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
}
return pgd;
}
           

其中 USER_PTRS_PER_PGD 值為 768,pgd_alloc 中把 swapper_pg_dir 中的 768~1023 項拷貝到程序頁表中。

所有的程序共享核心空間,是以共享核心頁表是很自然的事。理論上核心隻有一個頁表,對應的核心全局頁目錄 swapper_pg_dir。每個程序有自己的頁目錄,共 1024 項,其中的 768 項後與 swapper_pg_dir 相同,指向的是核心空間。

但是每個程序的核心位址空間的頁表并不是與主核心頁表完全一緻的,原因就是當 vmalloc 後修改了主核心頁表,但是程序的頁表并沒有修改。

asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...

if (address >= TASK_SIZE && !(error_code & 5))
goto vmalloc_fault;

...


vmalloc_fault:
{
/*
* Synchronize this task's top level page-table
* with the 'reference' page table.
*
* Do _not_ use "tsk" here. We might be inside
* an interrupt in the middle of a task switch..
*/
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
pgd_k = init_mm.pgd + offset; //核心頁表swapper_pg_dir的偏移處

if (!pgd_present(*pgd_k))
goto no_context;
set_pgd(pgd, *pgd_k); //把核心頁表swapper_pg_dir中目錄項拷貝到使用者頁表中

pmd = pmd_offset(pgd, address); //頁表偏移量
pmd_k = pmd_offset(pgd_k, address);//在swapper_pg_dir頁表偏移量
if (!pmd_present(*pmd_k)) //判斷pmd項是否存在,若否,傳回失敗
goto no_context;
set_pmd(pmd, *pmd_k); //把核心頁表swapper_pg_dir頁表項拷貝到使用者頁表中 

pte_k = pte_offset(pmd_k, address);
if (!pte_present(*pte_k)) //判斷pte項是否存在,若否,傳回失敗
goto no_context;
return;
}
}
           

對于二級頁表來講,上述代碼擷取的 pmd 也為 pgd,同理 pmd_k 也為 pgd_k。是以對于二級頁表,其隻是把 swapper_pg_dir 中的頁目錄項複制到使用者程序頁表中,頁表項大家共用。

swapper_pg_dir 以及 kmalloc() 與 vmalloc() 介紹

當程序通路到這塊虛拟位址空間時,程序的頁目錄項是空的,此時會産生一個缺頁中斷,然後調用 do_page_fault,在該方法中會判斷通路的虛拟位址是否大于 0xc0000000(3G),若大于 3G 說明要通路的虛拟位址在核心态,是以執行 vmalloc_fault,在 vmalloc_fault 中會把虛拟位址對應的頁目錄項、頁表項從 swapper_pg_dir 拷貝到程序的頁表中,這樣程序頁目錄項的值與主核心頁表進行同步後完全一緻。結果最後隻有通路到這段 vmalloc 區間的程序才會進行與主核心頁表的更新,當然核心修改了主核心頁表後一定要向所有 CPU 發送一個 TLB 重新整理請求,因為有可能某個 CPU 上正在運作的程序對應的頁表項儲存在了 TLB 中。

vmalloc 介紹

vmalloc配置設定的虛拟位址空間則限于 VMALLOC_START 與 VMALLOC_END 之間。

swapper_pg_dir 以及 kmalloc() 與 vmalloc() 介紹

每一塊 vmalloc 配置設定的核心虛拟記憶體都對應一個 vm_struct 結構體。不同的核心虛拟位址被 4k 大小的空閑區間隔,以防止越界。

vmalloc 區域并不和使用者空間記憶體映射一樣,通過 page fault 來裝載頁面的。vmalloc 映射建立好後,邏輯位址,實體頁面全部配置設定好,而且頁表也已經更新好,隻是此處為核心頁表,并沒有更新相關程序的頁表。在 vmalloc 區發生 page fault 時,将“核心頁表”同步到“程序頁表”中。這部分區域對應的線性位址在核心使用 vmalloc 配置設定記憶體時,其實就已經配置設定了相應的實體記憶體,并做了相應的映射,建立了相應的頁表項,但相關頁表項僅寫入了“核心頁表”,并沒有實時更新到“程序頁表中”,核心在這裡使用了“延遲更新”的政策,将“程序頁表”真正更新推遲到第一次通路相關線性位址,發生 page fault 時,此時在 page fault 的處理流程中進行“程序頁表”的更新。

static inline 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)
{
void * addr;
struct vm_struct *area;
//把size 參數取整為頁面大小(4096)的一個倍數,也就是按頁的大小進行對齊
size = PAGE_ALIGN(size);
if (!size || (size >> PAGE_SHIFT) > num_physpages) {
BUG();
return NULL;
}
/*
如果有大小合适的可用記憶體,就調用get_vm_area()獲得一個
記憶體區的結構。但真正的記憶體區還沒有獲得,
函數vmalloc_area_pages()真正進行非連續記憶體區的配置設定
*/
area = get_vm_area(size, VM_ALLOC); // 擷取一個 vm_struct 結構,代表申請的一塊區域
if (!area)
return NULL;
addr = area->addr;
//在swapper_pg_dir中建立相關頁表
if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) {
vfree(addr);
return NULL;
}
return addr;
}


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;
}


//該函數實際建立起了非連續記憶體區到實體頁面的映射。
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);
spin_lock(&init_mm.page_table_lock);
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;
}
           

釋放該區域使用 vfree()方法,該方法釋放區域時隻是修改核心頁表 swapper_pg_dir,它會把addr對應的pte頁表項設定為0,也就是調用ptep_get_and_clear() 将頁表項清0,設定完後調用 flush_tlb_all() 重新整理下TLB 。但它不會修改所有程序的程序頁表。

從 上面可知,swapper_pg_dir 和 使用者程序頁表共享頁目錄表,是以當再次通路該虛拟位址時,找不到頁表項,産生 page fault。

缺頁異常和上面配置設定的流程一樣,隻是最後對核心頁表 pte 項做檢查時候,發現核心頁表關于 addr 的 pte 頁表項是 0,就會報錯。這樣就避免了程序的非法通路。

kmalloc() 與 vmalloc() 的差別

kmalloc() 與 vmalloc() 都是在核心代碼中提供給其他子系統用來配置設定記憶體的函數,但二者有何差別?

kmalloc 代表的是 kernel_malloc 的意思,它是用于核心的記憶體配置設定函數。

kmalloc 的釋放對應于 kfree。

從前面的介紹已經看出,這兩個函數所配置設定的記憶體都處于核心空間,即從 3GB~4GB;但位置不同,kmalloc() 配置設定的記憶體處于3GB~high_memory 之間,而 vmalloc() 配置設定的記憶體在 VMALLOC_START~4GB 之間,也就是非連續記憶體區。

swapper_pg_dir 以及 kmalloc() 與 vmalloc() 介紹

一般情況下在驅動程式中都是調用 kmalloc() 來給資料結構配置設定記憶體,而 vmalloc() 用在為活動的交換區配置設定資料結構,為某些 I/O 驅動程式配置設定緩沖區,或為子產品配置設定空間,例如在 include/asm-i386/module.h 中定義了如下語句:

#define module_map(x) vmalloc(x)

其含義就是把子產品映射到非連續的記憶體區。

與 kmalloc() 和 vmalloc() 相對應,兩個釋放記憶體的函數為 kfree()和vfree()。

vmalloc() 配置設定的實體位址無需連續,而 kmalloc() 確定頁在實體上是連續的。

函數 vmalloc()從核心的虛拟位址空間(3G以上)配置設定一塊虛存以及相應的實體記憶體,類似于系統調用 brk()。

不過 brk()是由程序在使用者空間啟動并從使用者空間中配置設定的,而 vmalloc()則是從系統空間,也就是從核心中啟動的,從核心空間中配置設定的。

由 vmalloc()配置設定的空間不會被 kswapd換出,因為 kswapd 隻掃描各個程序的使用者空間,而根本就看不到通過 vmalloc()配置設定的頁面表項。

至于通過 kmalloc()配置設定的資料結構,則 kswapd 隻是從各個 slab 隊列中尋找和收集空閑不用的 slab,并釋放所占用的頁面,但是不會将尚在使用 slab 所占據的頁面換出。

繼續閱讀