892
技術社區[雲棲]
阿裏內核月報2017年02月
The future of the page cache
持久化內存用得越來越多, 促使了內核的一係列變更, 內核是否還真的需要頁麵緩存呢? 在2017 linux.conf.au會上, Matthew Wilcox先是糾正了數年前的一個錯誤,然後表示, 我們不僅需要頁麵緩存,還要將他的作用將進一步得到提升。
他從他作為微軟員工的時候開始講起,以前他以為不會提及這個。 然後進入主題,內容如下, 計算機就是緩存的世界。隻要緩存都命中,他的新電腦每秒可以執行100億條指令。但是內存每秒隻能跑5億3千萬條cache line,因此緩存未命中就會嚴重影響性能。如果數據沒有緩存到主存,需要從存儲設備讀, 即使是快速的SSD,也會變得很慢。
計算機就是這樣,PDP-11也會因緩存未命中而顯著變慢。更嚴重的是,CPU的發展速度比內存快,而同時內存的發展速度相比存儲也更快。緩存未命中的帶來的性能損失也會越來越嚴重。
頁麵緩存
很久以來,Unix係統都有緩衝區緩存,位於文件係統與磁盤之間,目的是為了緩存磁盤塊到內存中。為了準備這次演講,他回過頭查閱了1975年發行的Unix第六版,並在那裏找到了使用緩衝區緩存的例子。Linux從一開始就有個緩衝區緩存。在1995年發行的1.3.50版本中,Linus Torval做了一個牛逼創新, 頁麵緩存。頁麵緩存與緩衝區緩存的區別在於,它是位於虛擬文件係統(VFS)與文件係統本身之間。有了頁麵緩存,如果所需的頁麵已經存在,則根本無需調用文件係統的代碼。起初,頁麵緩存和緩衝區緩存是完全獨立的,在1999年,Ingo Molnar統一了它們。現在,緩衝區緩存仍然存在,但是內容是指向頁麵緩存。
頁麵緩存有非常多的內置功能。明顯的如通過給定的索引查找頁麵;如果頁麵不存在,則創建並選擇從磁盤填充。髒頁可以刷回磁盤,頁麵可以被鎖定,解鎖,以及從緩存中刪除。線程可以等待頁麵狀態的變更,同時有以給定狀態搜索頁麵的接口。頁麵緩存也能夠追蹤與持久化內存相關的錯誤。
頁麵緩存內部處理鎖機製。在內核社區,應該在哪層之上處理鎖存在分歧,但是內部實現鎖是肯定的。當頁麵緩存被修改時,有個自旋鎖以控製其訪問;而查找則通過無鎖的讀-拷貝-更新(RCU)機製來處理。
緩存是預測未來的藝術,他說。當緩存增長太大時,各種啟發算法開始決策哪些頁麵應當被移除。僅使用了一次的頁麵很可能不會被再使用,因此它們將保留在“不活躍”鏈表並相對較快的換出。第二次使用將會把頁麵從不活躍鏈表提升到活躍鏈表。活躍鏈表的頁也會因為超時而被移到不活躍列表。 有一個例外,“影子”條目用於追蹤已脫離不活躍鏈表並已回收的頁麵,這些條目可以延長相對遙遠的過去使用過的頁的生命周期。
一段時間以來,大頁一直作為頁麵緩存的一個挑戰。內核的透明大頁(THP)特性最初隻用於匿名(非文件後端)內存,盡管在頁麵緩存中使用大頁有很多優點。該領域的最開始的工作就是簡單地往頁麵緩存中增加了大量單頁條目,以對應於單個大頁。Wilcox認為這種方法是“愚蠢的”,他增強了用於追蹤頁麵緩存中頁麵的基數樹代碼,以能夠直接處理大頁條目。待提交的patch將使得頁麵緩存可使用單個條目對應大頁。
我們是否仍然需要頁麵緩存?
近期,Dave Chinner預測將不再需要頁麵緩存。他指出,最初由Wilcox創建的DAX係統,支持直接訪問持久化內存,完全繞過頁麵緩存。Wilcox說,“沒有什麼比你的同事懷疑你的整個動機更糟糕”。但也有其他人不同意Chinner,包括Torvalds。Torvalds在一個單獨的論壇指出頁麵緩存很重要,因為在數據訪問的關鍵路徑上,好的東西不是來自低層的文件係統代碼。
在最後,Wilcox深入講了IO請求在DAX係統上如何工作。他在設計DAX原始代碼的時候, 沒有使用緩存。但是這個決定是錯誤的。
在當前的內核中,當一個應用使用類似於read()的係統調用從存儲在持久化內存中的文件讀取數據時,DAX介入。由於請求的數據不存在於頁麵緩存中,VFS層調用文件係統特定的read_iter()函數。這反過來調用到DAX代碼,它將回調到文件係統將文件偏移轉換為塊號,然後查詢塊層以獲取持久化內存塊的位置(如果需要,將其映射到內核地址空間),最終使得塊內容可以被拷貝回應用。
這“不可怕”,但理應以另外一種方式工作,他說。初始步驟應當相同,因為read_iter()函數仍將被調用,同時它將調用到DAX代碼。但是,DAX不是回調到文件係統,而是應當調用到頁麵緩存以獲取文件中所需偏移關聯的物理地址,然後數據從該地址拷貝到用戶空間。雖然這一切都假定信息已經存在頁麵緩存中,但在這種情況下,低層文件係統代碼完全無需介入。文件係統已經完成了這項工作,同時頁麵緩存也緩存了結果。
當Torvalds寫了上述關於頁麵緩存的帖子時,他說:
從鎖定角度來看,這也是一個重大的災難:相信我,如果你認為你的文件係統可以進行細粒度的鎖定,當諸如並發路徑查找的事情到來時,你會生活在一個夢幻世界。
這是“如此正確”,Wilcox說。DAX中的鎖定的確是災難性的。他最初認為可能用相對簡單的鎖定來解決,但複雜性在每個新發現的邊緣場景蔓延。DAX鎖定現在“實在醜陋”,他很抱歉他犯了一個錯誤,認為可以繞過頁麵緩存。現在,他說他必須去解決。
未來的工作
他想重新考慮文件係統塊大小大於係統頁麵大小的想法,這是人們多年想要的東西。現在頁麵緩存可以處理多個頁麵大小,應該是可行的。“一個簡單的編碼問題”,他說。他正在找尋其他感興趣的開發人員一起來做這個項目。
巨大的交換條目也是感興趣的領域。我們在內存中有大量的匿名頁麵,但當換出時,他們被分解成正常頁麵。“這可能是錯誤的答案”。目前有提升交換性能的工作,但需要重新調整以保持大頁在一起。這可能有助於交換到持久化內存的關聯想法。持久化內存交換空間中的數據仍然可以被訪問,因此將其留在那可能是有意義的,尤其是沒有被大量修改。
The Machine: Controlling storage with a filesystem
HPE的新機器:The Machine HPE即HP Enterprise。之前的LinuxConf上,來自HPE的Keith Packard向大家展示過他們正在開發中的名為The Machine的新架構機器。而本次2017的北美LinuxConf上,Keith Packard著重展示了這個新機器中存儲係統管理方麵的一些特點,和The Machine本身一樣,這一類的係統往往是幾個新主意加上若幹早已存在的成熟技術拚接而成。
簡單介紹一下The Machine(詳細的介紹可以直接看https://lwn.net/Articles/655437/, A look at The Machine)。The Machine差不多是一種針對傳統服務器的Rethink。基礎想法是傳統服務器是以cpu為中心的,把數據從各種外存零散地傳送到分散在處的cpu上加以處理,而The Machine則是要以數據為中心----在拓撲結構的中央放置一個非常高速的龐大持久化內存(計劃中一台機器最終會達到300TB內存,目前的實現全是DRAM,未來要切換到真的persistent memory上)。龐大內存的內部靠光通信同步,因此嚴格從物理意義上看它顯然還是NUMA的,但不同node之間的速度差異已經足夠小,程序員可以認為主存速度是均一的,即UMA。龐大的主存區域外圍環繞著大量cpu,目前用的是ARM64,一台The Machine計劃裝80個ARM64。
由於每一個cpu都具有通過Load/Store指令直接訪問持久化主存的能力,傳統的文件係統和磁盤、塊設備抽象就不再需要了,每一個cpu都直接麵對自己要處理的數據的唯一拷貝。Packard把這種設計叫做”memory-drivencomputing“,內存驅動計算。
The Machine上的眾多cpu們對於這片大家共享的主存區域有各種管理需求,其中比較重要的一點是The Machine的各個cpu不被認為是可信的,因此它們每個人能訪問到的主存區域都各有限製。Packard承認最初他完全從頭設計了一套API,但很快就意識到這個API和傳統的POSIX式文件接口非常相似。索性就直接用文件係統抽象來管理這個龐大的主存區域了。
於是Packard用FUSE實現了一個使用POSIX文件式風格管理內存的係統,稱為LFS(LIbrary Filesystem),它把主存分成若幹8GB片來管理,目前每台The Machine有300TB內存,所以一共也就是四萬左右的分片需要照顧,元數據量 並不大。談到分片,Packard也承認可以用LVM2來管理它們,但是從頭設計一個LFS有額外的好處。比如各種接口比起用lvm tools來要簡潔得多:touch文件意味著建一個新的volume set、fallocate意味著要做實際的空間分配了,等等。
LFS直接麵對主存,下邊不再有塊設備等等的抽象。這使得它不能使用一些傳統的組件,比如軟RAID。Packard認為這可以接受:軟RAID在直接讀寫持久化內存的場景下沒有需求。使用文件係統抽象的一大挑戰是:你要如何表示那些傳統上通過NUMA API來控製的東西,例如程序員知道這個文件是放在主存上的,也知道主存是NUMA的,他想指定這個文件一定要放在某幾個結點上。POSIX API沒有這樣的語義,Packard選擇了使用XATTR來表達這些需求。
https://github.com/FabricAttachedMemory 提供了一個The Machine模擬器供有興趣的讀者進一步深入研究。
kvmalloc()
內核提供有兩種基礎的內存分配機製,一種是slab分配器,用於在內核自己的地址空間分配物理地址連續的內存,使用這種分配器的典型代表是kmalloc。另一種是vmalloc,用於在一個獨立的地址空間分配虛擬地址連續但物理地址可能不連續的內存。
slab分配器幾乎是大部分內存分配首選,在沒有內存壓力的時slab分配器不用修改地址空間從而更加快速。slab分配器最適合的是小於一個物理頁大小內存的分配;當內存碎片化之後,物理地址連續的內存頁會變得很難查找,係統性能也會由於分配器要不斷的合並出物理地址連續的頁而變差。
vmalloc分配的內存不要求物理地址連續,因此在內存緊張時更容易成功。但多方麵原因也導致過度使用vmalloc會有很多阻礙:1)每次vmalloc分配完成需要更新頁表,並刷新tlb; 2)vmalloc隻能夠分配整個物理頁,因此也不適合小內存分配;3)在32位係統上,vmalloc分配的地址範圍是有限的(但是在64位係統上目前已經沒有這個限製)。
內核裏麵有很多地方必須要求分配物理地址連續的大內存,但更大部分其實並不沒有這個要求。對於不要求物理地址連續的分配請求,其實並不關心是使用kmalloc或者vmalloc分配,隻要能夠分配就ok。對這一類的請求,可以先嚐試從slab分配器分配,如果失敗再回退到使用vmalloc。內核裏麵也確實有很多不一樣的代碼在做這同樣一件事。
然而,就像Hocko指出的一樣,這其中一些代碼很有"創意",但很多代碼並不如他們期望的一樣工作。考慮如下代碼:
memory = kmalloc(allocation_size, GFP_KERNEL);
if (!memory)
memory = vmalloc(allocation_size);
這段代碼的問題在於,對於小內存(小於等於8個物理頁)的分配,kmalloc會一直重試,而不是返回失敗。這種情況下,回退到vmalloc的這段代碼並不會執行。更糟糕的是,kmalloc為了滿足分配請求甚至可能調用oom killer殺死一些未預料的線程。kmalloc的這種機製在某些情況下確實是有必要的,但是上麵這段代碼,這個並不在這些情況裏麵。
這就需要有一組接口能夠使用這種回退機製,又同時能夠最小化這種回退機製帶來的影響。最終Hocko的一組patch增加了以下幾個新接口函數:
void *kvmalloc(size_t size, gfp_t flags);
void *kvzalloc(size_t size, gfp_t flags);
void *kvmalloc_node(size_t size, gfp_t flags, int node);
void *kvzalloc_node(size_t size, gfp_t flags, int node);
正如大家期望的,kvmalloc首先嚐試從slab分配器分配內存,通過使用__GFP_NOWARN和__GFP_NORETRY標誌盡量減小在不能立即分配到內存情況下的影響(也避免調用oom killer)。如果從slab分配器嚐試分配內存失敗,kvmalloc會回退到用vmalloc分配。kvzalloc會在分配到的內存返回之前進行清零操作;_node結尾的接口用於從指定NUMA節點分配內存。和其他內存分配函數一樣,這些函數也依舊可能失敗。
這組函數使用上有以下兩點需要注意:1) 使用這組函數分配小於一個頁的內存沒有任何意義,回退路徑裏麵的vmalloc並不支持小於一個頁內存的分配,因此回退路徑在這種情況下會失效;2) 這組函數在原子上下文中工作會有問 題,原因是由於vmalloc不能在原子上下文中調用。
曆史上Changli Gao在2010年的時候也提交過一個版本的patch嚐試在內核裏麵增加kvmalloc,但是由於沒有考慮到這些不可預期的負麵影響,並沒有被合並。Hocko似乎找到了讓這組patch進入主線的方法,最終讓這組實現進入了內核
Last-minute control-group BPF ABI concerns
主線4.10中一個稱之為cgroup-BPF的特性被合並,這個新的特性可以將一個BPF(Berkeley Packet Filter)程序附加(attach)到cgroups中,該BPF程序可以通過cgroups中的進程對其接受和發送的包進行過濾。該特性就本身而言並沒有太大的爭議,並且直到最近,其接口和語義仍然沒有較大爭議。但自從這個特性被合並,出現了一些不同的聲音。開發社區可能不得不決定是否對其進行調整,或者在4.10發布之前臨時關閉這個功能。
相關問題的討論最早是由Andy Lutomirski發起的,第一個問題是:bpf()係統調用被用於附加(attach)一個程序到cgroups,他認為這個係統調用從根本上來說是個cgroups操作而非BPF操作,所以應該通過cgroups的接口進行處理。如果將來其它開發者引入其它類型的程序(非BPF程序),那麼仍然沿用bpf()接口將會失去其本來的意義。不管怎麼說,他認為bpf()並不是一個足夠靈活的係統調用。
這個異議並沒有引起廣泛關注,看起來並沒有多少開發者對添加其它的包過濾機製感興趣。BPF的開發者Alexei Starovoitov認為其他的機製也可以很容易的基於BPF實現。網絡的maintainer David Miller在這個問題上完全同意Starovoitov,所以對於這一點來說,不會有太大的改變。
下一個問題牽扯的更深一些。Cgroups是天然的層級結構的,對於version 2的cgroups接口,用戶期待控製器的行為是層級管理的。控製器規則一般來說會下沉到層級的下一級,比如:如果某個cgroup被配置為CPU占用率為10%,那麼其子組被配置為占用50%,這意味著其50%是基於其父親組的10%的一半,也就是說係統絕對資源的5%。BPF過濾器機製並非一個完全的控製器,但他的層級管理行為也會受到關注。
如果程序運行在一個兩層cgroups層級中,並且上下兩層均有過濾程序被附加(attach),通常認為這兩個過濾器都是可以運行的,即兩個過濾器的約束條件都是有效的。但事實並非如此,取而代之的是隻有低層級過濾器程序在運行,而高層級對應的過濾器則被忽略。上層過濾器打算阻止某些特定類型的通信,但底層的過濾器卻重載了這些限製,使其上層的限製無法生效。如果所有層級的過濾器設置都是一個管理員完成的,這個語義可能並不會帶來問題,但如果係統管理員希望設置整個容器和用戶名字空間,而容器可以添加自己的過濾程序,那麼這個行為將使得係統級的設置被旁路。
Starovoitov承認這個問題,最差情況也就是針對給定的層級結構,采用一個組合所有過濾規則的程序。但是他同時認為“當前的語義與設計是一致的”,並且說不同的行為可以在未來實現。問題是如果將來更改語義,會引入ABI的更改,這類更改會打破目前在4.10上的語義,是的係統出現問題,這種更改是不允許的。如何以兼容的方式添加新的語義,目前沒有相應的計劃,因此我們不得不假設:如果4.10按照當前的行為發布,未來將沒有人能夠改變它。
其他的開發者(Peter Zijlstra and Michal Hocko)也表達了對這個行為的顧慮。Zijlstra詢問cgroups的maintainer Tejun Heo對這個問題的想法,但並沒有獲得更多信息。Starovoitov確信當前語義沒有任何問題,並且可以在未來不打破兼容的情況下進行調整。
Lutomirski的顧慮則更加模煳。直到現在為止,cgroups都是用於資源控製,附帶的BPF過濾器的引入則改變了這個規則。這些程序可以作為攻擊者運行一些惡意代碼。例如:他們可以在輸入的協助下介入到一個setUID程序,產生潛在的權限問題。有些程序隱藏的有用信息也會被攻擊者發現。
對於現在來說,捆綁一個網絡過濾器程序是一個特權操作,所以暫時並不是個大問題。但一些人試圖使過濾程序工作在用戶命名空間,這將帶來更多問題。Lutomirski提出了“未完成提案”,該提案可以阻止創建“危險的”cgroups,除非將來相應的問題得到解決。
再次重申,降低未來係統的風險,需要在最開始就要施加相應限製,這將暗示這個特性需要在4.10發布的時候被關閉。但是Starovoitov之前同意在安全領域展開工作,他再次重申這些問題會在未來某個時間點完成。
這是到目前為止的所有討論。如果這些討論沒有結論,4.10將要如期發布這個新特性,即便關於ABI和安全的顧慮仍然存在。對於新的API發布時仍然有類似的未回答的問題,曆史上有一些教訓的。這裏給出的結論,僅僅是希望BPF的開發者可以發現和定位語義和安全問題並且不會產生ABI兼容問題。
Making sense of GFP_TEMPORARY
本文主要試圖描述 linux 內核內存分配標記 GFP_TEMPORARY 語義的合理性,以及現狀。
本文翻譯到此也許可以結束了,因為,幾乎沒有人看到 GFP_TEMPORARY 會聯想到背後一絲實際的語義。老實說我以前沒關注過這個 flag,也從來沒用過,居然還被 mainline 了,不過這就是 linux 的世界。
我們都知道,linux 內核在分配內存的時候一般會通過指定 flags (linux/gfp.h) 來告訴分配器如何處理不同情況(比如:是否分配器可以阻塞),那麼像之前的文章裏提到的有些 flags 是 common 的有些是 by design 的。
相比 GFP_KERNEL,GFP_TEMPORARY 僅僅增加了__GFP_RECLAIMABLE,也就是說增加了內存頁可以回收的標記 (顧名思義了),而這之前__GFP_RECLAIMABLE 主要是被 slab/slub 間接標記的 (SLAB_RECLAIM_ACCOUNT)。
為啥僅僅增加 __GFP_RECLAIMABLE 就變成了 GFP_TEMPORARY?Who knows?
討論中有些聲音是讚成 GFP_TEMPORARY 的,我覺得其實不是讚成 GFP_TEMPORARY 而是覺得 __GFP_RECLAIMABLE 會產生更多潛在的可回收的內存分配,在係統整體上有利於滿足高階內存分配。即使這樣,起碼換個名?
最後,GFP_TEMPORARY 即將被 Michal Hocko 在後續的 patch 中拿掉,這下真的唿應了 “TEMPORARY” 。
BTW,大家在實際的開發中,本著對代碼負責,定義變量也是最好能合理的體現它的語義,關於變量名定義的語義表達,也許夠給很多程序員開個專題了。
A pair of GCC plugins
這些年來,很多gresecurity/PaX內核的加固特性由於內核自保護項目的貢獻進入了內核主線中。其中之一是4.8內核引入的GCC插件基礎架構,該特性在內核代碼編譯時引入各種類型的保護。還有其他很多特性引入到4.9內核代碼中,比較重要的是latent_entropy插件。此外最近兩個引入的插件是kernexec來組織內核執行用戶空間代碼和清理從用戶空間拷貝數據結構的structleak插件。
kernexec
攻擊者經常會通過引誘內核執行一些用戶態代碼來攻擊係統。通過這一方式,攻擊者可以以內核權限來執行他自己的代碼。為了防止這個問題的發生,英特爾和ARM的CPU上分別實現SMEP和PAN技術來保護係統。
但是對於那些沒有SMEP的英特爾CPU來說,kernexec可以提供相同的保護功能。一月中旬,Kees Cook提交了第一版kernexec插件的代碼。該插件通過將內核代碼所在地址的最高比特位置位來實現保護功能。所有內核地址空間中的函數地址的最高比特位被置位後,係統在調用函數前會檢查最高比特位是否置位。因為用戶態函數內存地址的最高比特位沒有置位,因此係統就可以發現內核將要執行用戶態代碼,並生成一個通用保護錯誤。
添加內核加固特性後的性能開銷總是被特別的關注。為了優化性能,插件會嚐試優化函數調用和返回指令。在函數調用過程中,使用一個寄存器並進行或運算來實現最高比特位的檢查。在函數返回時,使用btsq指令來檢查返回地址的最高比特位。
Cook特別注明當前的插件還不能支持內核匯編代碼的部分。也就是說匯編代碼仍然可以調用和返回到用戶態內存地址上。
structleak
內核結構(或包含在其中的字段)經常被複製到用戶空間。如果這些結構不進行初始化,它們可能包含一些“interesting”的值,而這些值是存在於內核內存中。如果攻擊者可以安排這些值與內核結構對齊,並將它們複製到用戶空間,最終就會導致內核信息泄漏。cve-2013-2141就是這種類型的信息泄露;這也就促使“PaX Team”(Pax補丁集的作者)創建structleak插件。
Cook還在1月13日發布了該插件的端口到內核郵件列表。它會在函數裏的局部變量結構中查找__ser屬性(這是一個注釋,用來表示用戶空間指針)。如果這些變量沒有被初始化(因此也可能會包含堆棧“垃圾”),插件就會清理它們。這樣的話,如果這些值在某個時候被複製到用戶空間,也不會有內核內存內容的暴露。
PaX Team在補丁發布中也進行了評論,不過大多是建議調整一些插件的文本說明。特別是,Cook已經改變了在Kconfig描述的插件描述。然而,Cook對於那些變化有合理的理由。
此外,一個Kconfig選項用來打開structleak詳細模式的(gcc_plugin_structleak_verbose)不符合PaX Team的標準。需要指出的是,可能會發生誤報,這是因為“不是所有現有的初始化由插件檢測“,但PaX Team對此表示反對:“一個變量要麼有一個構造函數要麼沒有”。但Cook不這麼認為:
正如指出的那樣,在[插件]報告需要初始化變量時有大量的誤報。它並沒有報告說,缺少一個構造函數。 這是對正在發生的事情進行一個務實的描述,因為插件有時在不需要的地方確實沒有必要初始化,那對我來說真的是一個假陽性。
除了選項的問題,正如Mark Rutland指出的,這__user注釋並不是一個真正的跡象用於表明有問題:
對我來說,似乎__user注釋隻能是發生偶然問題的一個指標。我們有__user指針結構,而這些結構永遠將不會被複製到用戶空間,相反我們有這樣的結構,它們不含__user注釋,但將被複製到用戶空間。
他建議,分析 copy_to_user() 的調用可能會有更好的檢測。PaX Team也表示同意,但同時也說,最初的想法是要找到一個簡單的模式匹配來消除cve-2013-2141和其他類似的錯誤。既然錯誤已經消除了,插件是否還有問題還不清楚,但沒有理由不保持它,PaX Team說:“我把這個插件放在這裏是因為維護無需成本,並且替代它(更好的)解決方案還不存在。”
這些都是相當簡單的功能,可以防止內核錯誤被攻擊者使用。從這點來講,structleak可能並不真正被需要,但新的代碼可能引進一個相似的問題,而沒有特定的插件用於解決這些問題。另一方麵,Kernexec有潛力去阻止那些依賴於內核執行用戶空間代碼的攻擊。現在這兩個插件已經存在了一段時間,讓它們進入upstream,這樣就能使發布者開始建立他們的內核,從而使他們手中有更多Linux用戶,這會是一件好事。希望我們會看到有些人使它們很快進入主線。
Unscheduled maintenance for sched.h
在內核的發展過程中,對頭文件的維護遠沒有像C文件那般重視。4.10內核包含18,407個頭文件,隻有不到10,000個頭文件會在特定子係統外部被引用。然而,在內核0.01版本,總共才31個頭文件。以為例,4.10中該文件達到3,674行,還直接引用50個頭文件,而這其中的許多頭文件又會進一步引用其它頭文件。
臃腫的頭文件會降低內核編譯的速度。假如sched.h冗餘1000行代碼,被2500個文件引用,則內核編譯階段需要多處理250萬行代碼,嚴重影響編譯速度。同時,臃腫的頭文件也難以維護。
Ingo Molnar, CPU調度器的主要維護者,決定以sched.h為切入點,開啟精簡頭文件的工作。主要方法是將sched.h中的數據結構和函數接口分類,拆分成多個更小的頭文件。這是一個繁瑣的工作,許多引用sched.h的文件都需要重新引用新的更細粒度的頭文件。整個工作下來,涉及將近1200個文件的修改。
工作雖辛苦,但效果顯著,重新整理sched.h後, all-yes-config kernel build節省了30秒。目前這部分工作由於patch數量多,review不便,還沒有決定在哪個窗口期進入主幹分支。可以肯定的是,將來內核頭文件將會變得更清爽,後續還會對其他臃腫的頭文件進行改造,如<linux/mm.h>等。
Understanding the new control groups API
過去幾年,Linux的cgroup部分經曆了一次重寫,修改了很多API。理解這些變化對於開發者來說非常重要,特別是那些做容器相關的項目的開發人員。 這篇文章將會過一遍cgroup v2的一些新加的特性,這些特性在4.5的內核中已經宣稱達到生產級。這篇文章主要基於作者在西班牙Seville開的Netdev1.1會議上做的一次talk, talk的鏈接見: https://www.youtube.com/watch?v=zMJD8PJKoYQ。
background
cgroup子係統已經與其關聯的控製器計量並管理機器上的各種資源,包括CPU、內存、I/O等等。cgroup子係統以及namespace子係統(出現得比cgroup早且相對更成熟一些)是構成Linux容器的基礎。當前,絕大多數涉及到Linux容器的項目,像Docker、LXC、OpenVZ、Kubernetes等,都是同時基於兩者。
Linux cgroup子係統的開發始於2006,最早在Google開始,由Rohit Seth和Paul Menage主導。最開始,項目的名字叫"Process Containers",但是後來為了避免跟Linux containers造成混亂改名為"Control Groups",現在,所有人都直接簡稱"Cgroups"。
當前cgroups v1有12個cgroup控製器,除了PID控製器之外的其他11個控製器都已經存在了數年之久,PID控製器是由Aditya Kali開發並合並到4.3的內核之中。它允許限製一個control group中創建的進程數量,因此它可以用作反"fork炸彈"(anti-fork-bomb,指一直fork新進程的程序)的解決方案。Linux的最大的PID空間數量大概也就4百萬個(PID_MAX_LIMIT)。按照今天的RAM容量,這個限製很容易並且可以很快被單個容器中的一個“fork炸彈"耗盡。cgroups v1和cgroups v2都支持PID控製器。
這些年來,對於cgroups的實現有很多批判的聲音,因為它有不少不一致性以及混亂的地方。例如,當創建一個subgroup(在一個cgroup中再創建一個cgroup),有好幾個cgroup控製器的實現都會把參數傳遞給他們的直接subgroup,而另一些cgroup控製器卻不傳遞。又如,一些cgroup控製器使用的接口文件(如cpuset控製器的clone_children)出現在所有的cgroup控製器中,卻僅僅隻有一個控製器會用到它。
作為cgroup的maintainer,Tejun Heo自己也承認,cgroups v1有不少缺點:”實現先於設計“,”不同的控製器使用不同的方法“,“有時候太靈活反而成了攔路虎”。在一篇2012年的LWN文章中,這麼說到“cgroup是內核開發者又愛又恨、相愛相殺的一個功能”。
Migration
cgroups v2 在kernel 4.5版本中宣稱已達到生產級別;但cgroup v1的代碼仍然在內核樹中,且v1跟v2都是默認打開的。目前v1與v2允許混合使用,但是對於某一特定類型的cgroup無法同時使用。值得一提的是,在kernel4.6中,已經添加了一個命令行啟動參數(cgroups_no_v1)來強製關閉v1的邏輯,相應patch見: https://lkml.org/lkml/2016/2/11/603.
隻要存在用戶態應用程序繼續使用v1接口,那麼內核裏v1代碼將繼續存在,這可能持續好幾年。好消息是,一些用戶態程序(比如systemd與CGManager)已經開始往v2接口升級。Systemed使用cgroups來管理服務,每個服務都被映射到了一個單獨的控製組,對v2部分支持。同樣,CGManager也是部分支持v2。
用戶通過掛載文件係統來使用這兩個版本的cgroups。過去幾年,v1有一個掛載選項(__DEVEL__sane_behavior)允許用戶使用一些實驗性質的特性,其中的部分特性是v2的基礎功能,該選項在kernel 4.5中已經刪除。比如,v1中使用該選項後會強製使用統一分層模型,這在v2中是默認支持的。__DEVEL__sane_behavior選項與 noprefix, clone_children, release_agent等互斥,後者在v2中已刪除。
v2已經支持了三種cgroup:I/O,memory,PID。CPU cgroup已有相關patch以及郵件列表討論。改進的同時也帶了一些問題,比如v1支持配置同一進程的不同線程至不同的cgroup,v2不支持。這導致了一個問題:如何對同一進程的不同線程分配CPU資源,v2中是無法支持的,因為所有線程隻能屬於同一控製組。好在Heo提交了一個resource cgroup的patch來解決這個問題,可以看做是setpriority()係統調用的一個擴展。
Details of the cgroups v2 interface
這一段主要介紹了 cgroup v2 的接口細節,原文比較囉嗦,翻個大概意思:
首先是掛載方式,大致命令如下:
mount -t cgroup2 none $MOUNT_POINT
重點是 -o 選項沒了,所以你無法指定這個掛載點是什麼類型的 controller,事實上 cgroup v2 是單一層次結構的(single hierarchy),所以無需 -o 選項來指定了。掛載上去之後根組多了三個接口文件:
1. cgroup.controllers - 顯示支持的 cgroup controller;
2. cgoup.procs - 顯示 attach 進來的進程,這一點和 v1 差不多;
3. cgroup.subtree_control - 這大概是和v2的精華接口,因為 v2 不支持 -o 指定 controller 了,需要用這個文件來控製 controller 的開關。隻要運行下麵的命令:
echo "+memory" > /sys/fs/cgroup2/cgroup.subtree_control
echo "-memory" > /sys/fs/cgroup2/cgroup.subtree_control
就可以啟用或者停用對應的controller,而且可以反複操作。
這三個文件既存在於根組也會出現在子組中;另外子組專屬的還有一個新的接口文件,叫 cgroup.events.
這個文件可以看有多少個進程 attach 到了這個子組。文件裏有一個 “populated: value” 項,value 是個 0/1 值,表示這個子組和它的子孫有沒有進程 attach 上去(就是看空閑不空閑)。
子組的創建和刪除和 v1 差不多,都可以通過 mkdir/rmdir 來完成。不通的是由於 cgroup v2 的單一層次結構限製,你隻能創建到唯一的 cgroup v2 掛載點下。
你可以用一些諸如 poll() inotify() dnotify() 等係統調用來監控子組的事件活動,比如第一個或者最後一個進程 attach 的時間等等。這種機製比 cgroup v1 的 release_agent 機製更高效。
每個不同的子組也可以有 controller 獨有的接口文件,比如 v2 memory controller 有一個 memory.events 文件,可以監控OOM事件等。子組創建的時候,這種獨有的接口文件會在啟用了對應controller的子組中出現,比如一個子組啟用了PIDs controller,這個子組就多了兩個文件, pids.max 和 pids.current; 前者用於設定這個子組可以 fork 的進程數上限,後者用於統計當前子組有多少個進程。
下麵就是舉栗子了。
比如說我們跑完下麵幾個命令之後,會出現什麼呢:
mount -t cgroup2 nodev /cgroup2 // 出現一個名為 /cgroup2 的根組
mkdir /cgroup2/group1 // 出現一個子組
mkdir /cgroup2/group1/nested1
mkdir /cgroup2/group1/nested2 // 子組下麵產生兩個新的孫子組
echo +pids > /cgroup2/cgroup.subtree_control
最後一條命令執行完畢之後, /cgroup2/group1 下麵出現了 pids.max, pids.current 兩個文件,因為 cgroup2 啟用了 PIDs controller 支持。
然後我們運行:
echo +pids > /cgroup2/group1/cgroup.subtree_control
就會在 netsed1 nested2 這兩個目錄下出現 pids.max, pids.current 文件。這個故事告訴我們,subtree_control 隻管兒子,不管孫子和其他後代。
The no-internal-process rule
和 v1 不同的另外一點是,v2 隻能在葉子節點上 attach 進程。還是拿上麵的舉栗子,如果你 echo 0 到 /cgroup2/group1/cgroup.procs 會失敗。關於這個規則,更多的討論在 Documentation 裏麵有。
還有一點和 v1 不同的是,一個進程隻能 attach 到一個子組去了,就不會像 v1 一樣結構混亂。接下來文章舉了一個網絡 controller 的例子說明其意義。
Summary
cgroup v2 的開發還在繼續,比如接下來有 RDMA 相關的 controller patch,這個 patch 允許 per-RDMA-device 的資源隔離,這個特性大概很快就能合並了。
文末最後總結性地誇了一下 cgroup v2。
Reliably generating good passwords
Title: 如何生成一個靠譜的密碼
如今生活中,密碼在電子郵件,銀行卡以及其他一些你認為需要安全加密的地方隨處可見。但是對於如何生成一個靠譜的密碼,當前業界還沒有一個標準以及一個被認可的工具。這篇文章將會為你講述什麼是一個靠譜的密碼以及哪些工具能夠生產一個靠譜的密碼。
我們開始逐步意識到當今的密碼有一定缺陷。例如:通常人們使用的密碼會暴露一定的個人機密信息。甚至一些密碼能被很容易推導至另一密碼:假如你的密碼被盜取,其他人也能夠輕易的偽裝成你去獲取/盜用你的重要資產。因此,一些比較大的公司開始逐步意識到單一密碼認證是不夠的。比如Google,現在逐步要求員工(訪問內網)使用手機客戶端進行雙因子認證,或者完全使用雙因子認證方式代替傳統的密碼認證方式。當然上述方式還存在一定的爭議。
傳統的密碼認證方式依舊會存在相當長的一段時間直到另一種更好的認證方式被大家所公認。請大家注意下,英文單詞中的"password","PIN","passphrase"是同一個意思,廣義上都指代使用一段文字信息對用戶進行唯一性校驗。
什麼才是一個靠譜的密碼?
對於不同的人來說,一個靠譜的密碼有不同的定義。我堅持認為一個靠譜的密碼需要同時具備如下幾種屬性:
- 高信息含量:不容易被暴力破解
- 易傳遞:方便使用不同協議在不同人/計算機傳遞
- 易記憶:方便被使用者記住
高信息含量意味著密碼不容易被黑客推導。當你選擇一個密碼時,不管你覺得他有多麼的生僻,然而對黑客來說,卻可能比較容易找到。當黑客想去做這個事情是,你的生日信息,你的初戀信息,你母親的名字,你最近一個暑假在的地方或者其他你能想到的信息,對於黑客來說,根本不是事情。
唯一的解決辦法就是利用足夠的隨機手段去生成一個無法被暴力破解的密碼。考慮到如今已有現成的軟件(hashcat)能夠利用GPU每秒能夠進行數百萬次密碼推算,現在8位數的密碼已經不再安全。使用合適的硬件,離線狀態下一天就能夠破解這樣的密碼。即使最近NIST起草了一份文檔依舊推薦密碼至少8位數,但是我們如今已經被推薦使用12位或者14位數的密碼。
同時,密碼也必須具備易傳遞的功能。一些字符,例如 & 或者 !, 對web係統或者shell係統有特殊的意義,在傳輸的過程中,很容易被破壞。另有一些軟件則明確拒絕這些特殊字符。同樣,含有特殊字符字符的密碼,在人們口語化的傳遞中也極為困難。舉一個極端的例子:一些流行的軟件中,依舊使用數字作為簽名信息的傳遞。
但是,對於"易記憶",那些離散隨機的字符對人們的記憶來說,有太大的成本。正如xkcd上說的, "通過20年的努力,我們成功的訓練了每一個人去使用人類難以記憶的密碼,但這些密碼卻很容易被計算機所推測"。同時他也指出一係列(無關聯)的單詞集合作為密碼比單個單詞同時替換掉其中幾個字符作為密碼要安全的多。
當然,你不需要記住任何密碼。你隻需要把密碼放在一個安全的密碼管理器中(這個點我們將會在另一篇文章中闡述)或者把他們寫下來放在你的錢包中。在有些情況下,你需要的不是一個密碼,而是一個我稱為"token"的東西。或者像Daniel Kahn Gillmor(Debian開發者)在其私人郵件中說的那樣,你需要一個“高信息含量,小巧的,易交換的字符”。一些API使用那些被精心設計的token. 比如有些API使用OAuth認證體係。OAuth會生成由隨機字符串組成的"access toekn",用於API訪問前的鑒權。
請注意,在“token”的設計上,對其中一項屬性我們為何使用“小巧”去代替“易記憶”是為了應對部分係統的密碼策略(比如對位數要求,字符要求),我們需要高效的將高信息含量的種子轉換為合適的長度. 比如,有些銀行隻允許5位數字的密碼,以及絕大部分web網站對密碼的長度有上限要求。"小巧"隻適用於"token", 而不是"password",是因為我假設你僅僅在秘鑰管理,SSH登陸,電腦登陸,秘鑰加密使用"password",因為"password"在上述地方,其長度限製等都在你的控製範圍內。
生成一個安全的密碼
現在,我們來看如何生成一個不易被攻擊的,易於交換以及易於記憶的密碼。"password" 與 "token"一樣,很多時候不能在屏幕上直接顯示出來。這裏描述的密碼生成器都是通過命令行來工作的。密碼管理器通常內置了密碼生成器,但是這些密碼生成器比較難以使用。
我經常使用xkcd漫畫來跟大家解釋什麼才是一個好的密碼,最後,有人把這些來自Randall Munroe建議做成了一個叫xkcdpass的工具:
$ xkcdpass
estop mixing edelweiss conduct rejoin flexitime
在詳情模式,你會看到xkcdpass所采用的編碼量:
$ xkcdpass -V
The supplied word list is located at /usr/lib/python3/dist-packages/xkcdpass/static/default.txt.
Your word list contains 38271 words, or 2^15.22 words.
A 6 word password from this list will have roughly 91 (15.22 * 6) bits of entropy,
assuming truly random word selection.
estop mixing edelweiss conduct rejoin flexitime
請注意,用上述工具生成的15字符密碼含有91位元的信息資訊,如果使用大小寫,數字,符號隨機的方式,那麼則是:
log2((26 + 26 + 10 + 10)^15) = approx. 92.548875
有意思的是,這個很接近15字母base 64編碼的信息含量:因為在base64中,每個字符包含6位編碼,所以最終你會得到90位元的信息資訊。xkcdpass是一款腳本化以及非常容易使用的工具。甚至,你可以自定義詞組列表,分隔符,以及不同的命令行參數。默認的,xkcdpass使用12個字典中2^12個單詞列表,這個設計並不是為了優化密碼生成的速度,而是為了對不同長度的詞元做重新組織。
關於xkcdpass的另外一方是是其“骰子係統”。“骰子係統”利用類似骰子的投擲從一份詞元列表中拿出相因的詞元。例如,你投擲5次骰子“1 4 2 1 4”,你可能會拿到“bilge”這個單詞。通過投擲骰子,你將會得到得到一個既隨機又便於記憶的密碼。
$ diceware
AbateStripDummy16thThanBrock
diceware能夠明顯的改變xkcdpass的輸出,當有人對內置的骰子係統有質疑時,其也可以使用自定義的骰子程序用作與編碼元:
$ diceware -d ' ' -r realdice -w en_orig
Please roll 5 dice (or a single dice 5 times).
What number shows dice number 1? 4
What number shows dice number 2? 2
What number shows dice number 3? 6
[...]
Aspire O's Ester Court Born Pk
diceware基於詞源列表運行,默認的詞元列表用於密碼的生成。默認的詞元列表來自於SecureDrop project。diceware同樣的可以運行在優化重組的EFF詞元列表上,但是此列表默認情況下未作開啟,因為EFF列表是後麵才被加入的。當前,項目開發者們正在考慮把EFF列表作為默認詞源列表使用。
diceware有一個明顯的缺點是在其生產密碼是無法告知使用了多少位元的信息資訊,使用者必須自己去計算。計算結果依賴於輸入的詞源列表:默認的詞源列表每個詞元有13位元的信息資訊,這也就意味著默認的6個詞元擁有78位元的信息資訊。
log2(8192) * 6 = 78
diceware和xcdpass都是比較新的程序。比如在Debian上,最新的發行版才會集成它們。另外,骰子係統需要一些列骰子算法以及詞元列表。假如你需要安裝diceware和xkcdpass,你可以用過pip來安裝他們。如果說上訴對你來說還是太複雜,你可以看下OpenWall的passwdqc,這款程序相對來說比diceware和xcdpass比較老。但是你同樣的可以利用他們來生成易於記憶的密碼以及允許你來設定信息含量的等級。
$ pwqgen
vest5Lyric8wake
$ pwqgen random=78
Theme9accord=milan8ninety9few
由於某種原因,passwdqc限製其生成密碼的信息含量在24至85位之間。而且pwqgen的工具鏈明顯少xcdpass和diceware。以及其4096個詞源列表是固定的(來自sci.crypt, 1997)。
xkcdpass和diceware比較關鍵的優勢是你可以自定義資源列表,這個能夠讓那些基於詞典攻擊的黑客增加一定的成本。事實上,針對這些基於詞組的密碼生成器的攻擊,最有效的方式就是使用字典攻擊。因為密碼足夠長的情況下,按照單詞來猜測會讓攻擊變得不可行。而且對默認詞源列表的更改會給攻擊者帶來成本。講到這裏,其實有一個關於“隱藏式安全”說法。事實上“隱藏式安全”是不安全的。當你使用你母語詞典作為詞源列表,這隻會阻止一部分攻擊者,但是卻阻止不了對你有一定了解的攻擊者。
另外有一點必須注意,一個大的詞源列表隻是擴大了索引空間,所以一個密碼的信息含量隻跟其長度有關,與你選擇用哪個詞源列表無關。換句話說,使用多一份的詞源列表還不如對密碼多增加幾位長度。
Generating security tokens ‘’‘生成安全Tokens’‘’
正如之前提到的那樣,很多密碼管理器支持使用不同的策略生成不易被攻擊的安全tokens。總的來說,你需要使用你的密碼管理器中的密碼生成工具去為你生成token用於訪問你的應用。但是如果密碼管理器不支持秘鑰生成,那該怎麼辦呢?
"pass",標準unix密碼管理工具,使用pwgen來作為密碼生成工具。但是它在安全問題上有著糟糕的表現。雖然"pass"支持所謂的安全模式(-s),但是我也要指出去除這個選項作為默認參數是有意義的。我已經為"pass"做了一個小patch,使其生成的密碼更加安全。如果不使用"pass",我建議使用如下的管道方式來生成密碼:
head -c $entropy /dev/random | base64 | tr -d '\n='
以上命令從內核讀取一個固定長度隨機字節,並做base64編碼。上述結果就是Gillmor描述的"高信息含量,可打印,易於交換的字符串"。重要的是,在這個例子中,我們可以獲取到一個小巧的,有含有高信息量的“token”。雖然在同一時刻,你隻能使用一個字符合集,這個可能導致一些小問題。例如部分網站會對字符做一些限製。Gillmor是"Assword password manager"其中一個維護者,Assword password manager使用base64,因為其能夠被更加廣泛的接受,以及隻比原先的8位二進製編碼多33%空間。經過一段漫長的討論後,"pass"的維護者Jason A. Donenfeld最終采用如下的管道命令:
read -r -n $length pass < <(LC_ALL=C tr -dc "$characters" < /dev/urandom)
這條命令與之前的命令比較類似,除了它使用tr命令直接從內核讀取字符,以及選擇一個基於一個用戶早些配置的固定的字符集。之後,read命令從輸出中抽取多個字符,最後把結果存儲與"pass"變量中。在mail list的討論中,Brian Candler認為:相比base64方式,使用tr命令從/dev/urandom會丟失一定的信息編碼量,但是最後放棄了這個異議點。
另外的密碼管理器,例如KeePass使用自己的方式去生成tokens,但是大致都過程都是類似的:從內核讀取信息編碼源,最終把這些數據轉換為一個能夠被傳輸的字符串。
結論
雖然目前有很多方式做密碼管理,但是我們的關注點在能夠為用戶和開發者生成即安全又有用的密碼的技術上。雖然pwgen軟件暴露了一些安全上的弱點,但是生成一個不易被攻擊且易於記憶的密碼依舊不是一個問題。一旦不使用用戶自己的設備,用戶自己生成的密碼很容易遭受黑客攻擊,特別是那些對用戶背景有相當研究的黑客。所以,向用戶提供能夠生成足夠健壯密碼的工具以及鼓勵他們使用密碼管理器則變得尤為重要。
最後更新:2017-06-07 17:31:26