閱讀988 返回首頁    go 阿裏雲 go 技術社區[雲棲]


阿裏內核月報2014年5月-06月

The initial kGraft submission

長久以來,重啟操作係統來安裝一個內核補丁一直是一個煩人的事情。很多時候,重啟係統的時機會受到其他條件的限製。此外,用戶則更希望能夠在不重啟係統的情況下完成內核補丁的安裝工作。2008年為了迎合這一需求Ksplice誕生了。但它並沒有被合並進主線內核,甚至在Oracle收購其後便消失在Linux開源社區的視線內了。最近其他一些解決方案陸續提交到了Linux內核社區,kGraft便是其中之一。

kGraft是由SUSE的Jiri Kosina和Jiri Slaby兩人共同開發的。該解決方案相比於Ksplice要簡單很多。當然,與此同時也意味著某些功能上的缺乏(比如:向數據結構中添加影子成員)。kGraft的補丁僅有600行,十分簡單。這也意味著使用kGraft後對係統的影響是很小的。

kGraft的工作方式是通過替換掉內核中的整個問題函數來實現內核代碼升級的。通過使用專門的工具,一個開發者可以輕鬆地將一個補丁變為多個需要替換的函數列表並將這些函數編譯為一個單獨的內核模塊。在加載這個內核模塊的時候,kGraft會將已有的問題函數替換為沒有問題的新函數。

函數的替換是kGraft中的核心。對運行著的操作係統內核進行補丁升級時十分危險的。然而好消息是這一問題已經被較為完美的解決了。ftrace也需要對操作係統內核進行類似的操作。為此ftrace的開發者已經完成了類似的函數替換功能,用來調試和解決那些奇怪的錯誤。因此kGraft開發者要做的工作便是使用ftrace機製將問題函數替換為新函數。

另外一個較為重要的難點是如何保證在升級的過程中,沒有進程正執行在問題函數之中。如果這一情況發生了,會造成不可預知的結果。kGraft開發者解決這一問題的方法是保證沒有進程會同時看到兩個版本的函數。

為了解決上述問題,kGraft會在每個進程的thread_info結構中添加一個標記用來追蹤該進程在升級開始後是否離開或者返回用戶態空間。當係統截獲對問題函數的調用後,一個叫做“slow stub”的模塊會去檢測當前運行的進程目前的標記。如果進程進入或者退出內核空間,則意味著該進程運行在有問題的上下文忠,因此需要調用舊的問題函數。否則該進程需要調用新函數。一旦係統中的所有進程都進入到了新的上下文後,“slow stub”模塊就可以被卸載,同時新函數可以無條件的被調用了。

接下來的問題是如果有進程在一定時間內沒有完成上述狀態轉換怎麼辦?舉個例子,一個進程可能花費很長的時間來等待網絡IO。Vojtech Pavlik在今年Collaboration Summit上提到過一個方法,即向這些進程發送信號強製他們轉換當前的狀態。這一機製並沒有包含在目前提交的補丁中。另一種解決方案是在/proc目錄中顯示當前有問題的進程,從而方便管理員的識別。

上麵的問題似乎解決了,那麼內核線程該如何解決呢?我們知道內核線程是不會返回用戶態空間的。大部分內核線程在等待某個時間時,會調用kthread_should_stop()來判斷是否需要退出。kGraft利用了這一點,它通過修改該函數來重置上麵的標記。對於沒有調用kthread_should_stop()函數的內核線程,kGraft會插入一個kgr_task_safe()函數用來標記當前的內核線程是否到達一個適當的狀態。

最後的一個問題是關於中斷的。kGraft解決中斷處理函數替換的方法是定義一個per-CPU數組用來標記對應CPU是否運行在進程上下文。該數據的初始值為false,當schedule_on_each_cpu()被調用的時候,kGraft在其中插入了一個新的函數用來檢查該CPU上的中斷是否都已經進行了處理。在該CPU上存在沒有處理的中斷時,kGraft會讓所有CPU都運行在就的有問題的上下文中,知道所有中斷處理完畢為止。

目前kGraft的補丁已經提交到社區了,社區並沒有特別的反對聲音。這一功能應當說確實是十分有價值的。但是由於存在競爭對手,內核不可能同時合並兩個熱打補丁的解決方案。因此,目前需要有人來合並兩種解決方案,或者需要有人站出來做決定。

The possible demise of remap_file_pages()

remap_file_pages()是一個有些怪的係統調用,它允許在任務地址空間和特定文件之間創建一個複雜的、非線性的地址映射。這樣的映射方式也可以通過多次調用mmap()來完成,但是明顯後者的代價要高一些,因為每次都會在內核中創建一個單獨的VMA(virtual memory area),而remap_file_pages()則隻創建一個VMA就夠了,如果有很多很多不連續的內存映射,這兩種方式的不同點在內核中將變得很大。

據說有很少開發者在使用remap_file_pages(),以至於Kirill Shutemov發布了一個patch把remap_file_pages()完全移出內核,他說“非線性映射維護起來很痛苦,並且,目前64位的係統可以充分的被使用,這時再使用它有些不太合理”。他目前還不打算將這個patch合並了,而這個patch僅表明他提出過這個觀點了。

這個patch吸引之處就是,它可以刪除600多行內核中難以琢磨的代碼。但是如果這樣做造成了某些應用程序無法使用,那麼這些代碼還必須待在內核中。一些內核開發者相信即使remap_file_pages()被移除了,也不會有人注意到的。但去相信一定不會造成應用程序無法使用是不可能的。所以,一些人建議在內核中增加一些警告。Peter Zijlstra 建議增加一個開關來激活remap_file_pages(),如果目前使用remap_file_pages()的開發者自己意識到這個變化的話就更好了。目前討論這些希望可以避免以後的一些麻煩。

The first kpatch submission

正直北半球的春天,一個年輕的內核開發者正在思考著如何給內核動態的打補丁。上周我們看到了SUSE的kGraft動態打補丁方案。隨後Red Hat的解決方案kpatch就呈現在我們麵前了。這兩個解決方案,在某些方麵十分相似,但也有一些顯著的不同。

與kGraft類似,kpatch也是對問題函數進行整體替換。kpatch通過用戶態工具將補丁文件轉換為一個可加載的內核模塊。在該模塊加載的時候,kpatch_register()函數被調用,並使用ftrace機製來截獲對問題函數的調用,並調用新函數。從這裏看,kpatch的工作原理與kGraft十分相似。但是,還是讓我們來看看kpatch的內部細節。

與kGraft使用的複雜方法來保證函數替換的正確性不通,kpatch直接調用stop_machine()讓所有CPU都暫定,雖有kpatch檢查所有進程的棧以確保沒有問題函數在執行。隨後kpatch將問題函數完全替換為新函數。不想kGraft,這裏沒有任何狀態上的記錄,所有進程都一次性的進入到新狀態中。

kpatch目前的方式有一些不足。首先stop_machine()殺傷力太大,內核開發者都在竭力避免使用該函數。此外,如果有問題函數正在被執行,kpatch會直接失敗返回;而kGraft則會等待並且再嚐試進行替換。這就是說,對kpatch來講,那些總是被執行的函數(比如:schedule(), do_wait(), irq_thread())是無法進行替換的。對於一個常見的係統來說,有上千個函數會造成熱升級的失敗。

對於kpatch使用stop_machine()的這一方法,很多內核開發這表達了自己的想法。其中Ingo Molnar支出可以使用進程凍結的方法來確保完全沒有進程在運行狀態,但這意味著大補丁的過程會更漫長一些。Ingo指出如果Linux發行版開始使用熱升級方案,首先要保證的是熱升級的安全,其次才是快速。

