1. 程式人生 > >阿里linux核心月報201405-06

阿里linux核心月報201405-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消耗。
現實條件下這個介面實現還面臨以下一些困難:

核心並不知道即將被排程的特定task的CPU利用率。因此這組patch使用的是load進行衡量,但這畢竟不是一樣的量,load並沒有考慮程序的優先順序。
排程器並不知道CPU frequency governor將會對哪些CPU執行什麼動作。主要是因為還沒有將這些子系統整合起來。
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傳輸完畢,但是也有一些例外:

正常情況下如果一個程序不再有任何請求,則它對磁碟的訪問就結束了。但是如果最後一個請求是同步請求(例如讀請求),BFQ會idle一會來給該程序一個機會產生新的IO請求。這是因為程序可能正在等待該讀請求的完成然後再產生後續新的IO,而這些IO很大概率上是和上一個請求連續的,因此服務起來也很快。這聽起來有點不合情理,但是通常情況下在一個同步請求後等待一會會提高吞吐量。
每個程序完成請求的時間也有限制,如果它的IO完成的很慢,比如很多隨機IO,那麼在完成所有預算前可能會停止它繼續訪問裝置,但是這種情況下仍然會記賬它使用了整個預算,因為它影響了整個裝置的IO吞吐量。
關於每個程序的預算分配演算法,簡單來說是它上次被排程時傳輸的sector數目,但有一個全域性最大值。因此起起停停傳輸量較小的程序會傾向獲得較小的預算,而IO密集型程序則有較大的預算。預算較小的程序對延遲響應更敏感,被排程地更加頻繁。預算較大的程序會做較多IO且等待時間較長,但是會獲得加時時間片來提高裝置的吞吐量。
關於啟發式演算法
BFQ的一些使用經驗顯示上面描述的演算法有不錯的效果,但是仍有提升空間。目前發出的程式碼增加了一些啟發式演算法來改善系統這方面的行為,具體包括:

剛啟動的程序會獲得一箇中等的預算和遞增的權重,使得它們能以相對較小的延遲獲得足夠的IO,目的是在應用程序啟動階段為它分配額外的IO頻寬來儘快地將程式碼載入至記憶體。遞增的權重會隨著程序執行時間線性地減少。
BFQ的預算計算以及允許的最大預算值,都基於底層裝置的IO速率峰值。由於資料在磁碟上的位置以及裝置本身的快取等影響,IO速率峰值可能變動較大,因此對速率計算進行了一些微調來將這些因素考慮在內。例如,會考慮已經超時但是仍沒有用完預算的程序,發生超時說明IO訪問是隨機的且磁碟沒有跑出峰值,也可能說明最大預算值設定過高。除此之外還會過濾掉計算出的過大的速率值,因為可能是裝置快取的影響而不是真實的IO速率。
預算計算公式也有一些調整。如果一個程序在用完預算前處理完了請求,之前的做法是降低預算到實際發出的請求數目,而目前的做法是排程器會檢查該程序是否有未完成的IO請求,如果有的話,速率值會被翻倍因為理論上當這些請求完成後會有更多的請求到來。當發生超時時,預算也會被翻倍,這是為了幫助程序度過較慢的一段,同時降低那些真正隨機訪問的程序被服務的頻率。最後,如果當預算用完後仍然有未完成的請求,說明可能是IO密集型程序,因此預算會乘4。
寫操作比讀操作更耗資源,因為磁碟傾向於快取寫資料並立即返回請求,而過段時間才會發生向磁碟真正的寫。這可能會對讀請求造成餓死。BFQ通過對寫請求更消耗預算來考慮這種代價,實際中一個寫相當於十個讀。
如果裝置內部可以排隊多個命令,那麼讓裝置idle可能會使內部佇列清空,從而造成吞吐量降低,因此BFQ會在能排隊命令的SSD上關閉idle。旋轉磁碟上也可以關閉idle,但是是在服務隨機IO時才會這麼做。
當多個程序訪問磁碟的同一區域時,最好能將它們的佇列合併而不是分開服務。一個很好的例子是QEMU,它會將IO分發給一組工作執行緒傳送。BFQ包含一個叫“early queue merge”的演算法會去探測這類程序並將它們的佇列合併服務。
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
很明顯,__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(): madvise(address, length, MADV_VOLATILE); 在呼叫該系統呼叫後,從address開始長度為length的記憶體隨時可能被作業系統回收,而記憶體中的資料將會被丟棄。應用程式如果需要訪問該段記憶體時,需要將其標記為不可散失: madvise(address, length, MADV_NONVOLATILE); 返回值為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(http://en.wikipedia.org/wiki/Voltage_and_frequency_scaling), 使問題加劇。ARM big.LITTLE(http://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