1. 程式人生 > >[翻譯] Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()(POST-COPY熱遷移)

[翻譯] Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()(POST-COPY熱遷移)

原文連結:Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()

核心開發者們經常想把核心中的功能移到使用者空間來實現,從而得到更好的效能。網路方面的一些功能就是這樣的。要把記憶體管理的一些功能移到使用者空間的想法可不太常見,但是並非沒有,比如Andrea Arcangeli的user-space page fault handing補丁。

頁面錯誤的處理一般需要從二級儲存中獲得資料,並將它放到出錯程序的地址空間中的某個地方。為啥想在使用者空間做這事兒呢?一個主要的應用場景是KVM虛擬機器的動態遷移。遷移需要移動虛擬機器的記憶體,這需要花費很長時間,而虛擬機器的使用者又希望在遷移時越快越好。最好是根本就意識不到虛擬機器正在遷移就好了。為了達到這個目標,就要只移動虛擬機器能在新宿主機上執行所需要的最少的記憶體。一旦虛擬機器開始在新宿主機上運行了,它必然會訪問一些還沒有移動過來的記憶體。如果(使用者空間的)虛擬機器管理器能獲取到頁面錯誤,那它就可以對頁面的需求程度進行排序。也就是在最小延遲的基礎上完成頁面的跨主機排程。

此外還有其他的應用場景——比如跨網路的分散式共享記憶體。

這個補丁集添加了兩個get_user_pages()的變體,用於使使用者空間獲取核心頁面:

long get_user_pages_locked(struct task_struct *tsk, struct mm_struct *mm,
                       unsigned long start, unsigned long nr_pages,
                       int write, int force, struct page **pages,
                   int *locked);
    long get_user_pages_unlocked(struct task_struct *tsk, struct mm_struct *mm,
                 unsigned long start, unsigned long nr_pages,
                 int write, int force, struct page **pages);

前一個函式在呼叫時會需要獲取mmap_sem訊號量。當*locked引數為零時,也可以釋放訊號量。第二個函式不需要獲取mmap_sem訊號量。在核心中使用這些函式可以在處理頁面錯誤時釋放mmap_sem訊號量從而提升效能。這在當前的核心中也很有用,而要把頁面錯誤處理交給使用者空間的話,它就是必須的了。在使用者空間佔有mmap_sem訊號量可不是啥好事兒。

然後給mdavise()系統呼叫MADV_USERFAULT標識。如果在某一塊記憶體上有這個標識,那核心就不會對這段記憶體進行錯誤處理。在沒有其他錯誤處理時,出錯的程序會收到SIGBUS訊號。從而把錯誤處理的任務交給了出錯的程序本身。一個新的系統呼叫在錯誤處理時用得到:

int remap_anon_pages(void *dest, void *src, unsigned long len,
                 unsigned long flags);

它把src處起始的len位元組大小的頁面移動到dest處。做這個操作之前有一些約束,首先dest處的記憶體必須為對映過——remap_anon_pages()函式不會覆蓋已經對映的區域的。src處的記憶體必須存在並且已經對映,而且頁面不能被其他程序共享。這些限制簡化了實現,但也增加使用者空間錯誤處理的靜態條件。

如果src是一個大頁面,並且len是2MB的整數倍,那整個頁面都會移動到dest。

有了這個機制,應用程式的SIGBUG訊號處理函式會響應記憶體分配錯誤,它會給記憶體中填入適當的內容,並呼叫ramap_anon_pages()函式進行記憶體對映。訊號處理函式返回時,頁面錯誤會繼續檢查,此時由於記憶體已經分配好,所以程式可以繼續正常執行。

用過類Unix系統訊號處理函式的人會認為這些工作不應該由這些函式來做。訊號處理函式不應該用於處理頁面錯誤。為此,Andrea添加了一個系統呼叫:

int userfaultfd(int flags);

這個函式返回一個檔案描述符,用於跟核心通訊,處理頁面錯誤。flags引數置為O_NONBLOCK時表示啟用非阻塞行為,但一般都不用。

獲取檔案描述符之後,應用程式寫入一個64位整數,用以表明協議版本。如果核心支援的話,也返回同樣的整數,如果不支援則返回-1。然後在有頁面錯誤產生時,應用程式可以讀取一個64位地址。應用程式解決了頁面錯誤之後,再將表示記憶體範圍的兩個指標寫回核心。

這裡程序需要一個專門用於頁面錯誤處理的執行緒。一旦有頁面錯誤產生,原來的工作執行緒暫停,錯誤處理執行緒開始工作。如果使用了userfaultfd()系統呼叫,則不會產生SIGBUS訊號。對於出錯的工作執行緒來說,一切跟原來都是一樣的,只不過速度會慢一點兒。

前文說過,使用者空間頁面錯誤處理有很多應用場景。如果一個應用程式要同時應付多個場景怎麼辦呢?如果這樣的話,應用程式可以用userfaultfd()函式開啟多個檔案描述符,並把每個檔案描述符限定在一定的記憶體範圍之內。可以通過寫入兩個指標來制定記憶體範圍;最低有效位由起始指標設定。這樣的話,只有在某記憶體範圍之內的頁面錯誤才會對應相應的檔案描述符。應用程式還必須設定MADV_USERFAULT標識。可以在同一個檔案描述符上設定多個範圍,但是同一範圍的記憶體只能由一個檔案描述符來處理。

關於remap_anon_pages()系統呼叫有很多討論。起初Linus懷疑remap_anon_pages()有沒有比remap_file_pages()更好,他覺得remap_file_pages()不好,不久就會被清除。然後他又覺得應該提供一個簡單的介面,錯誤處理程式只要呼叫write()進行處理就行。Andrea回覆說這樣的介面也能實現;錯誤處理函式只要將資料寫入檔案描述符,由核心處理剩下的事情。但他擔心這樣會失去零拷貝的功能。Linus回覆說他不關心零拷貝功能,他覺得不值得去實現它。

我們可以預見對get_user_pages()的這個優化很快就要進入核心了,儘管Linus對它還不完全滿意。餘下還有很多工作要做,可能要很長時間,而且最終可能也沒有remap_anon_pages()。但因為因為應用場景需要,對動態遷移的改進將會長期進行下去。