kpatch的開發者Josh Poimboeuf則指出內核中有很多不能凍結的線程。Frederic Weisbecker建議使用內核線程暫定機製來代替進程凍結的方案。Ingo則指出無論如何都需要一種機製來保證進程能夠到達一個安全的狀態。目前社區的意見是首先保證安全,然後再提高性能。

另一個問題是關於補丁中對數據結構修改的。kGraft表示可以通過上下文機製來處理簡單的數據結構修改。根據Jiri Kosina的說明,kGraft可以使用一種被稱為創可貼函數的方式來讓內核同時理解新舊數據結構知道所有進程都已經替換為新代碼。在這一過程結束後,舊版本的數據結構就可以被廢棄掉了,但是讀取舊數據結構的函數仍然需要保留。

反觀kpatch這邊,目前並沒有明確提出一種可行的修改數據結構的方法。目前kpatch有計劃提供一種毀掉函數的機製在進行熱升級的過程中對所有涉及的數據結構進行修改。這一方法意味著不需要維護舊數據結構的任何狀態。

目前看,似乎情況還不算太糟糕,畢竟內核補丁中對數據結構的修改並不常見。正如Jiri所說:
根據他們的分析,需要熱升級的補丁幾乎都是非常短小的補丁。這些補丁僅會增加額外的便捷檢查。很多年才可能出現一兩個需要格外處理的補丁。

目前看來思考如何安全的修改數據結構仍然為時尚早。目前的重點是如何找到一種能夠熱升級內核的方法。今年八月份舉行的Kernel Summit上開發者們將會討論這一議題。目前看,大家都認為應該尋找一種可靠地方法來解決目前熱升級的問題。

Braking CPU hotplug

最近的一組patch引發了對CPU hotplug子係統相關的討論。為一個正在運行的係統動態增減CPU有很多需求:硬件上支持物理上的增加和移除CPU,或者需要移除一顆異常的處理器。在虛擬化場景裏麵,CPU熱插拔是一個常見用於在用戶虛擬機運行狀態中動態調整虛擬機處理能力的手段。這個特性無疑很有意義,但是沒有人對CPU hotplug目前的實現感到滿意。

對一個正在運行係統的CPU進行插拔是一件複雜的工作,有大量的per-CPU狀態需要管理。為此,擁有一套完整的機製將很不錯,將這些複雜的工作細分為一個個簡單步驟,同時能確保這些步驟按序執行。但不幸的是Linux內核並沒有這種機製,而是使用了一係列難以修改的通知和回調實現,導致bug很難發現。

事實上在這一塊的bug很多,Borislav Petkov希望讓它們更難發現。他的patch介紹如下:

我們有一群熱心的測試哥們,在對CPU熱插拔不了解的情況下,拚湊腳本勐烈的壓測CPU熱插拔,然後報告他們觸發的bug.

當然,首先,大部分,不是所有的,他們觸發的bug是和CPU熱插拔相關。但是我們知道熱插拔全身布滿了輸管和“棕色紙袋”。

最終我們耗費了大量時間處理一個在最開始就有問題的機製。

他的解決方案很簡單:在每個CPU熱插拔操作前加入1秒的延遲。這樣使得能夠測試的操作數量及操作之間的並發量盡量減少。也能夠漂亮的減少源源不斷的bug報告。

當然,這有一個小小的瑕疵:這個patch並沒有實際上解決任何bug,它隻是將問題掩蓋起來不被發現。Andrew Morton指出這個patch將導致CPU熱插拔的bug解決得更少。Thomas Gleixner認為這也許是一件好事:“如果有人能夠花相同的時間重寫熱插拔這團混亂的東西,我們將會獲得更大的收益。但是可惜沒有,我們更願意為它插上更多的管道或者紮個繃帶”。

2013年2月Thomas曾經試圖重寫,這塊工作在這裏。他花時間將熱插拔操作拆分為一長串離散的步驟,然後建立了一個以定義好的順序運行這些步驟的係統。但是距離完整的解決這個問題還有很遠,大部分已經存在的熱插拔代碼依舊存在,僅僅是調用點不一樣。但是一個隨著時間推移能夠不斷重寫的框架已經提供出來。

唯一的問題是:沒有人做這個重寫。Thomas已經轉移到其他任務去,沒有時間繼續,同時也沒有其他人將這塊工作挑起來,因此這部分patch僅僅有最初始的發布。大量這一塊的bug修複僅僅定位到特定的bug,並沒有全盤考慮這一複雜而且難以維護的係統,事實上他們是的這些問題更加糟糕。

導致Borislav用於延遲熱插拔係統patch出現的原因是不斷的“管道和棕色紙袋子”帶來的挫折。最終,這塊的開發者不希望再有更多的bug修複,他們希望這部分代碼更加簡單而且易懂,他們不希望源源不斷的bug修複確是不斷增加代碼的複雜度。讓這個子係統的bug更難發現對於將開發者的注意力轉移到其他方麵是一個很大的幫助。

如果這個patch能夠被merge,將會讓人驚訝。在這樣一個世界--內核子係統維護者不能強迫開發者在特定子係統領域,同時沒有公司管理者指導他們的員工解決CPU熱插拔的問題--一個人有時需要些創造性才能夠讓事情順利解決。有人也許會希望這組patch能夠給一個足夠強的提示以使得有人能夠在這個問題上繼續解決。不幸的是Thomas已經不經意間破壞這種努力,他說加入沒有其他人重寫熱插拔CPU子係統,他將跳回來自己來做。

Tux3 posted for review

在經過多年的開發和一些錯誤的開始之後,Tux3文件係統已經進入代碼評審的階段,希望它可以在不久的將來就能並入mainline。Tux3開始就提出了一些下一代文件係統的特性和高度的可靠性。提交代碼評審是Tux3的一大進步。但是恐怕距離真正進入mainline還有一段時間。

目前唯一一個進行了代碼評審的開發者是Dave Chinner,而他也沒有感到非常滿意。還有一些工作要做,Dave認為Tux3中的很多對文件係統核心的改變需要單獨評審。其中一個Tux3的關鍵的機製“page forking”,當年並沒有被2013 LSFMM接收,並且Tux3的開發者Daniel Phillips也沒有對這點做出什麼大的改進。

Dave還擔心Tux3提出的一些特性目前正處在開發中,幾年前,btrfs在還處於未完成的狀態下被合並,以希望這樣能促進它的開發。Dave說這種錯誤最好不要再犯了。

btrfs的開發說明了把一個還處於原型的文件係統合並到mainline中並不會促進它加速提高穩定性和性能,事實上它還是單獨開發直到所有特性都基本完成會更好些。急於並入mianline隻會減速它變的功能完善和穩定。

總之,這個文件係統目前會受到冷遇。Daniel麵臨將代碼準備好合並的挑戰,隻有做到了這點才是時候將Tux3合並到內核。

BPF: the universal in-kernel virtual machine

最近關於ktap動態跟蹤係統的討論大多聚焦在添加一個lua解釋器和虛擬機到內核中。 在內核空間運行虛擬機似乎是不合適的。 但實際上,內核已經包含了不止一個虛擬機。其中一個, BPF解釋器已經在特性和性能上都有了發展;它現在似乎正在扮演著 超出初始目的的角色。在此過程中,它也許會導致內核網絡子係統中解釋器代碼的精簡。

“BPF”本來表示“伯克利包過濾器”;它最初是作為一種簡單的語言用於為一些像tcpdump的工具寫包過濾代碼的。Jay Schulist 在內核2.5中添加了BPF的支持。自那以後很長時間,BPF解釋器都是沒有太多變化,似乎僅有一些性能調整和添加了一些訪問 包數據的指令。在內核3.0中Eric Dumazet為BPF解釋器添加了及時編譯器功能。在內核3.4中,“secure computing”被增加 來,以便為係統調用支持來自用戶的過濾器。那種過濾器也是用BPF語言寫的。

