請求調頁和寫時複製

請求調頁

請求調頁和寫時複製

在創建進程的時候,一是進程結構體的創建以及插入到內核進程管理鏈表中,二是進程的空間是怎麼括增的,因為創建進程只是創建了結構體,並沒有執行代碼在裡面,所以怎麼把執行代碼加入是很重要的。

這裡就介紹一下內核的做法:請求調頁。

“請求調頁”指的是一種動態內存分配技術,它把頁框的分配推遲到不能再推遲的時候。也就是說,一直推遲到進程要訪問的頁不在 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 的使用計數器減少兩次。第一次的減少是取消複製頁框內容之前進行的安全性增加;第二次的減少是反映當前進程不再擁有該頁框這一事實。


分享到:


相關文章: