請求調頁
在創建進程的時候,一是進程結構體的創建以及插入到內核進程管理鏈表中,二是進程的空間是怎麼括增的,因為創建進程只是創建了結構體,並沒有執行代碼在裡面,所以怎麼把執行代碼加入是很重要的。
這裡就介紹一下內核的做法:請求調頁。
“請求調頁”指的是一種動態內存分配技術,它把頁框的分配推遲到不能再推遲的時候。也就是說,一直推遲到進程要訪問的頁不在 RAM 中時為止,由此引起一個缺頁異常。
為什麼使用請求調頁?因為在進程開始運行的時候並不訪問其地址空間中的全部地址;事實上,有一部分地址也許永遠不會被進程使用。此外,進程的局部性原理保證了在程序執行的每個階段,真正引用的進程頁只有一小部分,因此臨時用不著的頁所在的頁框可以由其他進程來使用。因此,對於全局分配(一開始就給進程分配所需要的全部頁框,直到程序結束才釋放這些頁框)來說,請求調頁是首選的,因為它增加了系統中的空閒頁框的平均數,從而更好地利用空閒內存。從另一個觀點來看,在 RAM 總數保持不變的情況下,請求調頁從總體上能使系統有更大的吞吐量。
但是請求調頁也是存在缺點的,那就是系統的額外開銷。因為請求調頁這一操作必須由內核完成,而這樣會造成 CPU 時鐘週期的浪費。
下來分析內核源碼
當內核響應請求調頁時,首先會檢測一下缺少的這一頁在哪?它有可能不在主存中,原因是進程從來都沒有訪問過該頁,或者是該頁長時間沒有被訪問,被內核回收了。
源碼中根據兩個宏定義的結果來區分這兩種情況:
entry = *pte;
if (!pte_present(entry)) {
if (pte_none(entry))
return do_no_page(mm, vma, address, write_access, pte, pmd);
if (pte_file(entry)) {
return do_file_page(mm, vma, address, write_access, pte, pmd);
}
return do_swap_page(mm, vma, address,pte, pmd, entry, write_access);
}
該代碼運行在 handle_pte_fault() 函數中。
其中的 pte_none() 宏定義是用來該頁有沒有被進程訪問過,如果沒有則返回 1 。
pte_file() 宏定義頁是否屬於非線性磁盤文件的映射,若是非線性的,則返回 1 。
當上述兩種判斷均 不成立時,則說明該頁被進程訪問過,只是該頁當前被內核回收了。
下來,討論的重點放在,進程從未訪問過該頁。如果是這樣,內核接下來將會執行 do_no_page() 函數。該函數通過檢查 vma 線性區描述符的 nopage 字段來確定這一點,如果頁被映射到一個文件, nopage 字段就指向一個函數, 該函數把所缺的頁從磁盤裝入到 RAM 。但是,內核在這裡對 nopage 字段做了判斷。
因為 vma->vm_ops->nopage 字段不為 NULL ,則線性區映射了一個磁盤文件, nopage 指向裝入頁的函數。
如果 vma->vm_ops 或 vma->vm_ops->nopage 字段為 NULL ,則線性區沒有映射磁盤文件,也就是說,它是一個匿名映射。因此,do_no_page() 調用 do_anonymous_page() 函數獲得一個新的頁框,執行代碼如下:
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table, pmd, write_access, address);
do_anonymous_page() 函數分別處理寫請求和讀請求,執行代碼如下:
if (write_access) {
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
page = alloc_page(GFP_HIGHUSER | __GFP_ZERO);
spin_lock(&mm->page_table_lock);
page_table = pte_offset_map(pwd, addr);
mm->res++;
entry = maybe_mkwriter(pte_mkdirty(mk_pte(page, vma->vm_page_prot)), vma);
lru_cache_add_active(page);
SetPageReference(page);
set_pte(page_table, entry);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
pte_unmap() 宏的第一次執行是釋放一種臨時的內核映射,它映射了在調用 handle_pte_fault() 函數之前由 pte_offset_map 所建立的頁表項的高端內存物理地址。 pte_offset_map 和 pte_unmap 這對宏獲取和釋放同一個臨時內核映射。臨時內核映射必須在調用 alloc_page() 之前釋放,因為這個函數可能阻塞當前進程。
函數遞增內存描述的 rss 字段以記錄分配給進程的頁框總數,相應的頁表項設置為頁框的物理地址,頁表框被標記為即髒又可寫。
lru_cache_add_active() 函數把新頁框插入與交換相關的數據結構中。
處理讀訪問時,頁的內容是無關緊要的,因為進程第一次對它訪問。給進程一個填充為0的頁要比給它一個由其他進程填充了信息的舊頁更為安全。Linux在請求調頁方面做得更深入一些。沒有必要立即給進程分配一個填充為0的新頁框,由於我們也可以給它一個現有的稱為零頁的頁,這樣就可以進一步推遲頁框的分配。零頁在內核初始化期間被靜態分配,並存放在empty_zero_page變量中(長為4096字節的數組,並用0填充)。
因此用零頁的物理地址設置頁表項:
entry = pte_wrprotect(mk_pte(virt_to_page(empty_zero_page), vma->vm_page_prot));
set_pte(page_table, entry);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
由於這個頁被標記為不可寫的,因此如果進程試圖寫這個頁,則寫時複製機制被激活。當且僅當在這個時候,進程才獲得一個屬於自己的頁並對它進行寫操作。
寫時複製
寫時複製(copy on write, cow)。這種思想相當簡單:父進程和子進程共享頁框而不是複製頁框。然而,只要頁框被共享,它們就不能被修改。無論父進程還是子進程何時試圖寫一個共享的頁框,就產生一個異常,這是內核就把這個頁複製到一個新的頁框並標記為可寫。原來的頁框仍然是寫保護的:當其他進程試圖寫入時,內核檢查寫進程是否是這個頁框的唯一屬主,如果是,就把這個頁框標記為對這個進程是可寫的。
頁描述符的 _count 字段用於跟蹤共享相應頁框的進程數目。只要進程釋放一個頁框或者在它上面執行寫時複製,它的 __count 字段就減小;只有當 __count 變為 -1 時,這個頁框才被釋放。
現在我們講述 Linux 怎樣實現寫時複製。當 handle_pte_fault() 確定缺頁異常是由訪問內存中現有的一個頁而引起時,它執行以下指令:
if (pte_present(entry)) {
if (write_access) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address, pte, pmd, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
set_pte(pte, entry);
flush_tlb_page(vma, address);
pte_unmap(pte);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
handle_pte_fault() 函數是與體系結構無關的:它考慮任何違背頁訪問權限的可能。然而,在 80x86 體系結構上,如果頁是存在的,那麼,範文權限是寫允許的而頁框是寫保護的。因此,總是要調用 do_wp_page() 函數。
do_wp_page() 函數首先獲取與缺頁異常相關的頁框描述符(缺頁表項對應的頁框)。接下來,函數確定頁的複製是否真正必要。如果僅有一個進程擁有這個頁,那麼,寫時複製就不必應用,且該進程應當自由的寫該頁。具體來說,函數讀取頁描述符的 _count 字段:如果它等於 0 (只有一個所有者),寫時複製就不必進行。實際上,檢查要稍微複雜些,因為當頁插入到交換高速緩存,並且當設置了頁描述符的 PG_private 標誌時,_count 字段也增加。不過,當寫時複製不進行時,就把該頁框標記為可寫的,一面試圖寫時引起進一步的缺頁異常:
set_pte(page_table, maybe_mkwrite(pte_mkyoung(pte_mkdirty(pte)), vma));
flush_tlb_page(vma, address);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
如果兩個或多個進程通過寫時複製共享頁框,那麼函數就把舊頁框(old_page)的內容複製到新分配的頁框(new_page)中。為了避免競爭條件,在開始複製操作前調用 get_page() 把 old_page 的使用計數加1:
old_page = pte_page(pte);
pte_unmap(page_table);
get_page(old_page);
spin_unlock(&mm->page_table_lock);
if (old_page == virt_to_page(empty_zero_page))
new_page = alloc_page(GFP_HIGHUSER | __GFP_ZERO);
} else {
new_page = alloc_page(GFP_HIGHUSER);
vfrom = kmap_atomic(old_page, KM_USER0);
vto = kmap_atomic(new_page, KM_USER1);
copy_page(vto, vfrom);
kunmap_atomic(vfrom, KM_USER0);
kunmap_atomic(vto, KM_USER1);
}
如果舊頁框是零頁,就在分配新的頁框時把 __GFP_ZERO 標誌置為 0 。否則,使用 copy_page() 宏複製頁框的內容。不要求一定要對零頁做特殊處理,但是特殊處理確實能夠提高系統的性能,因為他減少地址引用而保護了微處理器的硬件告訴緩存。
因為頁框的分配可能阻塞進程,因此,函數檢查自從函數開始執行以來是否已經修改了頁表項(pte 和 *page_table 具有不同的值)。如果已經修改,那麼新的頁框被釋放, old_page 的使用計數器被減少(取消以前的增加),函數結束。
如果所有的事情看起來進展順利,那麼,新頁框的物理地址最終被寫進頁表項,且使相應的 TLB 寄存器無效:
spin_lock(&mm->pagfe_table_lock);
entry = maybe_mkwrite(pte_mkdirty(mk_pte(new_page, vma->vm_pae_prot)), vma);
set_pte(page_table, entry);
flush_tlb_page(vma, address);
lru_cache_add_active(new_page);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
lru_cache_add_active() 函數把新頁框插入到與交換相關的數據結構中。
最後, do_wp_page() 把 old_page 的使用計數器減少兩次。第一次的減少是取消複製頁框內容之前進行的安全性增加;第二次的減少是反映當前進程不再擁有該頁框這一事實。
閱讀更多 有理想的代碼dog 的文章