在內核3.15中,BPF有了另一個重要的變化。它被分拆成2個變體,“經典BPF”和“內部BPF”。後者將可用的寄存器從2個擴 展到了10個,添加了許多與真實硬件匹配的指令,實現了64位寄存器,使BPF程序調用一組或多組內核函數成了可能。內部BPF 更輕易地編譯成了快速機器代碼並且更容易將BPF掛進其他子係統。

現在,至少內部BPF整個對於用戶空間是不可見的。包過濾和安全計算接口依然接受經典BPF語言寫的程序。這些程序在他們第 一次執行之前被翻譯成內部BPF。這個想法似乎是內部BPF是內核特定的實現,也許隨著時間會改變,因此它將不會很快被暴露 得用戶空間。

內核3.16以後,也許網絡意外的子係統也許也會使用BPF。 Alexei最近提交了一個將BPF用作跟蹤過濾器的補丁。這個改變幾乎 刪掉為可觀地提升性能而添加的代碼。

內核中的跟蹤機製允許一個有合適特權的用戶每次執行遇到特定跟蹤點時能接收詳細的跟蹤信息。正如你想象的,來自某些跟 蹤點的數據可能是相當大的。這就是為什麼需要過濾機製。
過濾器允許將布爾表達式與任何給定的跟蹤點關聯起來;僅當在執行的時候表達式為真,跟蹤點才會觸發。一個例子像下麵:

   # cd /sys/kernel/debug/tracing/events/signal/signal_generate
   # echo "((sig >= 10 && sig < 15) || sig == 17) && comm != bash" > filter

在上麵例子中,那個跟蹤點被觸發僅當特定的信號在跟定範圍內產生並且產生這個信號的進程沒有運行“bash”。

在跟蹤子係統裏,像上麵的表達式被解析且表示成一個簡單的數,它的每個內部節點都表示一個操作碼。每次跟蹤點被遇到, 將會遍曆那個樹,用此時的特定數據來評估每個操作。加入結果在樹頂是真的,這個跟蹤點將被觸發並且相關的信息將被發出。 換句話說,跟蹤子係統包含了一個小的分析器和解釋器用於特定的目的。

Alexei的補丁留下了分析器卻去掉了解釋器。代替地,分析器產生的可預測樹被翻譯成一個內部BPF程序,然後丟棄。BPF被 及時編譯器翻譯成機器碼。結果被執行無論何時跟蹤點被遇到。從Alexei發布的benchmark來看,它是值得努力的。大多數 過濾器的執行時間都被減少近20倍,有些更多。考慮到跟蹤的開銷經常可能掩蓋掉跟蹤正試著找的問題,開銷上的銳減是受 歡迎的。

這個補丁集真正是歡迎的,但不太可能被合入內核2.16。它當前依賴其他3.16改變。那些改變被合入了net-next樹;那個樹 沒有正常地用作內核中其他改變的依賴。因此,合入Alexei的改變進入跟蹤代碼樹導致了編譯失敗。

根原因是BPF代碼被深深地嵌入了網絡子係統。但BPF的使用不在僅限於網絡代碼;它正被其他內核子係統像安全子算和跟蹤 使用。因此是時候將BPF移到一個更忠心的位置,以便它能獨立於網絡代碼被維護。這個改變很可能設計到不僅僅是一個簡單 文件的移動。在BPF解釋器中依然有許多網絡特定的代碼需要被重構。那將是個大的工作,但對於一個將要被演進成更通用的 子係統來正常。

在這個工作被做之前,合入那些對非網絡代碼的BPF改變是困難的。因此,為解釋代碼,將BPF作為主要的虛擬機加載進內核, 那將是邏輯上的下一步工作。僅有如此一個虛擬機是有意義的,能更好地調試和維護。對於這個角色,沒有其他可信的競爭者, 因此,一旦它為整個內核使用被重新打包後,BPF很可能將扮演這個角色。在那之後,將會很有意思地看到將會有什麼其他的 用戶出現。

Expanding the kernel stack

每個進程即使退出時,在內核裏都要占用一定數量的內存,盡管占用的數量不大。其中有些內存被用來存放每個進程的內核棧。

因為每個進程可能同時在內核裏運行,因而每個進程必須有他自己的內核棧空間。如果有係統裏有大量的進程,內核棧所消耗的空間

總和也不少,而且內核棧還要求在物理上是連續的,這都給內存管理很大的壓力。這些方麵的考慮,也成為保持小的內核棧的一個強烈動機。

對linux的曆史而言,在絕大多數架構上,內核棧大小是8K,兩個連續的物理頁。2008年曾有些開發者嚐試把棧縮小到4K,事實證明這樣的努力是不現實的。現在的內核函數的調用深度已經超越了4k的棧。

在x86_64係統上,越來越多的調用棧已經無法裝入到8K棧。最近,Minchan Kim追蹤到一個由於棧溢出導致的crash。最為回應,他建議是時候把x86_64係統上的棧大小翻倍,變成16KB了。之前,這樣的建議是被抵製的,這次也是一樣被抵製。Alan Cox爭論說可有其他的解決方法,但是這種觀點比較孤單。

Dave Chinner 經常處理棧溢出的問題。因為XFS文件係統上,更容易發生這類問題。

他非常支持這種改變:

在x86-64係統上,對linux io來說,8K棧從來都不足夠大。但除了文件係統和io開發者,沒有人樂意接受這個觀點,盡管文件係統不得不一次次把棧溢出問題規避掉。

Linus一開始也不相信這個事實, 他澄清說降低內核棧足跡的工作(棧使用的深度?)還得繼續,更換stack大小不是一個可靠的解決辦法。

我基本計劃使用這個patch,我同時還想確認我們確實是修訂了一個我們見過而不是推論出來的問題。

我承認8KB有些痛苦和受限,並且變得相當痛苦。但是我不想當我們有一個深的stack使用的例子,就完全放棄。

Linus也澄清他不會在3.15裏更改棧的大小。但是3.16 merge的窗口近期就會打開, 我們可以期待這個patch。

Seccomp filters for multi-threaded programs

seccomp是一種通過限製進程能夠使用的係統調用來實現應用沙箱的方案。早期的seccomp對進程實行係統調用白名單製度,隻允許進程使用固定的open/close/read/write四個調用,隨後演化成可以使用靈活的過濾器組合(filter),並將過濾器邏輯使用一種類似匯編的特定語言(BPF)寫下來,上傳至kernel space去執行。每一個係統調用,連帶用戶傳來的參數,都會被送到過濾器組合中,各個過濾器可以獨立地決定放行還是拒絕,隻要有一個過濾器決定拒絕,這個係統調用就不會被允許執行。seccomp之類的方案適用於很多PaaS的場景,或者現代瀏覽器的Sandbox,或者移動客戶端上執行受限的應用。

在目前的內核裏,一個進程可以通過一個prctl()係統調用來給自己加上這種過濾器,格式如下:

 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter);

filter是一個指向struct sock_fprog結構體的指針,代表要被加載的過濾器。過濾器一但加載就不能被卸載。一般來說添加過濾器(即使給自己)也需要sudo root,但這裏有一個例外:如果一個進程在添加過濾器前調用 了: prctl(PR_SET_NO_NEW_PRIVS, 1); —這表示它放棄了以後獲得任何privileges的機會,包括獲得新的capability,調用setuid/setgid等等—它就可以給自己添加過濾器。

