進程的一生——請求調頁篇

本文主要解答了三個問題:
1、爲什麼會有請頁機制
2、Linux內核怎麼處理缺頁
3、寫時複製的內核實現

注:本文所有的的內核代碼都是來自於kernel3.14.54,讀者可以未經作者允許隨意轉載,但請保證文章的完整性。

在講內存之前有幾個很重要的結構體簡單分析以下。
1、mm_struct結構體:進程內存描述符結構體,進程的task_struct的mm和active_mm結構體指向該進程的內存描述符結構體(普通進程的這兩個字段是相等的,內核線程的mm字段是NULL(線程沒有自己的線性地址空間),active__mm是指向上一個運行的內核線程)。主要字段有:線性區鏈表的頭結點,線性區紅黑樹的根(線性區採用紅黑樹和),全局頁表,線性區的個數,主計數器,次計數器,程序各個段的起始地址和最後地址等等。
2、vm_area_struct:描述線性區結構體。進程的線性地址空間是用一個個的線性區組織起來(組織的方式有紅黑樹和雙向鏈表,線性區定義中有next,prev和rb_node)。主要字段有:vm的起始地址,vm的最後地址,擁有這個vm的進程mm_struct和vm的操作函數(全部是鉤子函數)。

Linux內存管理採用四級頁表,pgd(頁全局目錄),pud(頁上級目錄),pmd(頁中間目錄),pte(頁表)。

1、爲什麼會有請頁

linux內核認爲自己的所有任務都是很緊急的,前面分析調度函數我們已經瞭解了內核態所有的任務都比用戶態任務優先級高(還記得嗎?全局就緒隊列會優先掃描實時進程,然後是普通進程,最後是空閒進程)。進程的空間管理類似,內核認爲自己的所有要求都是不可推遲的,內核要求的空間會直接分配,但是對於用戶態的任務會採用延遲處理的方式,剛剛產生的新進程並沒有自己的空間,而是去共享父進程的頁框,當子進程真的需要一個頁面的時候(exec或者對共享頁框執行寫操作等一些不能再延遲的時候),會產生一個異常,啓動請頁機制。所以用戶態進程的內存分配基本上都會觸發請頁機制。
在do_fork函數中,新創建的子進程會調用函數copy_mm複製父進程的內存管理等部分,下面來簡單看一下函數copy_mm的部分實現代碼:

oldmm = current->mm;
if (clone_flags & CLONE_VM) {   //kernel thread do this   
    atomic_inc(&oldmm->mm_users);
     mm = oldmm;
     goto good_mm;}
mm = dup_mm(tsk);   //else process do this
tsk->mm = mm;
tsk->active_mm = mm;

tsk是新創建的進程。不難看出,新創建的子進程是完全複製了父進程的mm。

2、Linux怎麼處理缺頁

如果子進程執行了修改頁內容(或者執行exec),這時候會引發一個缺頁異常,調用缺頁異常處理程序do_page_fault。部分代碼如下:

if (!mm || in_atomic())
    goto no_context;
if (address >= TASK_SIZE)
    goto vmalloc_fault;
retry:
down_read(&mm->mmap_sem);
vma = find_vma(mm, address);
if (!vma)
    goto bad_area;
if (vma->vm_start <= address)
    goto good_area;
...
good_area:
...     //出於安全,進行vma的權限判斷
fault = handle_mm_fault(mm, vma, address, flags);
if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current))
return;
if (flags & FAULT_FLAG_ALLOW_RETRY) {
    goto retry;
    }
up_read(&mm->mmap_sem);
return ;
}

如果正在執行原子操作或者正在執行內核線程(內核線程的mm爲NULL),函數不執行退出。如果虛擬地址超出了4GB(程序訪問了不可訪問的地址),跳到錯誤處理。獲取讀信號量(缺頁異常的處理都是在讀信號量中執行的),找出距離給定地址最近的一個線性區(vma描述);如果沒有找到,轉到錯誤處理;該如果線性區的起始地址低於給定的地址,開始執行:判斷vma的權限,調用函數handle_mm_fault(分析見後文),結果保存在fault中。當處理缺頁失敗需要重來並且current(引發缺頁的進程)未決信號集不空,轉去執行current的未決信號;未決信號集不空,重新處理缺頁。處理缺頁成功時候,打開讀鎖,返回。
上述是do_page_fault的全部邏輯過程,很容易看出處理缺頁被封裝在了函數handle_mm_fault中。函數handle_mm_fault代碼如下(函數的三個參數分別是:引發缺頁進程的mm_struct,在do_page_fault函數中找到的vma,給定的地址address和flag):

int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags)
{
__set_current_state(TASK_RUNNING);
...
ret = __handle_mm_fault(mm, vma, address, flags);
return ret;
}

更新current進程的狀態爲就緒態(缺頁異常處理的時候是等待態),進行一些標誌位判斷,調用函數__handle_mm_fault獲得物理頁框並返回。顯然,我們的重點是這個函數,函數代碼如下(參數分別是進程的mm_struct,在函數do_page_fault中找到的vma,指定地址和flags):

static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, unsigned int flags)
{
pgd = pgd_offset(mm, address);
pud = pud_alloc(mm, pgd, address);
pmd = pmd_alloc(mm, pud, address);
pte = pte_offset_map(pmd, address);
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}

獲取頁全局目錄,申請新的頁上級目錄和頁中間目錄(不知道爲什麼,但是這裏就是做了這些操作),得到address在頁表中的對應地址,執行handle_pte_fault函數(內核在這個函數裏具體分析引發缺頁的原因進而執行重新創建映射或者分配新頁框,很重要的函數)。函數代碼如下:

static int handle_pte_fault(struct mm_struct *mm,struct vm_area_struct *vma, unsigned long address,pte_t *pte, pmd_t *pmd, unsigned int flags)
{
if (!pte_present(entry)) {
    if (pte_none(entry)) {
        if (vma->vm_ops)
            return do_linear_fault(mm, vma, address,pte, pmd, flags, entry);
        return do_anonymous_page(mm, vma, address,pte, pmd, flags);
    if (pte_file(entry))
        return do_nonlinear_fault(mm, vma, address,pte, pmd, flags, entry);
    return do_swap_page(mm, vma, address,pte, pmd, flags, entry);
ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (flags & FAULT_FLAG_WRITE) {
    if (!pte_write(entry))
        return do_wp_page(mm, vma, address,pte, pmd, ptl, entry);
    entry = pte_mkdirty(entry);}
entry = pte_mkyoung(entry);
}

如果頁不在內存中並且從未被進程訪問過,如果線性區的操作函數不爲空執行了XXX(好像是創建一個新的映射,不懂爲什麼要這麼做),如果爲空則從未分配頁框,內核執行do_anonymous_page函數(分析見後文)分配一個新頁框。讀頁面的髒位,如果髒位被置1(此時頁不在內存中),執行非線性文件映射(不是本文的重點,不分析),如果髒位爲0,頁面被置換出去了所以調用函數do_swap_page把該頁換進內存。在解釋後邊代碼之前來稍微提一下寫時複製機制。

3、寫時複製的實現

在copy_mm函數實現中我們瞭解了linux採用寫時複製機制,所以每一個頁都需要有讀寫保護。當有進程試圖對具有寫保護的頁進行寫操作的時候,內核會分配一個新的頁框然後複製共享頁框的內容。
我們接着來分析這個函數。啓動自旋鎖,如果進程要執行寫操作但是這頁有寫保護(pte_write讀取頁的write權限)轉去執行do_wp_page(實現cow,好奇怪這個函數我找不到???爲什麼找不到還把這個代碼寫上去了);如果沒有寫保護,置頁的dirty位。最後置頁的訪問標誌。
通過上邊的分析,我們發現handle_pte_fault函數的大部分功能是查看頁的標誌位識別出來什麼原因引起了缺頁異常,然後轉到各自的處理函數。下面是對各個處理函數的分析。重新建立映射(略),把外存的也交換進內存(do_swap_page,在文件那一部分分析)。do_numa_page(在內存管理那一部分會分析),所以在這裏我們主要分析一個匿名分配函數。
do_anonymous_page函數(終於要到了分配頁框的時候了),代碼如下(函數的參數分別是,進程的mm_struct,找到的vma,指定地址,頁表,頁中間目錄和標誌位):

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,unsigned long address, pte_t *page_table, pmd_t *pmd,unsigned int flags)
{
if (!(flags & FAULT_FLAG_WRITE)) { 
    entry = pte_mkspecial(pfn_pte(my_zero_pfn(address),vma->vm_page_prot));
    page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
    if (!pte_none(*page_table))
        goto unlock;
    goto setpte;}
}
page = alloc_zeroed_user_highpage_movable(vma, address);
...
entry = mk_pte(page, vma->vm_page_prot);
if (vma->vm_flags & VM_WRITE)
    entry = pte_mkwrite(pte_mkdirty(entry));
page_table = pte_offset_map_lock(mm, pmd, address, &ptl);
page_add_new_anon_rmap(page, vma, address);
update_mmu_cache(vma, address, page_table);

如果進程只要求了讀頁,並不用分配頁框,直接跳去解鎖。分配新的頁面,創建一個新的頁表項,如果線性區(在do_page_fault函數中找到的)允許寫操作,設置新頁的髒位和寫標誌,產生頁表項的線性地址。爲新的匿名頁增加頁表映射,更新內存管理單元。
說兩點:
1、源碼中錯誤處理要比真正實現功能的代碼多得多。
2、內存這部分還挺有意思的,東西也聽過的,本文只是寫了請頁,還有地址轉換、頁面分配和頁面回收。
3、看函數之前如果已經明白了一些比較重要的結構,代碼看起來就容易多了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章