零頁面機制在缺頁中斷中的作用

科技小能手發表於2017-11-12

在2.6的早期核心以及更早的2.4,2.2以及1.X核心中有一個empty_zero_page的陣列,它是一個全域性的頁面陣列,它的作用很大,要比現在2.6.2X/3X核心中empty_zero_page的重要性大,empty_zero_page的主要作用就是隻要使用者引用一個只讀的匿名頁面並沒有進行寫操作,缺頁中斷處理中核心就不會給使用者程式分配新的頁面。零頁面不加入lru連結串列,因此它不會被換出,也就是說這些頁面根本不參與記憶體管理,它們沒有換入換出的必要,它們中沒有資料,它們僅僅使一些樁子;零頁面僅僅佔用了若干個地址,並且很確定,影響的cacheline也很確定,讀頁面本身並不會影響cacheline,因為這些頁面不允許寫,唯一使得零頁面影響cacheline的是對於其page結構中引用計數的操作,因為page結構本身也在記憶體當中,而2.6核心新引入了反向對映,而反向對映必然要操作引用計數,即使零頁面也不例外。我們可以看一下2.6.1的缺頁處理中的匿名頁面部分:

static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma, pte_t *page_table, pmd_t *pmd, int write_access, unsigned long addr)

{

pte_t entry;

struct page * page = ZERO_PAGE(addr); //得到零頁面,注意所有的程式缺頁中都會引用同樣的這個零頁面

entry = pte_wrprotect(mk_pte(ZERO_PAGE(addr), vma->vm_page_prot)); //防寫,只要有寫操作就會分配新頁面

if (write_access) {

page = alloc_page(GFP_HIGHUSER);

if (!page)

goto no_mem;

clear_user_highpage(page, addr); //安全約定,清除遺留資料

mm->rss++;

entry = pte_mkwrite(pte_mkdirty(mk_pte(page, vma->vm_page_prot)));

lru_cache_add_active(page); //加入lru,接受記憶體管理模組的管理

mark_page_accessed(page);

}

set_pte(page_table, entry); //設定頁表項

pte_chain = page_add_rmap(page, page_table, pte_chain); //雖然對於零頁其實不進行寫操作進而也不會破壞既有的cacheline對映,但是對於零頁面的page結構本身卻還是需要操作的,page可以視為零頁面的後設資料,零頁面可以避開寫,但是零頁面畢竟確實被選作了使用者頁面返給了使用者,因此需要接受一定的記憶體管理,接下來的反向對映就是這麼一回事,在2.6.1版本中,反向對映忽略了零頁面,但是在後來的版本中為了統一管理還是需要修改零頁面page結構的一些欄位導致了寫記憶體進一步快取被更新,最終導致了徹底去除了零頁面

pte_unmap(page_table);

update_mmu_cache(vma, addr, entry);

}

也許有人會問只讀頁面就一定是零頁面嗎?有資料的頁面也可能是隻讀的,這當然是對的,但是對於匿名頁面來講,就不對了,首先由於安全原則,新的匿名頁面必須清零,但是由於零頁面實際上沒有什麼用處,那麼必然有一個程式對其進行寫操作,因此它必須是可寫的,最起碼對於某一個程式是可寫的,但是對於檔案對映來講,只讀的的並且有資料的頁面是存在的,最簡單的例子就是elf檔案的程式碼段記憶體對映。匿名頁面一旦被寫入了資料,接下來如果該頁面被換出,然後重新引用該頁面的時候,do_swap_page將會被呼叫,以後的操作就是從交換空間換頁了,匿名頁面搖身一變成了交換空間的檔案頁面。理解了這一點接下來的問題就是既然零頁面實際上沒有什麼用處,那麼為何在使用者引用只讀頁面的時候必須分配頁面呢?這是作業系統的要求誰也迴避不了,我們能做的就是儘可能的讓一切更高效,依據懶惰原理,也即是等到不能再拖的時候在行動的原則,使用者引用匿名頁面並不一定馬上就是寫資料,而一旦寫了資料它就成了交換空間的檔案頁面了,因此對於第一次引用的只讀頁面,它實際上不應該有資料的,因為資料必須是寫入的,如果你讀它,得到的是全部0,因此沒有必要為只讀頁面分配頁面,所有的只讀頁面在第一次引用的時候只需要返回同一個零頁面即可,這樣就節省了大量的頁面,防止一些程式只引用只讀頁面而不進行寫操作而浪費大量的記憶體最終導致頻繁換頁。核心只考慮語義合理,具體實現就是怎麼高效安全怎麼來,對於只讀的,匿名的,第一次引用的頁面,對映到同一處沒有任何問題,因為都是沒有資料的零頁面,符合安全規則也更加高效,同時符合使用者空間程式設計語義。