目前的seccomp實現有一點和cgroup很像:一但一個線程添加了一個過濾器,它以後派生的所有子線程都會繼承這個過濾器。但是之前派生的線程或者兄弟姐妹們則完全不受影響。考慮到有時候我們不太容易去修改一個線程組中第一個被創建的那個線程,如果可以提供一個接口說:現在添加的過濾器要應用到我所在的進程中所有的線程上,而不僅僅是給我自己,則會方便許多。Kees Cook提的就是這麼一個patch,他引入了一個新的接口

  prctl(PR_SECCOMP_EXT, SECCOMP_EXT_ACT, SECCOMP_EXT_ACT_FILTER, flags, filter);

如果flags傳了0進去,這個接口的行為就和剛剛說的的PR_SET_SECCOMP完全一樣,如果傳了常量SECCOMP_FILTER_TSYNC進去,新建的這個過濾器就會被應用到整個線程組上去。

另外,Kees Cook還添加了一個接口:

 prctl(PR_SECCOMP_EXT, SECCOMP_EXT_ACT, SECCOMP_EXT_ACT_TSYNC, 0, 0);

這個接口的語義是說:不添加新的過濾器,而是把我自己這個線程已有的所有過濾器都添加到線程組中的其他線程上去。
通過以上兩套新接口,seccomp對線程組的支持增強了。我們最早有望在3.16看見這套patch被合並。

Locking and pinning

內核通過mlock()係統調用可以實現將頁鎖在物理內存上,但是實際上卻不止這一種方式,而這些方式的行為也多少有些不同,這使得在資源計數和內存管理方麵有些混亂。Peter Zijlstra的一套patch定義了另一種稱為"pinning"的鎖頁操作。

鎖內存的一個問題是不能滿足所有用戶的需求。一個被mlock()調用鎖住的內存頁會一直占據係統物理內存,因此從表麵上看,當應用程序訪問這些頁時不會發生缺頁異常。但是這卻沒有要求這些頁必須一直在同一個位置,而內核則可以根據需要對頁遷移。當頁發生遷移後,應用程序再次訪問時會發生一次軟缺頁(不會發生任何IO)。大部分情況下這不是一個問題,但是硬實時程序的開發者要求更苛刻,他們要避免軟缺頁帶來的延遲。但是內核目前還沒有這種形式的鎖內存功能。

鎖內存也無法滿足一些內核內部的需求。例如內核用來做DMA緩存的內存不能被遷移,這些頁不能使用鎖機製,但是通過增加引用計數或調用get_user_pages()來達到了固定頁的目的。對於這些變相被鎖住的頁,它們是怎麼和資源計數機製交互的呢?係統管理員可以對用戶可鎖頁的數量設置一個上限,但是創建和用戶態共享的DMA緩存卻是應用程序的行為。因此對於一些用戶而言,可以通過創建RDMA緩存來變相達到鎖頁的目的,而又繞過計數機製,這使得想限製所有鎖頁數量的係統管理員和開發者很不爽。這些通過“後門”創建的鎖頁也帶來了其他問題。內存管理子係統對可遷移頁和不可遷移頁做了區分,這種情況下的頁是以正常匿名頁方式分配出來的可遷移頁,但是把它們強製鎖住使得它們不可遷移。當內存子係統試圖遷移內存頁來創建大塊連續內存時,這些頁便不能被操作從而無法創建更大的連續內存。

Peter的patch便是嚐試解決以上問題,他提出了“pinned”頁的概念,這些頁隻能在當前的物理位置上。pin住的頁被放在了一段單獨的VMA中,同時帶有VM_PINNED標誌。內核裏可以通過mm_mpin()函數來pin住頁:

   int mm_mpin(unsigned long start, size_t len);

在調用進程的資源限製允許的情況下,該函數會將內存頁pin在內存裏。內核代碼訪問這些頁時也仍需要調用get_user_pages(),且在mm_mpin()之後。

一個長期計劃是使內存pin功能對用戶態可用,新加一個類似於mlock()的新係統調用mpin(),來保證頁不會被遷移也不會發生缺頁異常。另一個當前未實現的功能是在鎖頁前先將該頁遷移到不可遷移的內存區域,因為mm_mpin()調用明確告知了該頁不能被遷移,因此內核事前將其分配在不可遷移區域是有益的,這避免幹擾以後的內存壓縮操作,可以增大創建大塊連續內存的概率。最後,將被pin的頁放在單獨的VMA可以更方便的追蹤,也可以被記賬從而避免了剛才提及的後門。

目前來看,似乎沒有人對這套patch強烈反對,在上輪討論中,有人提到改變鎖頁的計數機製可能對即將達到限製的用戶帶來regression,但是這個問題也沒有其他的解決辦法,可以選擇繼續讓pinned頁不在這個限製內,或者給它單獨設置一個限製。

Another attempt at power-aware scheduling

2013年的power-aware scheduling mini-summit提出希望CPU power-aware scheduling能夠集成包括CPU frequency和CPU idle governors等子係統。5月23日,Morten Rasmussen發的Energy cost model for energy-aware scheduling patch set。這組patch棄用之前的啟發式算法,改而嚐試測量計算每個調度決策將帶來的power消耗。

通過這組patch,將可以實現類似以下這樣一個函數接口:

int energy_diff_util(int cpu, int utilization);

即計算得到一個指定的負載(通過利用率代表)加到給定CPU時將帶來的power消耗。
現實條件下這個接口實現還麵臨以下一些困難:
1. 內核並不知道即將被調度的特定task的CPU利用率。因此這組patch使用的是load進行衡量,但這畢竟不是一樣的量,load並沒有考慮進程的優先級。
2. 調度器並不知道CPU frequency governor將會對哪些CPU執行什麼動作。主要是因為還沒有將這些子係統集成起來。
3. CPU喚醒對節能調度的影響。除了簡單的CPU利用率會影響CPU能耗外,另外將CPU從睡眠狀態喚醒過程本身帶來特定的能耗(取決於CPU睡眠的深度,這也是一個當前調度器無法獲取的量)。一個特定的進程不可能知道自己喚醒一個睡眠CPU的頻繁程度,但是可以通過計算處理器本身從睡眠狀態喚醒的頻繁度進行計算。通過估算有多大的概率進程的喚醒會發生在CPU處於睡眠狀態,可以估算進程喚醒導致CPU從睡眠狀態喚醒帶來的開銷。

擁有這些條件之後,剩下的就是在需要為一個給定進程選擇CPU時執行能耗計算。遍曆所有的CPU開銷過大,因此要求盡可能快的定位到一個盡可能低層級的group進行計算,最低能耗的CPU所在的group將會被選擇。在這組patch中,find_idlest_cpu被修改執行該計算,其他進程放置策略選擇(比如負載均衡)並沒有修改。

這組patch提供一小組benchmark信息,針對特定的負載,在big.LITTLE係統上,顯示節能效果能從3%提升到50%。同時進程切換開銷也差不多是原來的4倍,這是當前不可接受的,需要繼續一些優化工作。

截止目前,這組patch的討論還在默默繼續。過去對power-aware調度的patch進行reviewer人員不足一直是個問題,這組patch將使得相關review工作變得簡單。然後我們將會看到這個想法是否代表一個可行的前進方向

The BFQ I/O scheduler

塊設備層IO調度器的作用是向存儲設備分發IO請求,使得吞吐量最大化且並使延遲最小化。Linux內核目前包含幾個不同的調度器,但是近些年這方麵的改動很小,既沒有提出新的調度器也沒有對現有調度器進行較大的改動。但是最近出現了一個新的"budget fair queuing" (BFQ) IO調度器,提出了一些有趣的想法。

BFQ介紹

