阿裏內核月報2014年3月
目前Linux內核急需的一項功能是在線打補丁的特性。此前被Oracle收購的ksplice一度是Linux上唯一的解決方案。但是在被Oracle收購後,ksplice就閉源了,並且成為了Oracle Linux的一項商業特性。而目前可以拿到的最新版本的ksplice仍然僅僅停留在0.19上,而可以直接使用的內核版本則是2.6.26。對於更新的內核版本則還無法使用。
在目前的實際生產環境中,不論是企業應用還是互聯網公司,對於內核bug的修複仍然停留在原始的安排停機,升級內核,重啟服務的方式上。這一方式帶來的後果就是停機時造成的服務中斷。而通過主備切換方式進行停機也有可能造成部分數據的損失和服務的短時間中斷。總之,隻要你需要停機,就存在服務中斷的風險。而這對於企業用戶、互聯網公司來說屬於不能接受或應該盡量避免的。想必是看到了這一點,Redhat和SUSE兩家公司分別啟動了一個關於內核熱打補丁的項目:kpatch和kGraft。
根據目前看到的文獻資料,這兩個項目解決問題的方案是十分接近的,即通過Linux內核已有的ftrace機製來對有問題的函數進行替換。目前ftrace通過在函數調用過程中插入斷點(INT 3)的方式來實現相應的功能。而kpatch和kGraft則將這一斷點替換為一個長跳轉,從而將有問題的函數替換為新的功能正常的函數。在實現這一功能的過程中,這兩個項目的區別是替換時如何解決新舊代碼不一致的問題。kpatch的解決方法是通過stop_machine()加以解決的;而kGraft則是通過類似RCU的方式來更新舊代碼。此外的一個區別是生成帶有補丁的內核模塊的方式。kpatch是基於patch的方式來生成內核模塊的;而kGraft則可以自動通過源代碼來生成帶有新補丁的內核模塊。
目前這兩個項目均可以在網上獲取到源代碼。kpatch,kGraft。有興趣的讀者可以一探究竟。
Finding the proper scope of a file collapse operation
係統調用的設計絕不簡單。當開發者們設計一個接口的時候,經常有一些邊角的情況沒有被考慮到。涉及文件係統的係統調用似乎 特別容易出現這種問題,因為文件係統實現的複雜性和多樣性意味著當一個開發者想創建一個新的文件操作的時候,他將不得不考 慮許多特例。在fallocate()係統調用的推薦添加的討論中能看到一些這樣的特例。
fallocate()是關於一個文件內空間分配的係統調用。它的初始目的是允許一個應用在寫這些文件之前給它們分配空間塊。這種預 分配確保了在試著往那寫數據之前已經有了可用的磁盤空間;它也有助於文件係統實現在磁盤上更有效地分布被分配的空間。後來 添加了FALLOC_FL_PUNCH_HOLE文件操作,用來去分配文件裏的塊空間,留一個文件空洞。
在二月份,Namjae Jeon推薦了一個新的fallocate()操作FALLOC_FL_COLLAPSE_RANGE,這個草案包括了針對於ext4和xfs文 件係統的實現。它像打洞操作一樣刪除了文件數據,但又有一個不同,它沒有留一個洞在文件裏,而是移動文件中受影響區間之後 的所有數據到該區間的開始位置,整體上縮短文件大小。這種操作的直接用戶將是視頻編輯應用,它能使用這種操作快速而有效地 視頻文件中的一段內容。假如被刪除的範圍是塊對齊(這對於一些文件係統是個必須的前提條件),這種刪除可能受改變文件區間 映射圖所影響,不需要任何實際的數據複製。考慮到那些包含視頻數據的文件可能是大的,我們不難理解一個有效的裁剪操作為什 麼是有吸引力的。
因此對於這種操作將會出現什麼樣的問題呢?首先一個問題將可能是與mmap()係統調用的交互,這種操作將映射一個文件到一個進 程的地址空間。被推薦的草案是通過從page cache中刪除從被受影響區間到文件結尾的所有頁麵來實現的。髒頁首先被寫回磁盤。 這種方法阻止了那些已經通過映射被寫入的數據的丟失,而且也有利於節省了那些一旦操作完成而超出了文件結尾的內存頁麵。這 應該不是個大問題,正如Dave Chinner指出的,使用這種操作的應用通常不會訪問被映射過的文件。除此之外,受這種操作文件 影響的應用同樣不能處理沒有這種操作的其他修改。
但是,正如Hugh Dickins提醒的,tmpfs文件係統會有相關的問題,所有的文件存在在page cache,看起來非常像是一個內存映 射。既然page cache是個後備存儲,從page cache裏刪除所有的文件不可能會有個好的結果。所以在tmpfs能支持collapse操作 之前,還有許多與page cache相關的工作需要做。Hugh不確定tmpfs是否需要這種操作,但他認為為tmpfs解決page cache問題 同樣很可能能為其他文件係統帶來更穩健的實現。
Hugh也想知道單向的collapse操作是否應該被設計成雙向的。
Andrew Morton更進一步,建議一個簡單的“從一個地方移動一些塊到另外一個地方”的係統調用可能是個更好的想法。但Dave不 太看好這個建議,他擔心可能會引入更多地複雜性和困難的極端情況。
Andrew不同意,他說更通用的接口更受歡迎,問題能被克服;但沒有其他人支持他這種觀點。因此,機會是,這種操作將局限於那 些超出文件的崩潰的塊組;也許以後添加一個單獨的插入操作,應該是個有意思的用例。
同時,有一個其他的行為上的問題要解答;假如從文件中刪除的區間到達了文件結尾將會發生什麼?對於這種場景,當前的補丁集 返回EINVAL,一個想法是建議調用truncate()。Ted Ts'o問這種操作是否應該之間變成truncate()調用,但Dave是反對這種想 法的,他說一個包含了文件結尾的崩潰操作很可能是有缺陷的,在這種情況下,最好返回一個錯誤。
假如允許崩潰操作包含文件結尾,很明顯將也會有一些有趣的安全問題出現。文件係統能分配文件尾後的塊。fallocate()肯定能 用顯式地請求這種行為。文件係統基本上沒有零化這些塊;代替地,它們被保持不可訪問以致無論包含了什麼樣的不穩定數據都不能 被讀取。沒有太多的考慮,這種允許區間超出文件結尾的崩潰操作最終會暴露那些數據,特別是假如在中間它被中斷(也許被一個係 統崩潰)。Dave不認可這種為文件係統開發者設置陷阱的行為,他更喜歡從一開始杜絕這種帶有風險的操作,特別是既然還沒有任何 真實的需求來支持它。
因此,所有討論的最後結果是FALLOC_FL_COLLAPSE_RANGE操作將很可能基本原封不動地進入內核。它將不會包含所有的一些開發者 希望看到的功能,而是支持一個有助於加速一類應用的特性。從長期來看,這是否是足夠的依然需要時間的檢驗。係統調用API設計是 難的。但是,假如在將來需要一些其他的特性,能采用某種兼容的方法來創建一些新的FALLOC_FL命令。
Tracing unsigned modules
在已簽名模塊中複用“tainted kernel”標誌會給在未簽名模塊中使用tracepoint造成麻煩,這個問題比較容易解決,但其實也有一些阻力,另外內核Hacker們也沒有興趣幫助Linux內核代碼外的模塊解決問題。
內核的模塊加載機製已有近20年的曆史(1995年自1.2版本引入),但直到最近才引入加密簽名的驗證機製:內核隻加載被認可的模塊。Redhat內核早就支持了這個特性。Kernel的編譯者可以指定用於簽名的密鑰;私有密鑰被保存或被丟棄,公有密鑰則被編譯到內核裏。
有幾個內核參數可以影響到模塊簽名:CONFIG_MODULE_SIG控製是否使能簽名檢查。 CONFIG_MODULE_SIG_FORCE 決定是否所有的模塊都需要簽名。如果CONFIG_MODULE_SIG_FORCE沒有被開啟(並且沒有設置啟動參數module.sig_enforce ),那麼沒有簽名或簽名不匹配的模塊也可以被加載。此時,內核被標記為“tainted”。 如果用戶使用modprobe –force去加載與內核版本不匹配的模塊,內核也會設置“taint”標記。強製加載內核非常危險,由於與內核的內存布局不一致,很可能會導致內核crash掉。而且此類的Bug報告肯定會被內核開發者們忽略掉。
但是,加載一個未簽名模塊並不一定會導致crash,因此並不是很有必要使用“TAINT_FORCED_MODULE”。Tracepoint機製不支持被強製加載的module是因為在不匹配的模塊中使用tracepoint很容易掛掉內核。Tracepoint允許TAINT_CRAP與 TAINT_OOT_MODULE,但是如果有其它任何一個“taint”標記,模塊中的tracepoint是默認被關掉的。 Mathieu Desnoyers發布了一個RFC patch意圖改變此現狀。該patch引入了“TAINT_UNSIGNED_MODULE”用以標記是否有此類module被加載。並且允許Tracepoint支持這種“taint”類型。Ingo Molnar立即給了NAK,他不希望外部模塊影響kernel的穩定。
但是,有一些發行版的kernel已經打開了簽名檢查,但是允許通過module.sig_enforce指定是否真的使用。發行版的密鑰存在kernel image中。嚴格的檢查隻允許發行版自己的模塊被加載。這嚴重限製了用戶對模塊的使用。
模塊子係統的維護者Rusty Russell也沒有被此項功能打動。他還在進一步尋找使用案例。
Steven Rostedt指出,如果沒有模塊被加載,我們為什麼還要設置FORCED_MODULE標記。他也經常收到用戶們發布的“在未簽名模塊中不能使用Tracepoint”的Bug報告。
Johannes Berg最終提供了Russell一直在尋找的使用案例。Berg還提供了使用未簽名內核的另一個原因:重kernel.org的wiki上backport內核模塊用以支持發行版提供者不支持的硬件驅動。
Berg的使用案例足以使Russell同意將此patch加入他的pending tree。我們也將會在3.15版本中看到Desnoyers的最終的patch版本。屆時,kernel將會區分兩種不同的taint,用戶也能夠在他們加載的模塊中使用Tracepoint,無論是否簽名。
Optimizing VMA caching
Linux進程的虛擬地址空間被內核分割成多個虛擬內存區域(VMA),每個VMA描述了這個地址空間的一些屬性,比如存儲後端,訪問控製等。一個mmap()調用創建一個空間映射,這個空間在內核中就用VMA來描述,但是將一個可執行文件加載到內存,需要創建好幾個VMA。我們可以通過/proc/PID/maps來查看進程的VMA列表。在內存管理子係統中,通過一個虛擬地址找到對應的VMA是一個非常頻繁的操作。比如,每一次缺頁中斷都會觸發這個操作。所以不出大家所料,這個內核操作已經是高度優化過的。可能會令一些讀者意外的是這個操作還有優化提升的空間。
內核用紅黑樹來保存進程的VMA,紅黑樹可以保證很好的線性查找效率。紅黑樹有自己的優點,比如擴展性強,能支持大幾百個VMA還有很好的查找效率。但是每一次查找都得從樹頂遍曆到葉子節點。為了在某些情況下避免這種開銷,內核毫不意外的用了一個極簡的緩存策略,保存上一次查找的結果。這種簡單的緩存效果還不錯,對於大多數應用基本上都有50%或以上的命中率。
但是Davidlohr Bueso同學認為內核可以做得更好。他覺得一個緩存項太少了,多一個肯定效果會好一些。在去年11月的時候,他提交了一個補丁,就是增加了一個緩存項,指向具有最大空間的VMA。背後的理論很直白,空間越大被查找的概率就越大。多了這個緩存項,一些應用的命中率確實從50%上升到60%了。這是一個不大不小的優化,但是沒有進入內核主幹。很大的一個原因就是Linus跳出來說,“這個補丁讓我很生氣。”當然,一般有品味的大神對你說這話,除了看得起你,肯定是有後話的。Linus說了一堆為什麼這麼做沒有品味的原因,最重要的是提出了他自己認可的解決方法,而且一針見血。大意就是要優化就要視野要更寬廣,目前VMA查找對於多線程應用來說是很不友好的,一個進程才一個緩存項,所以當務之急是把per-process的緩存變成per-thread的。
Davidlohr同學聆聽教誨之後,奮發圖強,經過幾次迭代,提交了一套很有希望進入主幹的補丁。該補丁主要就是把per-process的緩存項變成了per-thread的。優化效果喜人,對於單線程和多線程的應用都有幫助。比如說係統啟動,主要都是單線程操作,命中率從51%提升到73%。多線程應用,比如大家都以為無法提升的內核編譯,命中率從75%提升到88%。實際的多線程應用,效果可能更好。比如通過觀察模擬多線程網頁服務器負載的ebizzy,命中率驚人的從1%提高到了99.97%。
有了以上的測試數據,沒有任何理由把這個補丁阻擋在內核的門外。大概在3.15內核,大家可以受享到這個VMA查找優化了。
Unmixing the pool
H. Peter Anvin同學在2014年的LSF會上主持了一個簡短的討論會,向大家問了一個簡單的問題:你認為硬件特別是CPU的哪些改進可以簡化內核內存管理的工作?雖然HPA不保證這個問題會對硬件帶來真正的改進,但他說能保證把話帶回英特爾。
第一個抱怨更多的是跟底層硬件聯係緊密的代碼:Rik van Riel提到PowerPC架構沒有類似x86上TLB的頁表緩存的FLUSH操作。其它的架構(比如SPARC)也有類似的限製。這使得抽象層的共用代碼部分很難寫。他希望硬件方麵能有一些改進,讓硬件無關的代碼能很方便的更新頁表。
對於x86平台,Peter Zijlstra希望能方便的讓部分頁表失效。另外一個很受期待的請求是支持64KB的頁。目前隻支持4KB和2MB大小的頁。
Mel Gorman希望硬件提供一種對內存頁快速的填零操作。目前對於2MB內存頁填零是一個耗時的操作,所以這個特性可以提高內存大頁的效率。有人建議可以考慮在內核空閑時做填零的操作,但是Christoph Lameter說他已經這樣嚐試過了,沒有什麼效果。
另外一個請求是提供一個效率更高更快的iret實現。這將大大提高缺頁中斷的效率,因為它通過iret返回到用戶層。
期間有人討論了一下處理器之間發送消息的開銷,還有希望一個mwait指令讓對於用戶層的某些應用可能會有幫助。對於處理器未來的變化,讓我們拭目以待。
MCS locks and qspinlocks
spinlock是一種簡單的鎖機製,當某個bit被清除時鎖是available的。一個線程需要申請該鎖時,通過調用原子操作compare-and-swap指令設置那個bit。如果鎖不是available的,線程就會一直spinning。最近幾年spinlock開始變得複雜起來,2008年加入了ticket spinlock用於實現公平性,2013年又加入了新特性用於更好的支持虛擬化。
當前spinlock還存在一個比較嚴重的問題就是cache-line bouncing:每次嚐試獲取鎖的時候都需要將這個鎖對應的cache line挪到local cpu上。對於鎖競爭特別嚴重的場景下,這個問題對性能影響很大。之前有過一組patch嚐試解決這個問題,但是最終沒有被merge
MCS lock
Tim Chen提交了一組patch用於解決這個問題,基於一篇1992年的論文實現的"MCS lock"。其思想是將spinlock擴展為一個per-CPU的結構,能夠很好的消除cache-line bouncing問題。
3.15的tip tree裏麵,MCS lock定義如下:
struct mcs_spinlock {
struct mcs_spinlock *next;
int locked; /* 1 if lock acquired */
};
使用的時候首先需要定義一個unlocked的MCS lock(locked位為0),這個相當於是一個單鏈表的tail記錄頭,next記錄的是鎖的最末尾一項。當有一個新mcs_spinlock鎖加入時,新鎖的next指向NULL(MCS lock的next為NULL時)或者MCS lock的next的next,MCS lock的next指向新鎖。 當一個CPU1嚐試拿鎖的時候,需要先提供一個本地的mcs_spinlock,如果鎖是available的,那麼本地mcs_spinlock的locked為0,設置MCS lock的locked為1,本地mcs_spinlock的next設置為MCS lock的next,MCS lock的next設置為本地的mcs_spinlock。如果CPU2也嚐試拿鎖,那麼也需要提供一個CPU2本地的mcs_spinlock,MCS lock記錄的tail修改為CPU2的mcs_spinlock,CPU1的next設置為CPU2.
------------
| next |-------------------------
|------------| |
| 1 | |
------------ |
|
V
CPU1 ------------ CPU2 ------------
| next | -----------> | next | --> NULL
|------------| |------------|
| 0 | | 1 |
------------ ------------
當CPU1鎖釋放之後會修改CPU2對應的locked為0,CPU2就能拿鎖成功。這樣CPU2上麵拿鎖spinning的時候讀取的都是本地的locked值,同時也能夠保證按照CPU拿鎖的順序保證獲取鎖的順序,實現公平性。
Qspinlock
MCS lock目前僅僅是用在實現mutex上,並沒有替換之前的ticket spinlock。主要原因是ticket spinlock隻需要32bit,但是MCS lock不止,由於spinlock大量嵌入在內核的數據結構中,部分類似struct page這種數據結構不能夠容許size的增大。為此需要使用一些別的變通方法來實現。
目前看有可能被合並的是Peter Zijlstra提供的一組qspinlock patch。這組patch中每個CPU會分配一個包含四個mcs_spinlock的數組。需要四個mcs_spinlock的原因是由於軟中斷、硬中斷、不可屏蔽中斷導致的單個CPU上麵會有多次獲取同一個鎖的可能。
這組patch中,qspinlock是32bit的,也就解決了之前MCS lock替換ticket spinlock導致size增大的問題。
AIM7的測試結果看部分workload有一小點的性能退化,但是其他workload有1~2%的性能提升(對底層鎖的優化方麵,這是一個很好的結果)。disk相關的benchmark測試性能提升甚至達到116%,這個benchmark在vfs和ext4文件係統代碼路徑上存在很嚴重的鎖競爭。
Volatile ranges and MADV_FREE
目前kernel的"shrinker"接口是內存管理子係統用來通知其他子係統發生了內存緊張,並促使他們盡可能地釋放一些內存,也有各種努力嚐試增加一個類似的機製來允許內核通知用戶態來收縮內存,但是基本都太複雜而不易合並進內存管理代碼。但是大家沒有停止嚐試,最近就有兩個相關的patch。這兩個patchset都基於一個新的名詞volatile range,它是指進程地址空間中的一段數據可再生的內存區域。在內存緊張時,內核可以直接回收這種區域裏的內存,進程在以後需要的時候再重新生成;在內存充裕時,該區域的內存不會被回收,程序也可隨時訪問這些數據。這些patch的最初動機是為了替換Android上的ashmem機製,當然也可能有其他潛在用戶。
Volatile ranges
之前有很多版本的volatile range實現,有的是基於posix_fadvise()係統調用,有的是基於fallocate(),也有一些基於madvise()。John Stultz的實現則增加了一個新的係統調用:
int vrange(void *start, size_t length, int mode, int *purged);
vrange()操作的範圍是從start地址開始的,長度為length的空間。如果mode是VRANGE_VOLATILE,這段區域將被設置成volatile;如果mode是VRANGE_NONVOLATILE,這段區域的volatile將會被去掉,但是這個過程中有些volatile頁可能已經被回收了,此時*purged會被設置成一個非0值來標識內存區域的數據已經不可用了,而如果*purged被設置了0則表示數據仍可用。如果進程繼續訪問volatile range的內存且訪問了已經被回收的頁,它就會收到SIGBUS信號來表明頁已經不在了。這個版本的patch還有另外一個地方不同於其他方式:它隻對匿名頁有效,而以前的版本可以工作在tmpfs下。隻滿足匿名頁使得patch很簡單也更容易被review和merge,但是也有一個很大的缺點:不能工作在tmpfs下也就無法替換ashmem。目前的計劃是先在基本patch實現上達成一致再考慮實現更複雜的文件接口。vrange()內部是工作在VMA層次上,掛在一個VMA上的所有頁要麼是volatile的要麼不是,調用vrange()時可能會發生VMA的拆分和合並。因為不用遍曆range裏的每一個頁,所以vrange()速度非常快。
MADV_FREE
另外一個實現方法是Minchan Kim的MADV_FREE patchset,他擴展了現有的madvise()調用:
int madvise(void *addr, size_t length, int advice);
madvise()的advice標識了希望內核采取的行為,MADV_DONTNEED告訴內核馬上回收指定內存區域的頁,而MADV_FREE則將這些頁標識為延遲回收。當內核內存緊張時,這些頁將會被優先回收,如果應用程序在頁回收後又再次訪問,內核將會返回一個新的並設置為0的頁。而如果內核內存充裕時,標識為MADV_FREE的頁會仍然存在,後續的訪問會清掉延遲釋放的標誌位並正常讀取原來的數據,因此應用程序不檢查頁的數據,就無法知道頁的數據是否已經被丟棄。
MADV_FREE更傾向於在用戶態內存分配器中發揮作用,當應用程序釋放了一組頁,分配器將會使用MADV_FREE告訴內核。如果應用程序很快在同一段地址空間又分配了內存,它就會使用原來的頁,而不需要先釋放原來的頁在分配並清零新的頁。簡而言之,MADV_FREE代表"我不再關心這段地址空間裏的數據,但是我可能會再次使用這段地址空間"。另外,BSD已經支持MADV_FREE,而不像vrange()隻是linux的特性,這顯然會提高程序的可移植性。
目前這兩種方法都沒有收到很多的review,但是在2014 LSFMM上會有一個相關的討論。
SO_PEERCGROUP: which container is calling?
隨著Linux上的各種Container解決方案不斷成熟,主流發行版開發者們也開始行動起來了-他們開始構思一個完全由Container組成的運行時環境(還記得Docker嗎?)實現這偉大計劃的其中一個需求是:負責這些Containers的資源管理、生命周期管理的中控守護進程需要知道和自己通信的各個進程各屬於哪個cgroup。Vivek Goyal最近提交了一個實現此功能的Patch,通過給Domain Socket添加新的命令字來讓通信的雙方知道對方在哪個組裏。這patch代碼非常簡單,但仍然不出意外地引起了一場郵件列表上的大討論。
這個Patch的大概想法是這樣的:給Domain Socket的getsockopt()係統調用添加一個叫做SO_PEERCGROUP的命令字,每次打開這個socket的進程調用這個命令字,就給它返回對端所在的組 - 精確地說是連接建立時對端所在的組,因為它後邊可能被移到別的組裏了,對於開發者們考慮的典型場景來說,返回連接建立時的情況就已足夠。
主要的反對意見來自Andy Lutomirski,他的抱怨真是多,但核心思想主要是一條:“一個位於cgroup控製組中的進程根本不應該知道它自己在組裏!”而Vivek的Patch需要組內進程配合才能工作,遠不僅僅是“知道”自己在某個組裏。他提出了替代解決方案
- 首先,我們可以給每個cgroup組配上一個user namespace,然後就可以通過現有的SO_PEERCRED + SCM_CREDENTIALS獲得socket對端的進程的uid的映射,進而間接地推斷出它在哪個cgroup組裏。Vivek反對這麼做,主要原因是user namespace還遠未成熟,Simo Sorce也表示這麼做不現實,不會在近期考慮給Docker加上user namespace支持。
- 其次,Andy認為我們可以在/proc/{pid}/ns下增加接口,把各個進程的cgroup組信息輸出在那兒(這聽起來似乎是最簡單易行的辦法了),然而Simo認為這樣做易用性太差,某個進程可能占用了一個pid然後死掉,然後有新進程又占用了這個pid,同一個pid已經換人了,這種情況下用戶態程序很難感知到。這一類的race一直不好解決,不然我們直接添加一個/proc/{pid}/cgroup就萬事大吉了。
Andy在討論過程中也向Simo提了一個小問題:既然添加這個命令字主要是為了給Docker用,而Docker是同時使用了namespace和cgroup兩種機製的,那麼是不是說Docker可以跨network namespace使用domain socket了?比如,在根組裏建立一個叫/mysocket的domain socket,再把它bind mount到Container裏邊的同名路徑下,就可以跨container通信了,這樣做現在可以嗎?*應該*可以嗎?這個問題在工作中也曾困擾過淘寶內核組,留給親愛的讀者們做思考題好了 :p
總之,這個線索討論到最後誰也沒有說服誰,我們相信隨著Unix標準中不存在的新特性不斷進入Linux內核,這樣的討論還會越來越多。內核開發者們不禁開始思考這樣一個事情:當我們向內核加入一個新特性的時候,到底應該在多大範圍內勸說用戶使用它們?到底應該在多大範圍內和我們已有的特性結合在一起?畢竟如果我們加入一個新特性,又非常不相信它,以至於我們自己都不願意使用,那真是非常奇怪的事情。
User-space out-of-memory handling
大家應該對Linux上的OOM killer不陌生,當使用了過多內存且swap都被用完後經常就會發生。Linux kernel發現無法回收上來任何內存後它就會選擇kill掉一個進程,而這可能是運行幾年之久的web瀏覽器,媒體播放器或者X window環境,導致一下子丟失了很多工作。有時候這種行為可能是有用的,比如殺掉了一個正在內存泄漏的程序,使得其他程序可以繼續正常運行,但是大多數時候它都會在沒有任何通知的情況下讓你丟失重要的東西。Google的David Rientjes最近提出了一套patchset使得即將OOM時可以給程序發送通知並自己采取行動,比如選擇犧牲哪個進程,檢查是否發生了內存泄漏或者保存一些信息用於debug。目前的OOM策略是找到並殺死使用內存最多的進程,我們可以改變/proc//oom_score_adj的值對使用的內存量進行加權並影響OOM的決策,但是也僅限於此,我們無法把自己實現OOM kill的行為告知內核,例如我們不能選擇殺死一個新生成的程序而不是跑了一年的web服務器,也不能選擇一個優先級最低的程序。
目前有兩種類型的OOM行為:全局OOM-整個係統內存緊張時發生和memcg OOM-memcg內存使用量達到限製,用戶態oom機製可以同時處理這兩種類型的OOM。
memcg 簡介
Memcg是使一組進程跑在一個cgroup裏並對總的內存使用量整體監控,可以用它來防止使用的總內存超過某一限製,因此提供了一個有效的內存隔離機製。當memcg的使用量超過限製且無法回收時,memcg就會觸發OOM,這其中分為兩個階段:頁分配階段即頁分配器分配空閑內存頁的過程,和記賬階段即memcg統計內存使用量的過程。如果頁分配階段失敗,這意味著係統整體內存緊張,如果頁分配成功但記賬失敗,就說明memcg內部內存緊張。如果要使用memcg,首先要確認內核編譯了該功能,確認方法如下:
$ grep CONFIG_MEMCG /boot/config-$(uname -r)
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
# CONFIG_MEMCG_SWAP_ENABLED is not set
# CONFIG_MEMCG_KMEM is not set
可以看出內核開啟了memcg,接下來確認是否掛載:
$ grep memory /proc/mounts
cgroup /sys/fs/cgroup/memory cgroup rw,memory 0 0
也已經掛載了。如果沒有,可通過如下命令掛載:
mount -t cgroup none /sys/fs/cgroup/memory -o memory
根掛載點本身就是root memcg,它控製係統上的所有進程,可以通過mkdir來創建子memcg。每一個memcg都有以下四個控製文件:
cgroup.procs or tasks: memcg裏的進程pid列表
memory.limit_in_bytes: memcg可使用的內存總量,單位為字節
memory.usage_in_bytes: memcg當前使用的內存量,單位為字節
memory.oom_control: 當memcg內存緊張時,允許進程注冊eventfd()並獲取通知
David Rientjes增加了另外一個控製文件:
memory.oom_reserve_in_bytes: 可以被正等待OOM通知的進程繼續使用的內存量,單位為字節
memcg OOM處理
當memcg內存使用達到限製且內核無法從它以及它的子組中回收任何內存時,基本上OOM就要發生了。默認情況下內核將會在它的進程(或它的子組裏的進程)中選擇一個使用內存最多的並kill掉,但是我們也可以關掉memcg OOM:
echo 1 > memory.oom_control
但是關掉它以後,所有申請內存的進程都可能會處於死鎖狀態直到有內存被釋放。這個行為看起來沒有什麼作用,但是如果用戶態注冊了memcg OOM通知機製,情況就變了。進程可以通過eventfd()來注冊通知:
1. 以讀方式打開memory.oom_control
2. 通過eventfd(0, 0)創建通知用的文件描述符
3. 將"<fd of open()> <fd of eventfd()>"寫入cgroup.event_control
之後進程采取:
uint64_t ret;
read(<fd of eventfd()>, &ret, sizeof(ret));
read()調用將一直被阻塞直到要發生memcg OOM的時候,這時相關進程將會被喚醒而不需要一直去主動查詢內存狀態。
但是即使進程被喚醒並知道了即將發生OOM,它也什麼都做不了,因為基本所有可能的操作都會分配內存。如果該進程是個shell,因為無法分配內存運行ps,ls或者cat tasks都會被阻塞住。所以一個明顯的問題是:即使用戶態想kill掉一個程序,但是它卻無法獲取所有的進程列表,這是不是很無奈? 用戶態OOM處理機製的目的就是把責任轉移到用戶態,並允許用戶決定後續的操作,而這隻有在內存預留後才有可能。他增加的memory.oom_reserve_in_bytes接口就是可以被進程在OOM時超額使用的內存,但是該進程隻能是注冊了OOM通知的進程。例如:
echo 32M > memory.oom_reserve_in_bytes
那麼向memory.oom_control注冊了eventfd()的memcg進程就可以超額使用32M內存,這可以允許用戶做一些有意義的事:讀文件,檢查內存使用情況,保存memcg進程列表等。如果可以通過其他方式釋放一些內存,用戶態OOM處理也不一定要kill掉一個進程。它也可以隻是保存一些當前信息例如memcg內存狀態,進程使用內存等以便之後debug,並重新開啟memory.oom_control來打開OOM killer。當有內存預留時,寫memory.oom_control就可以成功。
係統OOM處理
如果整個係統內存緊張,那麼就沒有預留內存可供memcg使用,包括root memcg也無法允許進程分配內存,因為內核在頁分配階段就會失敗而不是記賬階段。對於這種情況我們需要另外一種預留機製:內存管理係統需要單獨為用戶態OOM處理機製預留一部分內存,使得它可以在OOM時繼續分配。目前已經有了per-zone的預留:sysctl min_free_kbytes可以為重要的申請預留一部分內存,例如需要回收的進程或者正在退出的進程。用戶態OOM處理的預留內存是min_free_kbytes預留的一個子集。
如果內核決定自己kill掉一個進程,那這些預留也沒什麼意義。默認下係統全局的OOM killer不能被關閉,而這組patchset允許我們像關閉memcg OOM一樣關閉全局OOM,這是通過root memcg的memory.oom_control接口實現的。他增加了一個flag PF_OOM_HANDLER來允許等待OOM通知的進程可以使用預留內存,如果進程屬於根組,那麼內核允許它在per-zone的min水位之下繼續分配。這個flag使用在頁分配器的slow path中,也不會產生什麼性能影響,對於無關進程就隻是多了一條判斷而已。這個設計的另一個重要方麵是統一了全局OOM和memcg OOM處理的接口,用戶態OOM處理也不需要根據它是屬於根組還是子組而改變使用方法。
當前狀態
2014 LSFMM上有個slot討論了用戶態OOM處理的進展,也有幾個問題。Sasha Levin說為啥不用現成的vmpressure機製,當內存開始回收的時候就獲得通知而不是等到要OOM時才期待用戶態處理,David說vmpressure機製跟這個不一樣,我們無法知道進程消耗內存的速度有多快,係統可能快速從"內存充裕"狀態迅速變成OOM狀態,使得用戶態根本沒時間反映。另外一個焦點在於用戶態OOM機製對全局OOM的處理,Michal Hocko認為改變全局OOM的處理可能從長期看很難維護,我們應該想辦法優化現有的OOM機製而不是直接替掉它。但是Google的一個工程師Tim Hockin說改變或升級內核是一個很緩慢的過程,而且很多OOM策略放在內核態也不太合適,所以他們希望能把主動權交給用戶態,讓用戶態程序決定OOM策略而不用更換內核。另外一個大家覺得有爭議的地方是在memcg的框架下改變全局OOM的處理,這個機製隻能在cgroup被編譯進內核時才能使用,但是目前仍有很多用戶不cgroup。Peter Zijlstra建議是否可以在/proc下為全局OOM再建一個接口,這樣就可以不用依賴memcg。
目前社區對這一套patchset還是不太買帳,對之後的開發方向也沒有一致的結論,短期內也應該不會被合並。
PostgreSQL pain points
Linux內核必須讓所有的workload都最大限度的優化,但是實際上卻並非如此(這也是阿裏內核組存在的意義,嗬嗬),PostgreSQL就是這麼一個例子。在今年的LSF上PostgreSQL的開發者Robert Haas, Andres Freund和Josh Berkus給內核開發者分享了一下他們的痛點。PostgreSQL在1996年發起,並運行在很多不同的操作係統上,所以他們隻能使用進程,並用System V共享內存來做進程通信。另外PostgreSQL保存著自己的buffer cache,同時也用操作係統的buffer I/O來做磁盤數據的讀寫。這種使用方式給PostgreSQL的用戶造成了很大的麻煩。
Slow sync
PostgreSQL遇到的第一個問題是從buffer cache刷數據到磁盤。PostgreSQL使用自己的journal模式Write-ahead logging,任何修改先寫日誌,當日誌落盤以後,數據庫的修改才會落盤。這些邏輯都是由checkpoint進程來實現,它先寫日誌再刷盤。日誌的寫入量很小並且都是順序寫入,所以性能不錯,太慢現在很滿意。問題主要是出在第二步:數據庫修改的落盤。checkpoint進程會盡量安排分階段寫入以防止阻塞整個IO,但是一旦它調用fsync,就會導致所有的寫入被推到設備的請求隊列,由於它一次寫入了太多的數據,導致其他進程的讀請求也被block住了。
Ted Ts'o問了一個問題:如果我們限製住checkpoint進程所能占據的磁盤帶寬是否有意義,Robert反饋說這樣不行,因為當沒有競爭時checkpoint進程還是希望利用整個磁盤帶寬。又有人提議用ionice(這個目前隻能用在CFQ上),但是這個對fsync()發起的I/O不起作用。Ric Wheeler建議PostgreSQL的開發者應該自己控製好寫入數據的速度,Chris Mason補充說O_DATASYNC應該會有用,但這裏的問題是需要用戶自己知道磁盤的能力... 於是乎討論又回到了I/O優先級。可惜很悲劇,如果用戶已經一下子發了大量的數據到設備層並占滿了設備隊列,現在即使進程有很高的I/O優先級也沒戲,因為隊列已經滿了。現在似乎沒啥好辦法了...
由於沒啥結論,囧,最後Ted問PostgreSQL的開發者是否有一個很容易複現的程序,這樣內核開發者可以自己來測試不同解決方案的效果。
Double buffering
PostgreSQL既需要控製自己的buffer也需要用buffer I/O。 這會導致一個明顯的問題:數據庫的某些數據會在內存裏麵存兩份:一份子PostgreSQL的buffer裏麵,一份子操作係統的page cache裏。這會增加PostgreSQL的內存使用,並降低了係統性能。大部分的重複都是可以被做掉的,比如說在PostgreSQL裏麵被修改過的buffer肯定會比內核裏麵page cache的內容新,這些page cache後麵會在PostgreSQL刷buffer的時候被更新。這樣保存這部分page cache就沒啥意思了,應該用fadvise(參數是FADV_DONTNEED)來通知內核移除這些內存。Andres提到不知道什麼原因,這麼做反而會導致一些讀的I/O,這個明顯是內核的一個BUG。madvise他們也用不了,因為先要把文件map上來太麻煩了,速度也不行。當然,這個問題也能反過來解:把內核的page cache留著,而把PostgreSQL裏麵沒修改的buffer釋放掉,這可能通過一個係統調用或者某種特殊的write操作來實現,但是這個沒有達成最終的結論。
Regressions
PostgreSQL用戶遇到的另一個問題是新內核經常導致性能問題。比如透明大頁,這個不但沒給PostgreSQL帶來幫助,反而顯著得降低了性能,囧。另外似乎內存compaction的算法雖然花費了很長時間,但是卻沒有生成很多大頁,最終把透明大頁的支持關閉掉以後整個世界清靜了。Mel Gorman指出如果內存compaction影響了性能,那就應該是個bug。而且他自己本人已經很久沒有看到關於透明大頁的bug匯報了,不過他還有一個patch可以限製compaction使用的cpu時間,這個patch目前沒有被合並,因為沒人發現compaction有問題。不過既然現在PostgreSQL發現了問題,可能後麵需要重新考慮合並這個patch。
另一個大問題是"zone reclaim"這個特性:kernel在其他zone裏麵有內存的時候仍然僅僅在這個zone裏麵回收內存頁。zone reclaim降低了PostgreSQL的性能,所以已經被默認關閉。Andres同時提到他經常由於這個事情去幫別人診斷係統,這個對他的收益有幫助,不過最好還是希望內核把這個解決掉。Mel解釋說當時加入zone reclaim的假設是所有的程序都能在一個zone裏麵,這個假設目前看沒啥道理,所以最好把這個關掉。
最後,PostgreSQL開發者指出內核的升級很恐怖,每個內核版本的升級所導致的性能差距沒法預期...有人提議說能否找出一個PostgreSQL的benchmark來測試新內核,但是這個最終沒達成一致。總得來說,內核開發者和PostgreSQL開發者對這次會晤都很滿意。
Facebook and the kernel
今年的LSF summit btrfs的開發者Chris Mason闡述了Facebook是如何使用Linux內核的。他和大家分享了一些facebook內核的數字以及facebook的一些痛點。這些痛點有些和PostgreSQL開發人員遇到的類似,其實也有好些和阿裏巴巴也類似,可見大家都是一樣的苦逼呀!
facebook的架構大概是這樣的:前麵有一個web接入層,主要是CPU和網絡是瓶頸,用來處理用戶請求。後麵是一個memcached層來cache MySQL的查詢結果。再後麵是一個存儲層,主要包括MySQL, Hadoop, RocksDB等。 在facebook任何人可以查閱任何代碼,facebook.com一天更新代碼兩次,所以新的代碼可以很容易被用戶用到。代碼變化如此之快,所以如果有性能或者其他的問題,內核開發人員可以通知其他開發人員"你錯了",並給出可能的解決方案。
Facebook內部有很多個內核版本,最多的是基於2.6.38的內核,另外一些運行3.2 stable+250patches的版本,還有一些運行著3.10 stable+60patches的版本。這些patches大部分都集中在網絡和跟蹤子係統,還有少量的內存管理方麵的patches。讓Mson很驚訝的是Facebook生產環境對錯誤的容忍性很高, 他舉了一個3.10的pipe bug,這個是一個小race,但是Facebook每天會遇到500次,但是用戶卻根本感知不到。
Pain points
Mason曾經在Facebook內部問了很多人他們認為內核最嚴重的問題是什麼,最終有兩個候選答案:stable pages和CFQ。不過btrfs也有stable pages的支持(囧),而且Facebook現在還有Jens Axboe,CFQ的開發者,嗬嗬!另外一個問題是buffer I/O的延遲,特別是對那些隻會追加寫的數據庫文件。很多時候這些寫入很快,但是有時候卻非常慢,如果內核能解決這個問題就完美了。他還提到了把內核的spinlock移植到用戶態。Rik van Riel提到有可能POSIX locks可以使用自適應的lock,先自旋一陣子再進入睡眠。memcached層有一個用戶態spinlock的實現,但是Mason認為這個實現和內核比起來還是太簡單了。
精細化的I/O優先級控製也是Facebook的想做的地方:一些做清理工作的低優先級I/O線程往往會阻塞前景的高優先級的I/O線程。有人問Mason需要什麼樣的優先級才能滿足需求,似乎聊下來Facebook也就需要2個優先級:一個低一個高。這時又有人提到了ionice,不過這個解決方案隻能在CFQ上用(CFQ已經被Facebook disable了),Bottomley於是乎建議讓ionice針對所有的I/O調度器都起作用,這點Mason也表示讚同,不過Ted Ts'o提到了一個可能的問題:回寫線程也要有ionice的設置才能讓整個環境工作。
Facebook的另一個問題是關於日誌的。Facebook記錄了很多的日誌,這些程序需要不停的調用fadvise()或者madvise()來通知內核是否掉不需要的page cache,而這是可以被優化的。Van Riel建議說新內核的頁麵置換算法有可能會有幫助。Mason解釋說其實Facebook倒是不介意這些,不過不停的調用這些係統調用似乎有點多餘了。
Josef Bacik正在對btrfs做一些小的修改從而能夠控製buffer I/O的速率,這個對btrfs比較容易,但是其實對內核其他文件係統來說,這個想法同樣適用。這時Jan提出僅僅限製buffer I/O是不夠的,因為係統中大部分條件下都存在著很多種不同的I/O。Chris對此表示同意,同時他認為這個方案雖然不完美,但至少可以部分解決問題。Bottomley認為ionice實際上已經存在了,我們就應該重用它,並且如果可以讓balance_dirty_pages能夠擁有正確的ionice優先級應該是可以解決90%以上的問題的。
Mason還提到目前Facebook是將日誌文件保存到了Hadoop集群裏麵,但是用來搜索日誌的命令仍然是很簡單的grep,最好能有一個更好的方法來給內核日誌打標記。對於這一點,Bottomley的意見是Mason最好把這個問題拋給Linus Torvalds,對這個建議,大家都隻能嗬嗬了。
Danger tier
盡管3.10已經很新了,但是Mason還是希望使用更新的內核。基於這個目的,他創建了一個新的層叫危險層,他移植了Facebook原來打在3.10上的60個patches重新打到了最新的內核上並部署了大約1000台機器,這些機器屬於前麵提到的web層,這樣他才可以收集到很多新的性能指標。說到這裏,他給大家show了一張請求響應時間圖(數據來源於最近3天抓的數據),從這張圖裏可以看出係統的平均響應時間以及10個最差的響應時間。這樣他就能登錄這些機器查看真正的問題出在什麼地方。這僅僅是一個例子,後麵隨著項目的進展他還會分享更多的信息這樣才能方便診斷新內核的問題並更快的解決它們。
Various page cache issues
Page Cache是文件係統和內存管理係統中的核心組件,它緩存了持久存儲上的大量數據,並對係統的整體性能起著關鍵作用。但目前Page Cache在很多方麵已經暴露出了一些問題。今年的LSF/MM峰會上最先討論的就是和Page Cache相關的幾個議題。
Large drives 32-bit systems
最先討論的議題是由James Bottomley提出的問題是關於32位係統上Page Cache不能尋址超過16TB大小的範圍。由於目前Linux頁的默認大小為4KB,所以最多隻能尋址16TB的磁盤空間。對於這個問題,首先要問的就是真的需要解決它嗎?
盡管大多數內核開發者認為沒有必要解決這個問題。但是實際情況卻並不是這樣。隨著嵌入式設備的流行,特別是以ARM處理器為核心的設備的大量使用。用戶使用超過16TB空間設備的機會大大增加。而這一問題也變得日益嚴重。
Dave Chinner則指出,這個問題的核心是Page Cache。如果用戶程序使用Direct I/O來訪問磁盤,由於Direct I/O會跨過Page Cache,因此沒有尋址範圍的問題。另一個問題是udev會使用buffered I/O來映射磁盤,因此即便應用程序進行了修改,仍然不能解決這一問題。此外,相對應的用戶態程序也存在問題。比如在32位的機器上用戶無法使用fsck程序檢查超過16TB的文件係統。因此對於32位係統尋址範圍問題的解決會涉及整個存儲應用的方方麵麵。
最後的一致意見就是,這個問題不解決。
Large pages in the page cache
接下來Dave Chinner和Christoph Lameter主持討論了關於大物理頁的議題,即在Page Cache中存放更大的物理頁。這兩個人為了解決不同的問題而同時希望能夠擴大目前的頁大小,而他們采用的解決方法也不一樣,不過最終的效果確實擴大了Page Cache中物理頁的大小。
Christoph擔心的是他的解決方案的性能開銷。盡管更大的物理頁會減小內核在管理內存頁和進行I/O操作時的開銷。但是他的解決方案是在分配內存頁的時候添加一個Order屬性來分配不同大小的內存頁。這樣做帶來的問題就是如何解決內存碎片化的問題。
對於上述內存碎片問題的解決方法,Christoph說可以專門為大物理頁保留一些內存頁,但是這一做法比較“惡心”。另外一種解決方法則是允許內核移動內存頁來消除內存碎片。而且移動內存頁的大部分功能在目前內核中已經實現了。目前的問題是有些內存頁是不能被移動的,而這些不能移動的內存頁會破壞大物理頁。很明顯,這又是個死胡同。
內存頁有很多原因不能被移動。比如內核中通過slab分配的對象。Christoph宣稱已經有補丁解決slab中分配的對象不能被移動的問題。但是該補丁還沒有被合並。此外,仍然有一些反對的聲音,比如這些對象能夠移動的前提是所有指向它的指針都已經被修改。Christoph則認為這些問題都可以通過一些數據結構來加以解決。
此外,Mel Gorman指出上麵的問題其實也還好。真正的問題是如果通過內存頁的移動來解決內存碎片的問題,那以後出現的問題很可能煽了各位內核開發者的臉。因為現在透明大頁在使用中也會遇到分配失敗的情況。所以問題遠沒有想象的簡單。
Larger blocks in filesystems
Dave Chinner希望擴大內存物理頁大小的原因是希望突破文件係統塊大小4KB的限製。顯然通過連續多個物理頁是可以滿足他的需要的。這樣做的另一個好處是文件係統基本不需要修改。但這意味著修改需要在內存管理代碼中完成。所以他隻不過是把文件係統的事情拿到內存管理部分來做罷了。
而在擴大物理頁上,主要的限製就是通用塊I/O棧的限製。此前,單個I/O請求最大大小很長一段時間曾經困擾著快速設備的用戶。由於這一大小太小,造成快速設備性能上不去。盡管這一問題目前已經被解決,但是還有其他問題。由於Page Cache目前假定文件係統塊大小,所以不會記錄文件係統級別的信息,因此文件係統需要自己來處理塊大小和內存頁大小不一致的信息。這樣就造成了目前文件係統磁盤塊大小不能超過內存頁大小的原因。
Nick Piggin曾經嚐試過來解決這一問題。並且從概念上也證明了其代碼是可以工作的。但是他的解決方法存在的問題是需要修改所有文件係統。因此Dave選擇另外一種方法來解決目前的問題。即不再通過Page Cache來維護內存頁和文件係統之間的映射關係。這樣就可以解決掉目前的問題。
這一方案的一個好處是徹底廢棄buffer_head這一結構。此前buffer_head維護著內存頁到磁盤塊的信息。而新的方案改用類似於extent的結構來跟蹤內存頁到磁盤塊的映射關係。這樣就大大簡化了此前的代碼。
另外很多開發者提出ELF文件格式的問題。因為ELF格式中有很多對齊規範,而文件係統的磁盤塊增大後可能會破壞對齊。但是這一問題除了32位係統外似乎並不嚴重。
最後的結論就是大家比較支持Dave的方案。
Support for shingled magnetic recording devices
今年LSF/MM上比較重要的一個議題就是SMR硬盤。SMR硬盤是下一代磁盤存儲,而它由於具有一些比較特殊的特性而對目前的文件係統、存儲帶來了挑戰。
目前SMR硬盤分為三種,即設備管理,主機感知和主機管理。設備管理類型和此前傳統磁盤的使用方法一致,由磁盤自己來對磁盤空間進行映射。但是這樣的結果會帶來性能下降(與現在Flash設備的擦出類似)。這種類型的硬盤由於和傳統磁盤在使用上沒有區別,所以不是討論的重點。討論的重點自然放到了主機感知(係統需要遵循磁盤的區域來進行操作)和主機管理(主機需要主動根據磁盤的區域來進行操作)兩類上。
SMR硬盤在內部會分成兩個區域,分別是可以隨機讀寫的區域和隻允許順序讀寫的區域。在隻允許順序讀寫的區域,會有專門的指針來記錄當前的寫入位置。如果寫請求要求寫入的位置與當前指針位置不一致,會導致磁盤返回IO錯誤(主機管理)或者磁盤會將該磁盤空間重新映射到別的區域(主機感知)。而重新映射操作會帶來很大的延遲。
目前已經定義了兩個新的SCSI命令來查詢磁盤內部的空間區域和重置記錄信息的指針。為了獲取更好的性能,SMR的磁盤驅動需要配合磁盤盡量進行順序寫(磁盤內大部分空間是隻允許順序寫的空間)。如果驅動程序違反這一規範,驅動器會返回錯誤或者進行重新映射。大部分內核開發者認為最有可能主機感知類型的SMR硬盤會更快的被廣泛使用,因為這類磁盤在目前的係統上還可以使用。
目前T10正在製定SMR硬盤的相關標準和規範。不久這一規範就會被正式確定。Ted指出目前就可以通過T10的網站來獲取這一草案。
隨後便是對草案內容,特別是接口協議的一些討論。目前最大的問題是大部分開發者手裏並沒有SMR硬盤,所以對SMR硬盤的任何評價目前都還是紙上談兵。
Persistent memory
Matthew Wilcox和Kent Overstreet在LSF上談到了內核對persistent memory(不知道這個中文應該咋稱唿)的支持。一直以來都有傳聞說這個東東會有,現在終於我們可以自己買到了(到底哪裏有賣的?我也想買)。Wilcox介紹了現在他為persistent memory做的一些支持工作並爭取一下大家的建議。
Persistent memory的讀寫速度和DRAM一樣快,並且在斷電以後數據不會丟失。為了滿足這個物理特性,Wilcox寫了一個可以直接訪問的塊設備層並把它命名為DAX,這個東東的設計靈感來源於內核裏麵的XIP(execute-in-place原地執行),因為這樣可以避免使用page cache。XIP最初來源於IBM,開始的目的是為了在虛擬機之間共享可執行文件和動態庫,後來也被用在嵌入式係統裏麵來直接從ROM裏麵運行。既然persistent memory和內存一樣快,就沒啥理由要在內存裏麵再放置一份copy了。目前,XIP更像是一個起點,但是實際上還有很多工作需要做。所以Wilcox重寫了它並重命名為DAX。文件係統可以通過DAX驅動直接訪問塊設備並避免使用page cache,聽起來不錯,Wilcox希望DAX能夠merge進入主線並希望其他人能夠review代碼並給予建議。
但是目前DAX還是有很多問題,比如首先現在用戶調用msync()去刷一段內存的時候實際上會sync整個文件以及相應的元數據,這個當然不是POSIX的標準而且Wilcox也準備去fix它。但是很顯然,這個不是僅僅修改DAX就可以工作的(還需要修改msync),Peter Zijlstra在這時提出警告說任何修改sync的動作都有可能導致用戶程序出現詭異的行為,因為用戶並不關心內核應該提供什麼功能,他們往往更關心的是現在內核提供了什麼功能。對於這一問題,Wilcox認為社區應該修複,而不是讓用戶依賴這個看起來明顯有問題的內核行為。另外Chris Mason補充說他也讚成修複msync的錯誤行為,因為這個會讓所有的做文件係統的人很happy,哈哈。
另一個比較大的問題是mmap()係統調用使用的flag MAP_FIXED,它有兩層意思,一個是說將mmap的地址映射到指定的地址上,這個一般大家都知道,另一個隱藏的含義是說需要將mmap過程中遇到的其他頁都unmap掉...這個功能太奇葩了吧,所以Wilcox建議增加一個flag MAP_WEAK,去掉第二個隱藏的功能。
get_user_pages()對於persistent memory無法正常工作,原因很簡單,因為對應每個page沒有一個struct page的對象。由於persistent memory設備容量很大以後會有很多頁,所以沒必要為每個頁浪費64bytes來保存struct page。Dave Hansen正在做一個新的get_user_sg(),他的作用是像其他存儲設備一樣支持scatter-gather list(這玩意不知道咋翻譯,不過做存儲的人都懂的)類型的IO。這個功能對truncate()的支持還有一些問題,因為有可能truncate和get_user_sg存在一定的race,這樣就可能看到髒數據,Wilcox目前的想法也很簡單:當get_user_sg運行時阻塞truncate操作的發生,這就排除了race的條件。對於這一點,Overstreet有不同的想法,最近Overstreet一直在重寫direct I/O,他認為DAX的mapping操作和direct I/O很類似。他目前重寫direct I/O的宗旨就是創建一個新的bio,並幹掉get_block(這個玩意確實是一個太tricky的東東),如果DAX也用這個bio就可以避免和truncate的race了。這時有人質疑如果I/O是bio-based就會讓NFS/CIFS很悲劇,Overstreet反駁說如果我們可以讓buffer I/O架在direct I/O上麵就ok了。Chris Mason也認為如果真搞bio based,對NFS來說工作量應該還好。Overstreet也補充說新的bio僅僅是一個包含了很多page的結構體而已,言下之意也是問題不大。
這個session最後沒啥結論,可能社區還需要先去review DAX和新的direct I/O的代碼然後才能再下結論。
Trinity and memory management testing
Trinity是對內核係統調用進行測試的工具。通過對內核提供隨機數據,Trinity已經報出了大量的內核Bug。Trinity的維護者Dave Jones在2014年的Linux Storage Filesystem and Memory Management大會上解釋了內存管理track技術以及他如何使用此工具發現了內存管理子係統中的Bug。
他首先介紹了Al Viro的想法:利用mmap()創建一段內存,在該段內存的中間unmap掉一個單獨的頁,並且將結果傳入各種係統調用中,以觀察會發生什麼。隨後,他說:“全亂套了”。出現了大量的bug,內存管理社區需要花很多努力去修複它們。但是,他說在3.14-rc7內核中已經沒有什麼問題了,這是一件好事。
Sasha Levin也使用Trinity發現了Linux-next tree中的Bug。這些Bug經常會被引入mainline內核。在合並Linux-next之前,必須要先測試它的穩定性了。
Trinity適合發現那些無人使用的代碼段中的bug。比如Huge page,page migration以及mbind係統調用。在mbind的支持下,所有的調用者都會先通過一個用戶空間的庫來檢查參數的有效性。而係統調用自身並不做此檢查。結果就發現了很多未被修複的bug。
內存子係統中的很多代碼目前沒有簡單的測試方法。Trinity在此方麵提供了一些幫助,但它在內存管理測試方麵還處在初級階段。他今年的後續時間裏會繼續從事此工作。不過,目前Trinity發現bug的速度要遠超過bug們被fix的速度。
Fedora用戶報告了很多關於大頁(Huge Pages)的Bug,它們跟Trinity發現的問題是同源的。用常規手段複現這些Bug很難,因為涉及Java Runtime及諸如此類的應用。但是Dave對此的關注不夠。至少在現在,透明大頁僅僅簡單地關閉了Trinity測試。
Trinity對於crash的複現功能也在不斷發展中。該工具能夠記錄下他所做的一切,但是日誌自身是個很重的操作,他記錄的時間與crash的時間未必一致。很多crash是kernel內部狀態的崩潰導致的。導致crash的操作可能是很久以前發生的。因此,查找crash的原因比較難。
Dave給內存管理的開發者提了2個需求。一個是任何人都能為現有的係統調用增加標記。通過此標記,他可以記錄下何時開始測試。另一個是,他希望更多的開發者能夠使用Trinity。上手有點難,但這不應該是我們拒絕Trinity的理由。
Compressed swap
目前內核社區有許多通過壓縮內存內容來提升內存使用率的方案,其中zswap和zram已經進入了主線內核。這兩個實現的共同點是通過壓縮內存內容換取空間並最終取代swap,不同點也很明顯,zram模擬成了一個特殊的塊設備這樣可以當作swap設備來是使用,而zswap而是通過"frontswap"來徹底避免swap。
Bob Liu有一個session專門介紹zswap以及它的性能問題。zswap通過將需要交換出現的數據頁壓縮以後放置到一個特殊的有zbud內存管理器所管理的區域中。當zbud滿了以後,他會將頁再置換到真正的swap設備中,這個步驟就會牽涉到數據解壓,寫數據到swap設備兩個操作,並會顯著得降低係統的效率。為了解決這個問題,Bob有幾個方案:一個是將zswap做成一個寫透的cache,任何寫入到zswap的page會被同步的寫入到真正的交換設備,這樣經zswap的數據清除會非常簡單,但是基本上和原來的swap設備區別不大了。另一個方案是讓zswap可以動態調整大小,這樣就可以根據需求來變大,但是即使做到這樣,我們也僅僅是推遲了問題的出現並沒有最終解決上麵的問題。
Bob希望社區能夠有一些建議如何來解決這些問題,可惜的是,Mel Gorman卻指出無論是zram還是zswap都沒有一個關於它們潛在收益的係統分析。當人們做一些性能測試的時候,他們一般會選擇SPECjbb,這個並不適合對zram或者zswap做測試,而內核的編譯就更不能算作對內存壓縮係統的測試了。所以其實現在內存壓縮子係統最缺乏的其實是一個合理的性能測試workload,這個才是Bob他們應該關注的地方。
Memory management locking
內存管理子係統作為kernel的一個核心係統,對鎖的競爭非常敏感,2014 LSFMM Summit中有一個由Davidlohr Bueso主持的slot專門討論內存管理中的鎖。其中有兩個鎖的競爭非常嚴重,一個是anon_vma lock,它控製對匿名虛擬地址空間的訪問,另一個是i_mmap_mutex,用來保護address_space結構中的幾個域。這些鎖之前是mutex,但是由於大多是隻讀訪問,因此開發者將其換成了讀寫信號量(rwsems),然而一旦需要修改這些數據結構時就會引起明顯的性能下降。某些應用程序還可能出現"rwsem stealing"的現象,即一個線程插隊搶走了另一個線程正在等待的鎖。Davidlohr說應該在mutex上加上一定程度的spin等待,這樣即使一個mutex是睡眠鎖,線程也可以在獲取它時嚐試spin等待一會以期望它馬上就會被釋放。他說為rwsem的實現加上spin等待可以使所有的workloads性能提升到原來的水平,目前社區沒有人反對合並這些代碼。
對於anon_vma,一個強烈的需求是不使用睡眠鎖。rwlock可以滿足這個需求,但是rwlock有公平性問題。Waiman Long在queue rwlock上做了一些工作,可以解決這個問題並提升性能。Peter Zijlstra說他又重寫過一遍這些patch,公平性問題解決了,但是沒有合適的benchmark來評價效果,而且這些鎖在虛擬係統下仍然有一些問題,但是這並不妨礙代碼合並進upstream。但是Sagi Grimberg說,有些code是在拿到anon_vma lock後需要睡眠的,比如invalidate_page(),因此轉換成無睡眠鎖會帶來新的問題。InfiniBand和 RDMA也有這類需求,它們需要在拿到鎖後進行睡眠。Rik van Riel 建議盡快將相關代碼合並,但是Davidlohr並不認為使用睡眠鎖帶來的性能損耗很嚴重。Peter說對於這些無睡眠鎖的patch應該發給Linus,並詳細解釋可能引起的問題,然後由Linus決定是否合並。
Davidlohr也建議重新開始mmap_sem信號量的討論。mmap_sem保護進程地址空間的很多部分,因此經常被鎖的時間很長,導致延遲增大,而且也在鎖裏處理了太多的工作。他認為如果隻是處理地址空間的一部分區域就沒必要鎖住整個空間,也許我們應該看一下range lock是否可以解決mmap_sem鎖競爭的問題。Michel Lespinasse認為雖然range lock可能有用,但是我們應該先解決mmap_sem鎖太長時間的問題。Rik建議我們是否應該換成per-virtual-memory-area鎖,但是Peter說這個方法以前試驗過,但是結果會從mmap_sem鎖競爭變成另一個"big VMA lock"的競爭。Jan Kara提出了在mmap_sem鎖下MM子係統調用文件係統代碼的一些問題,除了性能問題,這種使用方法也可能帶來lock inversion。他為了解決這個問題研究了一陣,目前已經快ready了,但是還有一些其他問題,例如page fault代碼,但解決辦法還是有的。另一個問題是調用get_user_pages(),這需要調用者先拿到mmap_sem鎖,Jan把調用換成了不需要鎖的get_user_pages_fast()。這種方法在大部分情況下可以工作,但是還有一些比較難處理的情況,比如調用get_user_pages()時mmap_sem被高層代碼鎖住了,Video4Linux videobuf2的代碼就包含了這類難處理的使用方法。另外一個擔心是關於uprobes,它需要在它加載進內存的代碼段中插入斷點,這會導致在mmap()裏
最後更新:2017-06-05 21:31:54