那麼這種方式帶來的另一個效果是什麼呢?試想如果每次都分配新頁面,那麼每次有很大可能分到不同的頁面,這樣操作這些頁面的後設資料的結果就是造成重置大量的cacheline,非常影響效率,零頁面位置固定,影響cacheline的位置也固定並且很有限,因此效率可以提高。在只讀的匿名頁面缺頁之後,核心只會給使用者一個零頁面,並且該零頁面是以防寫方式給使用者提供的,然後一旦有程式對該頁面進行寫操作,那麼核心會以防寫違規的方式觸發缺頁中斷,在中斷處理中會重新分配一個新的頁面給使用者,這就是所謂的懶惰的方式,直到使用者程式最終寫頁面的時候才會分配頁面給使用者,對於零頁面,其實就是將新分配的頁面清零,這在同一程式中常見,程式往往先引用一個頁面,然後就行寫操作或者什麼也不做,對於非零頁面就是將老頁面的資料拷貝到新的頁面,這在fork時比較常見。在2.6.1的程式碼,呼叫了一個copy_cow_page來拷貝頁面資料,過程就是無論如何先分配一個頁面然後再考慮怎麼初始化其資料:

static inline void copy_cow_page(struct page * from, struct page * to, unsigned long address)

{

if (from == ZERO_PAGE(address)) {

clear_user_highpage(to, address);

return;

}

copy_user_highpage(to, from, address);

}

2.6.17的程式碼中去除了copy_cow_page函式,將分配頁面的動作放到了判斷頁面型別之後,因為2.6.17的核心中分配頁面更加細化了,這些事情還是自己看程式碼的好:

if (old_page == ZERO_PAGE(address)) {

new_page = alloc_zeroed_user_highpage(vma, address);

if (!new_page)

goto oom;

} else {

new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);

if (!new_page)

goto oom;

cow_user_page(new_page, old_page, address);

}

但是等等,事情發生了,不知道是好事還是壞事,我覺得不怎麼好,零頁面雖然有很奇妙的功效,但是在最近的核心中被去除了,在缺頁中斷中無論如何都不會考慮零頁面了,而是無論如何都分配新的頁面,難道零頁面不好嗎?Nick覺得不好,正是由於每引用一次零頁面就要修改page中的某些欄位,而page存於記憶體,這勢必會沖刷cacheline,在機器中,很多的地址將共用一個cacheline,因此即便零頁面再好(它比每次重新分配一個頁面好在可以控制cacheline的沖刷),它還是會將很多cacheline沖刷,正如一個罪犯隨機殺了10個人,另一個罪犯殺了確定的10個人,他們誰的罪更輕些?顯然這個問題很可笑,只可惜linux核心的早期版本正是被這種笑話衝昏了頭腦,實際上零頁面節省的這種cacheline就是類似的一個笑話。雖然固定的沖刷cacheline比隨機的沖刷帶來的損失可能更小,但是能小多少呢?可以確定的是對於第一次的匿名只讀頁面分配零頁面導致的多了將近一倍的缺頁,我們需要做的額外工作是將頁面清零,這雖然是個代價,但是卻比大量沖刷緩衝要便宜的多。為何呢?注意,每次在缺頁中斷中引用零頁面就意味著一次cacheline的繫結,因為要修改page資料結構的欄位,由於cacheling是一個cpu所有程式共用的,在程式分時排程下cacheline不斷被沖刷,每次缺頁將導致一次cacheline沖刷,雖然是固定地址但是誰也保證不了缺頁之前這個cacheline存放的是哪個程式的資料。於是零頁面機制在缺頁處理中正式下課。

縱觀零頁面的歷史,顯示出的是linux核心開發的靈活和當仁不讓!只要有用的誰也去不掉,只要沒有用,再花哨的東西終將廢止。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273482


相關文章