BFQ已經被開發使用了好幾年,從很多方麵上它都參考了內核裏的CFQ調度器。CFQ對每一個進程的IO請求都單獨維護了一個隊列,並輪轉服務這些隊列來公平地劃分可用帶寬,CFQ工作的很好且通常是旋轉磁盤的選擇。但是CFQ在優化改進性能的同時代碼也越來越複雜,盡管加了一些啟發式算法但仍會產生一些較大IO延遲。

BFQ調度器同樣對每個進程都維護IO請求隊列,但是不采用CFQ的輪詢方式,而是給每一個進程都分配一個"IO預算"。該預算表示當進程下一次訪問設備時允許傳輸的sector數目。預算的計算方式有些複雜,但是整體上是基於每個進程的IO權重以及該進程過去的行為。IO的權重函數類似於一個優先級參數,通常被管理員設置且是一個常量,具有相同權重的進程將會獲得相同的IO帶寬。不同的進程擁有不同的預算,但是BFQ會盡力保持整體的公平性,因此一個有較小預算的進程會比有較大預算的進程更快的獲得調度機會。當決定服務哪些請求時,BFQ會檢查各個進程的預算,去選擇會盡快釋放設備的那一個,因此具有較小IO預算進程的等待時間會小於大預算進程。當選擇一個進程後,它會排它的占有這個磁盤設備直到預算裏的sector傳輸完畢,但是也有一些例外:
1. 正常情況下如果一個進程不再有任何請求,則它對磁盤的訪問就結束了。但是如果最後一個請求是同步請求(例如讀請求),BFQ會idle一會來給該進程一個機會產生新的IO請求。這是因為進程可能正在等待該讀請求的完成然後再產生後續新的IO,而這些IO很大概率上是和上一個請求連續的,因此服務起來也很快。這聽起來有點不合情理,但是通常情況下在一個同步請求後等待一會會提高吞吐量。
2. 每個進程完成請求的時間也有限製,如果它的IO完成的很慢,比如很多隨機IO,那麼在完成所有預算前可能會停止它繼續訪問設備,但是這種情況下仍然會記賬它使用了整個預算,因為它影響了整個設備的IO吞吐量。
關於每個進程的預算分配算法,簡單來說是它上次被調度時傳輸的sector數目,但有一個全局最大值。因此起起停停傳輸量較小的進程會傾向獲得較小的預算,而IO密集型進程則有較大的預算。預算較小的進程對延遲響應更敏感,被調度地更加頻繁。預算較大的進程會做較多IO且等待時間較長,但是會獲得加時時間片來提高設備的吞吐量。

關於啟發式算法

BFQ的一些使用經驗顯示上麵描述的算法有不錯的效果,但是仍有提升空間。目前發出的代碼增加了一些啟發式算法來改善係統這方麵的行為,具體包括:
1. 剛啟動的進程會獲得一個中等的預算和遞增的權重,使得它們能以相對較小的延遲獲得足夠的IO,目的是在應用進程啟動階段為它分配額外的IO帶寬來盡快地將代碼加載至內存。遞增的權重會隨著進程運行時間線性地減少。
2. BFQ的預算計算以及允許的最大預算值,都基於底層設備的IO速率峰值。由於數據在磁盤上的位置以及設備本身的緩存等影響,IO速率峰值可能變動較大,因此對速率計算進行了一些微調來將這些因素考慮在內。例如,會考慮已經超時但是仍沒有用完預算的進程,發生超時說明IO訪問是隨機的且磁盤沒有跑出峰值,也可能說明最大預算值設置過高。除此之外還會過濾掉計算出的過大的速率值,因為可能是設備緩存的影響而不是真實的IO速率。
3. 預算計算公式也有一些調整。如果一個進程在用完預算前處理完了請求,之前的做法是降低預算到實際發出的請求數目,而目前的做法是調度器會檢查該進程是否有未完成的IO請求,如果有的話,速率值會被翻倍因為理論上當這些請求完成後會有更多的請求到來。當發生超時時,預算也會被翻倍,這是為了幫助進程度過較慢的一段,同時降低那些真正隨機訪問的進程被服務的頻率。最後,如果當預算用完後仍然有未完成的請求,說明可能是IO密集型進程,因此預算會乘4。
4. 寫操作比讀操作更耗資源,因為磁盤傾向於緩存寫數據並立即返回請求,而過段時間才會發生向磁盤真正的寫。這可能會對讀請求造成餓死。BFQ通過對寫請求更消耗預算來考慮這種代價,實際中一個寫相當於十個讀。
5. 如果設備內部可以排隊多個命令,那麼讓設備idle可能會使內部隊列清空,從而造成吞吐量降低,因此BFQ會在能排隊命令的SSD上關閉idle。旋轉磁盤上也可以關閉idle,但是是在服務隨機IO時才會這麼做。
6. 當多個進程訪問磁盤的同一區域時,最好能將它們的隊列合並而不是分開服務。一個很好的例子是QEMU,它會將IO分發給一組工作線程發送。BFQ包含一個叫“early queue merge”的算法會去探測這類進程並將它們的隊列合並服務。
7. BFQ會探測“軟實時”程序,例如媒體播放器,並提高它們的權重來降低延遲。探測算法的原理是尋找特定的IO請求模式,並idle其一段時間。如果進程具有這種IO模式,它們的權重將會被提升。
除了這些之外還有很多啟發式算法,畢竟為所有負載類型優化係統模式是一個相當複雜的工作。從BFQ開發者Paolo Valente發出的測試數據來看,效果相當不錯,但是離BFQ合進mainline仍然還有很多困難。

合並進mainline

BFQ目前的反饋還是很不錯的,數字說明一切,同時大家也很高興調度器和啟發式算法被全麵的描述和測試。CFQ包含很多啟發式算法,但是懂的人很少,BFQ看起來是一個更幹淨清楚的版本。但是內核開發者不希望看到另一個像CFQ一樣的小生態係統被合並,他們希望能逐步把CFQ改進為BFQ,同時係統中隻有這一個調度器。Tejun Heo說這樣合並起來更容易,也能讓更多開發者了解一步步的過程,即使以後CFQ如果出現性能退化也可以通過bisect方式定位具體的修改。 BFQ已經使用了一段時間,一些發行版如Sabayon,OpenMandriva和CyanogenMod已經包含了進去。技術不錯,但是需要一些時間來慢慢地推動合並進mainline。

The unified control group hierarchy in 3.16

重新實現內核中控製組的想法準確來說不是新的,參見2012上半年的一篇文章。然而,那篇文章所講的還沒有太多在內核實現。 這種情況在內核3.16中得到改變,它包括了新的統一的控製組分層代碼。這篇文章將是統一分層在用戶級別如何工作的一個概述。

雖然控製組係統從一開始就支持了多分層,每個能包含一組不同的進程,這種靈活性有其吸引力,但帶來了開銷。跟蹤應用到 某個進程的所有控製器是昂貴的。在一些場景下,也需要更好的控製器間的協作來有效地控製資源的使用。最後這種特性在現實 世界裏很少得到應用。因此有計劃準備將多分層從內核中去掉。

統一控製組分層開發有一段時間了,許多準備工作也已經被合入了內核3.14和3.15。在3.16中,這個特性將是可用的,但僅僅 對於那些明確要求它的用戶。為了使用統一分層,新的控製組虛擬文件係統應該被掛載像下麵:

 mount -t cgroup -o __DEVEL__sane_behavior cgroup <mount-point>

很明顯,__DEVEL__sane_behavior選項不是永久存在的,在統一分層變成缺省特性之前,它還會存在一段時間。 在統一分層中,所有的控製器連接到分層的根。基於一些規則,控製器能在分層子樹中激活。出於解釋這些規則的目的,想象 一個像右圖的控製組分層。組A和組B直接位於跟控製組下,組C和組D是組B的孩子。

                         root
                         /  \
                        A    B
                            / \
                           C   D

規則1:控製組必須應用一個控製器在所有的孩子上或者誰也不被應用。一個控製器除非已經在它的父組裏被激活了,否則不能 在該組中激活。

規則2:僅在關聯的組沒有包含進程的時候,cgroup.subtree_control文件能被用來改變控製器的設置。

規則3:當組內或者它的子孫組中有進程,讀cgroup.populated文件將會返回非零。通過poll()這個文件,假如控製組變得完全空, 一個進程將會得到通知。

所有這些工作都討論了好多年了;控製組用戶的大多數都進行了評論,因此今天統一分層應該對大多數的用例是可用的。內核3.16 將給感興趣的用戶一個試用新模式,找出其中殘留的問題的機會。寄希望在未來幾個開發周期內互用能實際遷移到新模式是不切實際的。

The volatile volatile ranges patch set

“易失範圍內存”(Volatile Ranges)的作用是指定一段用戶空間內存範圍,該範圍內的內存在內存較為緊張的時候會被操作係統回收使用。常見的使用案例是瀏覽器圖片緩存。瀏覽器喜歡將信息盡可能保存在內存中以加快頁麵的加載速度,但實際上這些內存更適合被使用在其他更需要內存的地方。這一想法的實現經曆了許多波折,而且現在看對實現細節的修改仍然沒有結束。

早期的版本使用的是posix_fadvise()係統調用,但是有些開發者認為使用這個係統調用並不合適,因為這個係統調用給用戶的感覺更多的是與分配相關的工作。所以後來的版本改為使用fallocate()係統調用。隨後在2013年,對用戶的接口又改為了另外兩個新的係統調用:fvrange()和mvrange()。到了2014年的第十一個版本使用的接口變味了vrange()。在經曆了多輪迭代之後,開發者們又開始關注起用戶空間的語義(比如:當一個進程訪問一段已經被回收的內存時會發生什麼)以及內部是實現細節。因此到目前為止,這個補丁仍然沒有被合並。
到了第十四個版本用戶態接口又換成了madvise(): <source lang="c"> madvise(address, length, MADV_VOLATILE); </source> 在調用該係統調用後,從address開始長度為length的內存隨時可能被操作係統回收,而內存中的數據將會被丟棄。應用程序如果需要訪問該段內存時,需要將其標記為不可散失: <source lang="c"> madvise(address, length, MADV_NONVOLATILE); </source> 返回值為0則表明調用成功(該段內存變為非易失,內存中的數據沒有被丟棄);返回負值則說明有錯誤發生或者操作成功但內存中的數據已經被丟棄。

此前madvise()接口也曾經被考慮過,但當時的問題是當時的實現需要接口返回兩個結果:1)有多少內存頁已經被標記成功;2)是否有內存頁的數據已經被丟棄。這次John終於找到了一種可以原子操作的實現方法來實現這一操作。由於不再需要第二個參數,所以madvise()係統調用就變成一個較為適合的接口了。

如果用戶嚐試訪問一段已經被釋放掉的內存空間時會發生什麼呢?目前的實現中操作係統會向該進程發送SIGBUG信號。應用程序需要捕捉該信號從而向其他數據源獲取數據。如果該進程沒有捕捉SIGBUG信號,則會直接coredump。顯然這樣的做法不夠友好。但是大家認為既然用戶自己表明該段內存可以釋放,就應當遵守規範,不再訪問該段內存。

Minchan Kim不太喜歡這個這個解決方案。他認為應用程序在訪問到那些已經標記為易失的內存空間時操作係統應擔直接返回填0的內存頁,這樣做開銷很小。同時也不需要應用程序顯示調用MADV_NONVOLATILE並且捕捉SIGBUG信號。但是John認為這個工作應該是Minchan的MADV_FREE補丁來完成的。而Minchan不這麼認為,他覺得MADV_FREE的操作無法經曆反複的釋放和重用。而MADV_VOLATILE則可以做到。但John表示很擔心這樣做帶來意想不到的後果。
Johannes Weiner也比較傾向於返回填0內存頁的方案。同時他認為John的實現可以基於Minchan的MADV_FREE來進行。至於填0還是SIGBUG,他認為可以考慮同時提供,讓用戶自己來選擇。John認為可以一試。

John同時表示後麵恐怕沒有太多時間再來完善目前的工作了。確實,這個補丁經曆的時間太長了。從第一版到第十四版,經曆了不通開發者的審閱,知道現在仍然沒有被合並。
同時,錯也不在那些給出審閱意見的開發者身上。畢竟易失範圍內存這個概念的引入是對用戶可見的修改。如果實現、接口選擇不正確,影響會持續很長時間。內存管理代碼的改動似乎總是很難進入主線內核,而這一修改還會對用戶課件,那事情就更糟了。這個補丁真是如此,因此開發者們格外謹慎也就可以理解了。

目前沒有人能夠回答這個補丁是否應該進入主線內核。同時使用這一補丁的用戶(ashmem)早已開始使用類似的補丁了。所以除非有人能夠繼續推進這一補丁,否則主線內核恐怕很難能夠用上這一特性。

RCU, cond_resched(), and performance regressions

性能退化是內核開發者經常遇到的問題。一個看似不相幹的改動可能會引起一個顯著的性能下降,有時候這種性能的退化會潛伏好幾年,直到受影響的用戶升級了的內核,並注意到運行速度變慢了。 好消息是開發社區為了發現性能退化,正在做更多的測試。這些測試發現了3.16內核裏的一個典型的性能退化。 這個問題,作為一個例子來證明廣泛的適用很多用戶是多麼的困難的,很值得一看。

The birth of a regression 一次性能退化的產生:

內核的read-copy-update (RCU)機製通過數據結構更改的免鎖和集中的清理操作,極大的增加了內核的擴展性。 RCU的一個基本操作就是檢測每個cpu的"quiescent states" ,"quiescent states" 是指一個狀態, 在這個狀態下,內核不持有任何的RCU保護的數據結構的引用。最初,"quiescent states"被定義為“當處理器 運行在用戶態的狀態”,但是之後事情變得更加的複雜(詳情見LWN's lengthy list of RCU articles)。

內核的”full tickless mode”,已經比較正式的使用了,它會使得"quiescent states"的檢測更加困難。 由於這種模式的限製,一個運行在tickless模式下的cpu會一直運行一個進程。如果那個進程占用內核運行很長時間, 就沒有"quiescent states"被檢測到。結果導致RCU不能夠宣布一個"grace period" 結束,並運行(可能比較耗時) 一係列的累計的RCU回調函數。被拖延的"grace period"會導致過多的內核延遲,最壞情況下會導致內存耗盡。

Fixing the problem 修訂這個問題

有人可能(確實有開發者這麼做了)認為在內核裏這樣的循環會有嚴重的問題.確實有這樣的情景發生了, Eric Dumazet提到了一個: 一個打開了幾千個socket的進程調用exit(). 每個打開的socket都會通過RCU去釋放結構體. 當同一個進程在關閉socket時, 這就產生了一個很長待做的工作的鏈, 這阻止了RCU在內核裏的循環處理。

RCU開發者Paul McKenney在簡單觀察的基礎上,給出了一個該問題的解決方法:在內核裏已經有這麼一套機製, 當一個很長的操作在運行時,它允許其他事情去運行。在已知一段要長時間運行的代碼裏,不時的調用cond_resched(), 從而給調度器一個運行更高優先級進程的機會。在tickless模式下,沒有更高優先級的進程,因此,在當前內核裏, cond_resched()在tickless模式下啥也不做。

但是內核隻有在他可以被調度出cpu的時候,才可以調用cond_resched(),所以不可以運行在原子上下文中,不能夠持有任何 的RCU保護的數據結構的引用。換句話說,調用cond_resched()一次,意味著一次quiescent state,所需要做的就是通知RCU。

如果真的這樣的話,cond_resched()會在許多性能敏感的地方被調用(進而產生大量的負荷),而這是不可行的。 所以Paul沒有在每個cond_resched()時,都通知RCU進入一個quiescent state。通過一個percpu的計數器, 每次更改時,計數器加一,每調用256次cond_resched(),才通知RCU一次。這以較小的代價修複了問題,因此patch 被合入到3.16內核裏。

在這之後不久,Dave Hansen報告說,他有一個性能指標(一個除了打開並關閉大量文件外,基本不做其他事情的程序)下降了。 通過二分法,他定位到cond_resched()改動是罪魁禍首。有趣的是,沒有cond_resched(),程序會跟預期運行的一樣快。 相反,改動會讓RCU grace periods出現的比以前更頻繁。進而引起RCU 回調被以更小規模的批量執行,增加了在slab內存分配過程中 的競爭。通過把quiescent states的閾值從每256次cond_resched()調用調到更大,Dave更夠恢複到3.15版本水平的性能。

Fixing problem(修訂這個問題) 有人可能認為,簡單的提高閾值就可以為所有的用戶修複這個問題。但是,那樣的話不僅會恢複性能,cond_resched()試圖修複的問題又會出現了。 挑戰就是要修複一個性能問題,而不會導致其他的問題。 還有一個附加的挑戰,一些開發者喜歡把cond_resched()在全可搶占內核上,做成一個完全的空操作。畢竟,如果內核是可搶占的, 就不需要考慮需要調度的情景了。

Paul的第一個版修複方法是在幾個地方做修改的一係列patch。cond_resched()仍然有檢查,但是檢查使用了另外的一種形式。RCU的核心被修改成了 注意當有一個特定的處理器保持grace period很長的時間。當這種情況發生時,一個percpu的標誌位會被設置,然後cond_resched()隻需要 檢查那個標誌,如果標誌位被設置了,意味著經曆了一個quiescent period。這個改變降低了grace periods的頻率, 挽回了出大部分的性能損失。

另外,Paul提出了一個新的函數cond_resched_RCU_qs(),即所謂"慢版本的cond_resched()"。缺省情況下, 它跟普通的cond_resched()做同樣的事情,但是它的目的是即使當cond_resched()被改為跳過檢查,或者啥也不做時, 它仍然繼續執行RCU grace period檢查。這個patch在少量的戰略位置上把cond_resched()調用改為cond_resched_RCU_qs(), 過去,問題在這些地方出現過。

這個解決方法能夠工作,但是讓一些開發者不太高興。對於那些希望得到他們cpu最好性能的家夥,任何像在cond_resched()這樣函數裏的 計算都是太浪費的。所以, Paul提出了另外一個不同的方法,在cond_resched()不需要做任何的檢查。相反,當RCU注意到一個cpu 已經持有grace period很長時間,它就會發送一個核間中斷到那個處理器。核間中斷會在那個cpu不運行在原子上下文的時候被投遞到。 這樣,也是通知一個quiescent state的恰當時機。

這個方案可能第一眼看起來很奇怪,IPI是很耗資源的,這就顯得這個方法不是提高擴展性的一個正常方法。但是這個方法有兩個優勢: 它不再需要在性能敏感的cpu上進行監控,並且IPI隻在有問題的時候才產生。所以,大多數時間,它應該對運行在tickless模式下的cpu 沒有任何影響。這樣看起來,這個方案是蠻不錯的,並且解決了性能退化的問題。

How good is good enough? 怎樣才算足夠好?

盡管相比以前的,它變得更小了, 但是Dave仍然看到一些性能下降。這個方案不是完美的,但是Paul傾向於宣布無論如何這都是一個勝利。 考慮到短暫的grace periods幫助其他的負載,解決了實際問題的patch,大量的RCUtree.jiffies_till_sched_qs在3%內, 難道我們不應該認為是勝利嗎?

對這種情況,Dave仍然不是100%的滿意,他注意到相比缺省的設置,性能損失接近10%,並說“現有的改動抵消掉了我的係統通過RCU獲得的收益” Paul回應說,“不是所有的有興趣的微基準都能成為kernel的約束物”,並發起一個包含了第二版的解決方案的pull請求。

他建議,如果現實世界裏真有被影響到的負載,通過各種方法優化係統,以緩解問題。

無論,這個問題是否真正徹底的解決掉,這次回退證明了在當前係統上的內核開發的危險性。可擴展性的壓力使得複雜的代碼盡量保證所有事情都 能用最小開銷下在適當的時間發生。但是不可能讓一個開發者測試所有可能的負載,所以經常會有一個改動導致驚人的性能下降。解決一個工作量可能會 對另外一個不利。不影響任何工作量的改動是不可能實現的。但是,充分的測試並關注測試中暴露出的問題,可以讓絕大多數的問題在影響到 生產用戶前有望被發現並解決掉。

Reworking kexec for signatures

kexec機製允許在已運行的內核中直接切換到另一個內核。這對快速啟動很有意義,因為可以繞過firmware和bootloader。Kexec還可以與kdump一起產生crash dumps信息。不過,Matthew Garret在他的博客中提到,kexec可以被用以規避UEFI的安全啟動限製,他給出了一種在安全啟動時禁用kexec的方法。對大多數人來說,這並非很嚴重的問題。目前已經有一組patch使得kexec僅僅能啟動那些被簽名的內核。該方案在不需完全禁用kexec的情況下解決了Matthew Garret提到的問題。

Kexec子係統主要包括係統調用kexec_load()。該函數將新內核加載到內存,隨後可以用係統調用reboot()引導係統。命令行工具kexec可以同時完成加載與重啟工作,整個過程無需firmware與bootloader的幹預。

UEFI引入了安全啟動限製。Linux內核可以被用來引導未簽名的(可能是惡意的)windows操作係統,因為kexec能夠繞過UEFI的安全限製。微軟可能會因此將Linux的bootloader列入黑名單,那麼以後在通用機器上引導linux就會很難了。雖然微軟可能不會那麼快行動,kexec畢竟也可以影響到需要安全啟動的linux係統。

不管怎麼樣,Garrett最終還是將禁用kexec的代碼從他的patch中拿掉了,但是他依然建議支持安全啟動的發行版最好禁用kexec。當然,這些patch還沒有被合並。最近,Vivek Goyal提交了一組patch,用以解決上述的安全問題,但隻能保護那些開啟了模塊簽名機製的係統。Garrett在博客中解釋了繞過安全限製的方法:在新內核中修改原有內核內存中的sig_enforce sysfs參數,再跳回原內核。   Goyal的patch對kexec做了限製,使得它隻能執行帶簽名的代碼。他實現了一個新的係統調用:

long kexec_file_load(int kernel_fd, int initrd_fd,  *const char *cmdline_ptr, unsigned long cmdline_len,    unsigned long flags);

Kernel_fd指向新內核的執行文件。Initrd_fd指向"initial ramdisk" (initrd)文件。新內核啟動後將執行cmdline_prt製定的命令。現有係統調用的格式如下,大家可以對比下:

long kexec_load(unsigned long entry, unsigned long nr_segments,    struct kexec_segment *segments, unsigned long flags);

該係統調用要求在用戶空間對內核的可執行文件進行解析,然後盲目地將它加載進內存。kexec_file_load()加載了整個內核文件,因此它知道當前加載與執行的到底是什麼內容。

在加載進來的所有段中,有個較為獨立,名為“purgatory”的段。它在兩個內核之間運行。在重啟時,現有內核首先跳到purgatory,它的主要功能是檢查其它段的SHA-256 hash。檢查沒有問題,才可以繼續啟動。Purgatory將一些內存copy到備份區域,執行那些麵向體係結構的安裝代碼,然後跳轉到新內核。

Purgatory目前位於kexec-tools工具中,如果內核想要運行kernel binary與initrd中的段,就必須有它自己的Purgatory。Goyal的patch將內核的purgatory放在了arch/x86/purgatory/。

Goyal還將crypto/sha256_generic.c拷到了purgatory目錄下。實際上他可以直接使用,但貌似沒有成功。才會退而求其次,選擇了copy。

目前,該patch發布了版本3,狀態仍是“request for comment”。這組patch還有未盡的功能,首先就是簽名驗證。目前,還僅支持X86_64體係和bzimage的內核格式。後續還需要支持其它的體係結果和ELF格式的kernel image。此外還得完善文檔,包括man。

Goyal解釋了他在簽名驗證上的想法。它基於David Howells在加載內核模塊時進行簽名驗證的工作。本質上,每次調用kexec_load_file()時都會驗證簽名。此時,還會計算每個segment的sha256 hash,存儲在purgatory段中。purgatory必須驗證這些hash,以確保被執行的是一個正確簽名的內核。

該組patch的每一個版本都得到了很多評論,其中大多數都是技術性的改進意見。尚沒有人反對這個想法本身。所以在明年的某個時候,我們就能看到kexec將執行帶有加密簽名的kernel。希望會更快一些。當Garrett的安全啟動patch被merge時,Goyal的patch將會很有用。

Teaching the scheduler about power management

在移動領域中,高能效的CPU調度變得越來越重要。然而在降低大規模數據中心電費開銷上,高能效CPU調度也變得同樣重要。遺憾的是,內核CPU功耗控製部分與調度器的結合非常有限,使調度策略不夠完善。本文總結CPU功耗控製機製的現狀,關注正在進行的改進。

曆史

進程調度器是操作係統的關鍵組件,它決定接下來哪一個進程執行。Linux調度器的實現經過了多年的演變,甚至是完全重寫。由Ingo Molnar實現的完全公平調度器(Completely Fair Sheduler,CFS)在2.6.23內核中被引入,它替代了O(1)進程調度器。O(1)進程調度器同樣也是由Ingo在2.5.2內核中引入替代了之前的調度器。不管這些調度算法有什麼區別,它們的目標都是一樣的:盡可能使用CPU資源。

在這段時間CPU資源也發生了變化。最初,調度器隻負責在所有可運行的進程間管理處理器時間。隨著SMP、SMT和NUMA的出現,硬件並行度的增加使問題變得更複雜。另外,調度器還需要麵對不斷增加的進程和處理器數目,其本身還不消耗過多的調度時間。這些變化解釋了在過去半個多世紀多個調度器被設計開發,並且目前也被不斷研究的原因。在其發展過程中,調度器複雜度不斷增長,隻有少數人成為了這方麵的專家。

最初的進程調度隻考慮吞吐量,完全不需要考慮能耗;調度器工作由企業驅動,這裏的係統都使用固定電源。另一方麵,嵌入式和移動領域使用電池的設備逐漸出現,能耗成為關鍵問題。解決能耗問題的子係統,如cpuidle和cpufreq被分別加入內核,它們的開發者並沒有很多調度器經驗。

至少在初期,這種分離的安排效果不錯,分離的子係統降低了開發和維護的難度。隨著移動設備性能的增長和大數據中心開銷的增加,人們開始關心能耗的問題。這催生了很多關鍵的變化,包括可延時定時器(deferrable timers)、動態tick(dyntick)和運行時功耗控製(runtime power management)。多核移動設備的流行甚至導致出現了備受爭議的CPU offlining技術。

這些變化顯現出一種模式:調度器和功耗管理越來越複雜,它們也變得越來越分離開來。由於一方麵不能了解另一方麵的變化趨勢,這種模式出現了適得其反的效果。盡管這樣,芯片製造商還不斷在操作係統外的硬件上實現DVFS(https://en.wikipedia.org/wiki/Voltage_and_frequency_scaling), 使問題加劇。ARM big.LITTLE(https://lwn.net/Articles/481055/) 問題的支持和調度器修改對功耗的影響越來越大,使功耗管理和調度器的合並無法避免。

調度器和cpuidle

當CPU空閑時,cpuidle子係統進入低功耗狀態或者叫空閑狀態(C態)以降低功耗。然而,使一個CPU空閑也是有代價的:進入低功耗狀態程度越深,將CPU從該狀態喚醒需要的時間越長。需要平衡實際降低的功耗和進入推出低功耗狀態花費的時間。更進一步,進入某些狀態時CPU的過渡就不可避免花費一定數量的電量,這意味著CPU必須處於空閑狀態的時間足夠長,進入空閑狀態才是有意義的。大多數CPU有多個空閑狀態,以方便多種情況下的功耗控製和喚醒延遲的平衡。

因此,cpuidle控製必須收集cpu使用信息,在CPU支持的空閑模式中進行選擇。這些信息收集的工作使調度器變得更加複雜,即使通過不精確的啟發式的算法。

選擇深度不同的空閑狀態由將CPU從空閑狀態喚醒的事件決定。這些事件可以分為三類:

  • 可預測的事件:包括可以獲得到期時間的定時器,可以設置空閑狀態的時間。
  • 半可預測的事件:通常是重複事件,如IO請求完成,通常遵循一定的模式。
  • 隨機事件:其他,如敲擊鍵盤、觸摸屏事件、網絡包等。

把調度器加入到選擇深度不同的空閑狀態中,非常有利於對半可預測事件的處理。IO模式與發起的進程和設備密切相關。調度器可以記錄每個進程的IO延遲,結合IO調度器的信息,根據某個CPU上等待的進程估計下次IO請求完成的時間。從調度器的角度出發更容易掌握CPU處於idle狀態的時間。

因此使調度器和cpuidle結合更緊密,由調度器管理可選擇的空閑模式最終接管cpuidle很有必要。把idle循環移到調度器中,也有助於統計空閑時CPU響應中斷的時間和中斷出現的次數。

此外,調度器了解當前的空閑狀態也有助於負載均衡。例如對於/kernel/sched/fair.c中的函數find_idlest_cpu()來說,它通過比較CPU負載選擇其中負載最小的CPU。如果多個CPU處於空閑狀態,那麼它們的負載都是0,這時選擇空閑狀態最早結束的CPU是最合適的。如果所有空閑CPU的結束時間是一樣的,最晚進入空閑狀態的CPU cache是最新的(假設空閑狀態保留cache)。Daniel Lezcano已經提交了一些這樣的補丁(https://lkml.org/lkml/2014/3/28/181)
這也說明了角度不同,一個事物的含義有區別。調度器中的find_idlest_cpu()函數僅僅實現了find_busiest_cpu()的相反功能,而在cpuidle上下文中它表示選擇idle狀態最深的CPU。處於idle狀態越深,使CPU恢複工作的開銷越大。同樣對於“power”這個詞來說,調度器中傳統的含義是“消耗的能量”,在功耗控製裏它的意思是“能量消耗速率”。最近的一些補丁(https://lkml.org/lkml/2014/5/26/614) 解釋了這個問題。

調度器和cpufreq

調度器記錄各CPU上的可調度程序工作量,公平的分配CPU資源給進程,決定負載均衡的時機。按需cpufreq控製器(Ondemand cpufreq governer)也做類似的記錄,動態調整cpu頻率以延長電池壽命。由於功率和電壓的二次方成正比,比較合適的方法是在保證CPU足夠快的基礎上,盡量降低時鍾頻率,以降低CPU工作電壓。

與cpuidle類似,cpufreq也是與調度器分別開發的,兩個子係統之間也有一些問題: