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


阿裏內核月報2017年01月

Controlling access to the memory cache

控製對Cache的訪問

cpu對內存的訪問一直以來都會通過L1/L2/L3緩存來加速,我們都知道當你打算嚴肅地去考察性能問題時,各級緩存的命中率一直是一個重要的指標。而一個進程的緩存命中率在很大程度上又和它在各級緩存中所占的空間大小正相關。由於緩存本身是socket範圍上的共享資源,一個進程的緩存命中率不僅取決於它自己的行為,同時也受和它共同運行的其他進程影響,這使得工程師們很難將一個特定進程的緩存命中率維持在理想的值上。

Intel自Haswell開始的cpu已經搭載了它們的緩存控製技術。Intel把它稱為Cache Allocation Technology,縮寫為CAT。在本屆LinuxCon 2016上,來自Intel的Yu Fenghua向大家匯報了將這一技術集成進Linux內核的進度情況。

那麼CAT具體是怎麼工作的呢?在一個普通的Intel X86 cpu上,對於一個N-way set associative cache(N路組相聯緩存)來說,當一個64B的cache line被帶入LLC時,它的一部分虛擬地址決定了它會被hash到哪個行(或者說set)中,具體的映射算法Intel保密沒有公開。進入一個set之後,放到哪個way(或者說列)上就按照正常的淘汰算法來進行,而在一個開啟了CAT的cpu上,這時會有一個掩碼(Intel稱它為CBM,Cache Bit Mask)介入,決定這個新cache line隻能放到哪幾個way上、隻能在哪幾個way的範圍之內進行淘汰。這樣就控製住了一個進程可以汙染的cache範圍。為了使用方便,這個掩碼並不會直接使用,而是通過和一個名為CLOSID(Class of Service ID)的值聯係在一起。Intel希望集成這一特性的OS在上下文切換時把新進程對應的CLOSID寫到一個特定的MSR寄存器裏,這樣CAT就流暢地跑起來了。

話雖如此,當Linux集成一個新特性的時候,考慮的可不僅僅是讓它跑起來這麼簡單。硬件廠商對於一個新特性的想法會持續地變化,一個通用操作係統的架構必須能夠在一定範圍內適應這種變化,它既要做好抽象,把細節屏蔽掉,使得這個框架能夠適應不同廠商的相似特性;又不能抽象得太過份,以至於某些特性無法得到支持。在集成CAT這個問題上我們再一次看到了類似的故事上演。最初的一版patch是基於cgroup寫的,經過幾輪review之後,社區的反對意見集成在cgroup這個事情本身-- CAT可以有兩種配置風格,一種是像上文說的那樣,以進程或者進程組為中心配置,這時使用cgroup是沒問題的;另一種是以cpu為中心配置,即我們指定給某一個cpu分配某些cache,跑在它上邊的東西,不管是進程的用戶態代碼,還是陷入內核後執行的內核代碼,還是中斷處理函數,都遵守相同的分配規則。這時就無法和cgroup協調起來了,因為整個cgroup的設計都是圍繞著進程視角來實現的。另外,CAT的掩碼作用範圍是在socket級別的,這就意味著操作係統可以允許一個進程跑在不同的socket上時使用不同的掩碼,cgroup抽象由於沒有辦法描述cpu,同樣抑製了這種用法。

放棄cgroup這個抽象之後,又有人提出了ioctl風格的接口,響應者寥寥。大家都不願意再往ioctl這個大雜燴裏加東西了。

最新一版的代碼以新提出的kernfs抽象為基礎,這是一個脫胎於sysfs的新的虛擬文件係統,cat的專屬目錄會出現在/sys/fs/resctrl下。在它之下有三個虛擬文件:tasks、cpus以及schema。tasks文件包含了被schema中的掩碼控製的進程PID。cpus則包含被這些掩碼控製的cpu編號。那麼當一個特定進程跑在一個特定cpu上,而它倆在schema中的配置衝突了該聽誰的呢?目前的策略是以cpu為中心的配置優先。

Yu Fenghua的演講最後提到了這一新特性具體的配置方法,這可以在patchset中找到;同時他的slides裏也提到了一些性能測試,從中可以看到對於一些cache競爭嚴重的場景CAT確實會有很大作用。

Context information in memory-allocation requests

本文要解決的是內核 api 設計在曆史演進過程中的合理化問題。

大家都知道,內核的內存分配過程中,是要清楚的區分各種不同的情況的,例如:是否內存分配過程中會引起進程切換,大部分的驅動代碼的中斷處理函數中(麵試中常問的問題,為什麼中斷上下文不能發生進程切換?)。所以,linux 內核的做法一直以來也就和我們大多數人在設計 api 的時候一樣,即:加個 flag 吧,於是內核的內存分配函數變成下麵的情況:

void *kmalloc(size_t size, gfp_t flags);

具體多少 flag, 看 <linux/gfp.h> 吧,處理內存分配中要區別處理的種種問題(這裏麵顯然有些問題是 common 的,有些是 by design 的)

現在考慮下麵三種情況:
1. 當調用棧足夠深的時候(內存分配 api 被多層封裝):
Amalloc(..., flags) -> Bmalloc (..., flags) -> Cmalloc (..., flags) -> ................... -> kmalloc(..., flags)
2. 當調用在某個上下文頻繁多次的時候:
kmalloc(..., flags) ... kmalloc(..., flags) ... kmalloc(..., flags) ...
3. 當底層調用需要修改 flags 的時候:
Amalloc (..., flags) -> kmalloc(..., new_flags) (比如:判斷自己的上下文發生變化了)

我們看到了什麼?很多重複的 flags 出現在連續的 api 調用裏,還有就是上層 flags 未必能完全決定底層 flags, 顯然功能都 ok 了,隻是過了 n 年之後,現在的開發者覺得太 ugly 了。

解決方法?大概就是很直觀的:
1. 拿掉 API 裏麵的這些 flags (或者提供新的 api 來過渡) 2. 最底層分配函數處理的時候,通過訪問某個全局的信息來拿到這個 flags 或者根據具體情況來調整這個 flags, 全部由底層調用決定

1 很簡單了,2 這個全局的信息存哪呢,社區給出的部分做法是 task_struck 裏麵了,當然這個沒問題,可以解決掉很多進程上下文的內存分配 flags 冗餘問題(暫且就叫 flags 冗餘吧),這應該是大頭,因為在中斷上下文中內存分配顯然要簡化很多,不過依然存在上述問題。

目前內核中給出了相應做法的例子:

PF_MEMALLOC_NOFS ==> task_struct -> flags
memalloc_nofs_save
... 這裏麵的內存分配,默認相當於增加 GFP_NOFS ...
memalloc_nofs_restore

目前解決方法存在的問題:
1. 我個人認為這也不是一個通用的方法,因為 flags 實在很多(就像我上麵提到的有些是 common 的,有些是 by design 的),如果都這樣單獨處理難道不就是用另外一種複雜度替換之? 2. 曆史問題太久遠,很多已有的 callers (users) 依然需要支持,或者在很長一段時間內兩種 api 要並存。 3. 並沒有解決中斷上下文的冗餘問題
本文原作者提出了這樣一個問題,也給出了一些解決方法,不過依然不完整,希望有興趣的同學繼續跟進內核社區在此處的進展,或者有更好的主意,也不妨自己改改。

Reworking kexec for signatures

kexec 可以用於從第一個內核切換到第二個內核,它用了 kexec_load() 這個係統調用,把新內核加載到內存中,然後用 reboot() 係統調用快速重啟;用戶態也有一個 kexec 命令,可以加載新內核然後啟動之。由於 kexec 跳過了固件加載和bootloader階段,它可以用於快速啟動,當然它最主要還是被用於 kdump 以生成 vmcore . 不過最近 mjg 發現個問題,kexec 可能繞過 UEFI 安全啟動的限製,簡單來說 UEFI 安全啟動機製要求內核必須是經過有效簽名的,而 kexec 啟動第二個內核的時候可以直接跳過 UEFI 的檢查,這樣就導致了安全問題,而一些公司比如微軟,覺察到這個風險之後可能會把用來簽 Linux bootloader 的 key 給禁掉,從而給 Linux 發行版帶來災難(比如說就不能裝 Linux 到一些微軟合作的廠商的電腦上),所以他們不得不把 kexec 禁掉了。

幸好 kdump 的開發者 Vivek Goyal 最近提交了一係列 patch 可以讓 kexec 隻啟動簽過名的內核,這樣應該就可以解決 mjg 的問題。不過 mjg 還是建議那些需要支持 Secure Boot 的發行版禁用 kexec,因為還有方法可以繞開 Secure Boot 的檢查限製,比如說改一下 sysfs 中的 sig_enforce 參數,然後跳回原內核。不過不管怎麼說, Vivek 一直在努力解決這個問題,以盡早消除這個安全風險。

Vivek 現在的 patch 裏,引入了一個新的係統調用:

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

和原來的的 kexec_load 係統調用對比:

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

新舊係統調用都會把內核分成不同的片段(segments),所不同的是,用了新的係統調用之後,隻會先加載簽過名的部分的片段,而不會像原來一樣什麼都不檢查直接把所有片段都加載到內存中;然後,kexec-tools 裏引入了一個新的工具,叫 "purgatory",它在舊內核調用 reboot 之後,新內核啟動之前檢查剩下那些片段的hash,驗證通過了才會啟動第二個內核。不過如果不是在起第二個內核,而是正常內核啟動的時候,也是需要 prgatory 的,這部分代碼也被 Vivek 加到了內核中。

這部分代碼目前還是 RFC 階段,還有很多要完成,比如最重要的是怎麼利用這個思想去驗證簽名。Vivek 也解釋了一下他的思路,主要是基於 David Howell 驗證內核模塊簽名的想法。大體上是在 kexec_load_file() 調用發生的時候進行簽名驗證,此時還會要計算每個片段的 SHA-256 hash,然後存到 purgatory 中。

社區對這批補丁的接受度還挺高的,估計過完年就能進主線了。

Enhancing lockdep with crossrelease

Lockdep 是一個運行時的鎖有效性檢查工具。它不僅僅是在死鎖事件發生後上報bug,而且支持潛在的死鎖檢測,非常有用。細節請參考kernel文檔:https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt

但是目前的實現有一些限製,死鎖檢測隻是用於一些經典鎖:例如spinlock,mutex,要求鎖的持有釋放均是在同一個上下文中。所以,lockdep可能會漏掉一些死鎖場景的檢測。例如,page lock 或者completions這類同步原語,它們允許在不同上下文中釋放(稱之為crosslock)。本次提交的crossrelease(指在不同的上下文中釋放)特性支持上述場景的死鎖檢測。

pagelock的一個死鎖案例如下:

       CONTEXT X      CONTEXT Y         CONTEXT Z
                      mutex_lock(A)
       lock_page(B)
                      lock_page(B)      
                                        mutex_lock(A) /* DEADLOCK */
                                        mutex_unlock(A)
                                        unlock_page(B) /* acquired by X */
                      unlock_page(B)
                      mutex_unlock(A)

首先在上下文Y中持有mutex A,等待page lock B;上下文X首先持有page lock B,在上下文Z中釋放,但Z在釋放page lock B前,需要先持有metex A,形成死鎖。
首先看一下lockdep的原理。定義A->B表示事件A依賴於事件B的發生,如果
同時存在B->A,那麼則形成了一個閉環,也就意味著死鎖。比如一個依賴圖如下所示:

   A-> B -> E <- D <- C

圖中的A,B,C,D,E都是lock class,箭頭表示依賴關係。當lockdep檢測到一種新的依賴關係時,比如E->C加入圖中,則形成了一個CDE閉環,即發生死鎖。lockdep往圖中添加的依賴關係越多,那麼檢測到死鎖的概率越高。如果去除傳統lockdep的約束:鎖的持有、釋放必須在同一個上下文中,那麼可以檢測到的依賴關係則將更加完善。

對比傳統實現,crossrelease新增了一步commit操作,整體的操作步驟如下所示:
1. Acquisition: 對於經典鎖,與之前一樣,放在task_struct的所屬隊列中。對於crosslock,則是添加至一個全局鏈表中。
2. Commit:對於經典鎖,無副作用。對於crosslock,則對上一步收集到的數據進行依賴檢查。
3. Release:對於經典鎖,與之前一樣。對於crosslock,則從全局鏈表中刪除。

所以crossrelease實現的關鍵點在於把per task的鎖隊列擴展到了一個全局的隊列,從而支持cross context release支持。當然,具體的實現還得考慮不同鎖的申請釋放時間點來判斷依賴關係,看作者的patch是使用了全局遞增的id來實現這一點。

結論:crossrelease特性為lockdep模塊增加了更多的依賴檢測場景,完善了死鎖檢查體係。相應的,比傳統實現也會更複雜。死鎖的場景很多,目前所能支持檢測的場景並不完備,相關的完善工作仍在繼續中。

BBR congestion control

擁塞算法允許網絡協(通常指TCP)可以達到任何給定連接的最大吞吐量,同時可以與其他用戶共享有效帶寬。BBR (Bottleneck Bandwidth and RTT)算法由Google發布,因此吸引了大量的關注。它用一種新的機製取代了傳統算法,嚐試在無線連接、中間設備幹預和緩存膨脹(bufferbloat)中獲得更好的結果。

對於給定連接,網絡沒有任何機製將有效的帶寬通知到端,任何擁塞算法都要解決這個問題。所以算法必須通過某種方式得知特定時間內可以發送多少數據。由於有效帶寬隨著時間一直變化,因此帶寬評估需要實時更新。換句話說,擁塞控製算法必須持續的評估多少數據可以被發送。

這些算法的測量標準是:沒有到達對端而不得不重傳的包的數目。當網絡“平穩運行”,丟包是極少發生的。一旦路由器的緩衝被填滿,則會開始將無法容納的包丟棄。丟包因此成為一個簡單可靠的信號,該信號指示連接超過了有效帶寬,需要進行降速處理。

這個方法的問題是在現有的網絡環境下,端到端的連接中緩衝區非常大。過大的緩衝區是近年來公認的問題,並且已經開始著手解決緩存膨脹問題。但是仍舊大量的路由器處於緩存膨脹狀態,並且一些鏈路層技術(例如WiFi)需要確定緩衝區的總量以便於性能優化。等到端已經發送出足夠的數據使得連接的緩衝區溢出,積累下來的數據緩衝區是非常巨大的。丟包信號來的太晚,當獲得丟包信號時,連接已經被超負荷工作了很長時間。

基於丟包的算法在遇到包錯誤的時候會產生問題。算法不必要的降速,使得有效帶寬沒有被充分利用。

BBR (Bottleneck Bandwidth and RTT)

BBR算法不同於那些關注於包丟棄的算法,相反它主要度量標準是發往遠端數據的真實帶寬。每次收到一個應答包,BBR都會更新數據發送總量。固定周期內累計發送數據量是一個很好的指標,該指標指示連接可以提供的帶寬。

當一個連接開始建立,BBR進入“startup”狀態;在這個模式下,它的行為類似於傳統擁塞控製算法,接著會不斷提升傳輸速度同時嚐試測量有效帶寬。大多數算法會持續的增加帶寬,直到丟包發生;BRR則是觀察上述的帶寬變化。尤其是觀察最後三次往返的發送帶寬是否發生變化。當帶寬停止上升,BBR認為它已經發現了連接的有效帶寬,同時停止增加數據發送帶寬,這比持續增加帶寬直到丟包要好得多。測量的帶寬被認為是連接的發送速率。

但是在測量速率的時候,BBR有可能在短時間內以較高的速率發送數據包,某些包將要被置入隊列中等待發送。為了排空這些被置入緩衝區的包,BBR將要進入一個“drain”狀態,在這個階段,發送速度會低於測量的帶寬,直到之前超額的包被發送完成。

一旦排空周期完成,BBR將進入穩定狀態,傳輸速率會在所計算的帶寬上下波動。“上下波動”是因為網絡連接的特性會隨時間變化,所以要持續的監控真實的發送帶寬。同時,有效帶寬的增加也隻能通過嚐試以更高的速率發送去檢測,所以BBR會在1/8的時間裏麵增加25%的速率去嚐試更高的帶寬。如果帶寬沒有增加,則接下來的1/8時間段會進入“drain”周期,排空之前發送的多餘包。

BBR不像大多數其他算法,它不用擁塞窗口作為主要控製組件。擁塞窗口限製了在給定的時間內在傳輸管道中的數據,窗口的增加會使得突發的包消耗新的有效帶寬。BBR 應用tc-fq(Fair Queue)包調度按照適當的速率發送數據。擁塞窗口仍然存在,但已經不再是主要的管理機製,它隻是為了保證沒有過多的數據存在傳輸管道中。

Virtually mapped stacks 2: thread_info strikes back

Virtually mapped kernel stacks
在我們熟悉的3.10內核中,內核棧是通過alloc_thread_info_node()函數直接從buddy system分配,即棧空間的物理地址連續。該方式有以下幾個不足:
1. 內核棧空間太小(4k for 32-bit system, 8K for 64-bit system),容易導致棧溢出;因此,在kernel 3.15版本之後,棧空間被擴大(8k for 32-bit system, 16K for 64-bit system)。
2. 當內存碎片化嚴重時,分配高階頁可能失敗,導致進程創建失敗;
3. 如果使用保護頁(guard page)防止棧溢出刷掉其鄰近頁,該保護頁將會占用一個物理頁,導致內存浪費,因此內核默認沒有開啟保護頁機製;
4. 在關閉保護頁機製時,缺乏手段檢測棧溢出,最終,往往是由於鄰近頁內容被篡改(memory corruption),導致內核panic
5. 存在潛在安全性問題,thread_info結構被放置在內核棧的底部,精心設計的內核棧溢出可能會修改thread_info,導致安全隱患;

基於上述不足,目前主流思路是通過vmalloc分配內核棧,該方案有效的解決上述缺點:
1. 便於內核棧擴張,且不會出現分配高階頁失敗而導致創建進程失敗的情況;
2. 保護頁不需要占用一個物理頁,隻需要在頁表上添加一個頁表項,並標記為禁止訪問;
3. 棧溢出能夠被保護頁及時檢查出來,溢出時鄰近的頁不會被篡改,因此隻需要kill當前進程,不會導致內核panic。這使內核棧溢出便於調試。

但是該方案存在幾個小問題,比如:
1. performace regression:即通過clone()創建一個進程會多消耗1.5微秒,在大量創建和銷毀進程的場景下,影響性能,Linus要求該patch進入upstream之前必須修複該問題。
2. TLB miss增加;對於64位係統來說,以前內核棧對應的虛擬地址屬於直接地址映射,在頁表中使用1G的大頁,因此隻需一個TLB entry就能裝下。而vmalloc對應的虛擬地址空間使用單頁映射;內核棧需要對應多個TLB entry。
3. 有些非常老的代碼竟然在內核棧中執行DMA操作,這些代碼需要重寫

其中最值得關注的是一個問題如何解決這個regression:

很顯然主要是由vmalloc導致了performace regression,調用vmalloc()比alloc_pages()這類函數代價更高。很自然想到的一種方法就是,預先分配一定數量的內核棧數據結構。
作者驚奇的發現,這不能解決問題,進程退出時並沒有立刻釋放資源(包括內核棧)。因為使用了RCU機製來保證該進程的資源在釋放前沒有被其它代碼應用,隻有在下一個RCU grace period,這些資源才會被釋放。

這造成一個效果:在大量創建和銷毀進程的場景下,會導致內核棧結構被批量申請,然後批量釋放;由於釋放的數量超出緩存管理的上限,超出的內核棧結構被直接丟棄。這導致內核棧緩存效果很差。

原則上來說,進程退出後,其資源不應該再被其它代碼應用,但由於曆史原因,thread_info中的一些數據仍然被應用,而thread_info 被放在內核棧的底部,連累內核棧所占的空間也不能被直接釋放。

如果thread_info與內核棧完全獨立開,這個問題就能愉快的解決了。因此,開發者正在想辦法將thread_info中的數據結構挪動到 task_struct中去。thread_info在內核代碼中被大量用到,顯然這不是一個簡單的工作。

A way forward for BFQ

BFQ(Budget Fair Queuing)I/O調度器自2014年起被內核社區討論了無數次。通過修改調度算法、添加很多啟發式策略,BFQ能夠讓I/O設備提供更好的響應,特別是針對傳統的機械硬盤設備。盡管有這些明顯的優勢,但BFQ一直沒有被內核主線接受。然而最近的一些信息顯示阻礙BFQ進入主線代碼的障礙就要被移除了。

過去幾年對於BFQ調度器的反對主要集中在如下幾點。首先BFQ剛開始嚐試進入內核主線的時候,內核中已經有很多I/O調度器了,大家都認為應該將BFQ的功能集成到已有的CFQ調度器中。2016年2月,BFQ作者嚐試將BFQ功能添加到CFQ中,但是這一嚐試存在很多缺陷,後來就不了了之了。此外,大家認為用BFQ來替換已經經過大量使用和測試的CFQ是不妥當的。而最大的問題是,BFQ使用的是此前的內核API,而這些API並不支持多隊列。

多隊列功能是用來解決塊設備擴展性問題的一個特性。該特性允許I/O請求被放入不同CPU的隊列中並被這些CPU處理。因此內核開發者當然希望盡可能多的塊設備代碼能夠建立在這些已有的架構上。BFQ利用新的多隊列API存在一個問題,即當前的多隊列不支持I/O調度器。因為多隊列特性開發之初主要為了解決高性能塊設備的擴展性問題,缺少對傳統機械硬盤的考慮。因此,在此基礎上集成一個I/O調度器的工作確實有一定的困難。

當然如果傳統機械硬盤會被慢慢淘汰,那麼我們也不用考慮這一問題了。但現實是傳統機械硬盤還要繼續存在很長一段時間。因此為多隊列代碼提供一個I/O調度器是有意義的一件事。
最近Jens Axboe開始嚐試在多隊列代碼中添加I/O調度器的支持。同時BFQ的作者也開始嚐試將BFQ調度器移植到最新的代碼上。

最後我們要說,BFQ即將進入內核主線!

Debating the value of XDP

XDP價值大討論

內核各個子係統的設計都麵臨擴展性和性能的要求,網絡子係統走在了前麵。最近出現了一種XDP(express data patch)的計數開始出現,然而評論褒貶不一。XDP大致想對收方向的packets做出一些簡單快速判斷,它最早被希望應用在丟棄不需要的包上,隨後加上了簡單路由和包修改的功能。實現上內核通過加載BPF程序,在包進入內核協議棧之前做出判斷。

很多關於XDP的討論都集中在XDP的實現方麵,而不是XDP設計本身。直到12月初,Florian Westphal在給Hannes Frederic Sowa的回信中說到,“netdev分支開始出現大量關於XDP的patch,我覺得應該停一下了”。他寧願開發者解決那些網絡子係統正在麵臨的問題,而不是那些“很有意義但是意義不大”事情。

這些事情,(包括DPDK)用用戶態程序來bypass網絡協議棧,需要使用定義限製嚴格的一套機製,並且把通用內核協議棧帶來的好處全部拋棄,來換取性能上的提升。他還說,這種情況下用硬件提供的包過濾功能比較合適。Westphal還認為XDP是一種比較low的機製。用戶態的網絡代碼好歹可以用各種語言編寫,調試方便等等。相比之下,BPF開發不方便,功能上也更受限製。用XDP來解決的路由、負載均衡和先期包過濾等功能,各自都有更好的解決方式。

Thomas Graf提到:packet出了內核之後一切就變得不可控了,在用戶態做安全控製就不合適了,用戶態代碼可能被破壞。內核裏麵的BPF代碼應該更難被破壞,他也不同意負載均衡這樣的事情在用戶態來做。

Sowa提到:在早期包丟棄這個應用場景下,用硬件做丟包已經可以解決問題,使用XDP有什麼好處呢?Herbert解釋了這個問題,靈活性和高性能都是需要的:麵對DDOS攻擊的時候XDP非常有幫助,XDP讓係統遭受攻擊時可以盡快丟包,可以影響到協議棧,可編程對越來越雞賊的攻擊者來說也非常必要。僅僅用硬件解決方案在這個問題上遠遠不夠。

網絡模塊maintainer David Miller也認為XDP應用在丟包場景非常合適,硬件方案不足以解決問題。

Sowa還提到了不太好解決的API問題,隨著XDP的發展會用到越來越多內核用戶態的ABI。反過來ABI會限製網絡協議棧的發展。

statx() v3

某些開發到取得成果耗費了很長時間。這在提議的statx()係統調用上得到了驗證——至少,很長時間是肯定的,盡管我們依舊需要等待其開花結果。不過從大部分描述來看,這個stat()係統調用擴展將接近ready。近期的補丁顯示出當前statx()的狀態以及遺留的阻塞點(sticking point)。

stat()係統調用用於返回文件的元數據。它有著悠久的曆史,早在1971年的Unix發行版第一版就首次登場。在接下來的45年裏stat()很少變更,甚至操作係統其餘部分圍繞它變更。因此,它毫無疑問趨向不符合當前的需求。現在,它無法表示文件的關聯信息,包括generation和版本號,文件創建時間,加密狀態,是否存儲在遠程服務器上,等等。它使得調用者無法選擇獲取何種具體信息,甚至可能強製耗時的操作獲取應用並不需要的數據。同時,時間戳域又有著year-2038問題。等等。

David Howells自2010開始零星地致力於替換stat()的工作,他的第3版Patch(算上他今年早些時候重啟該項努力)於11月23日發布。而提議的statx()係統調用看上去和5月份的基本一樣,存在少許變更。

statx()的原型依舊是:

<source lang="c">int statx(int dfd, const char *filename, unsigned atflag, unsigned mask, struct statx *buffer);
</source>

通常地,dfd表示一個目錄的文件描述符,filename表示對應的文件名;該文件可通過給定目錄的相對路徑查找到。如果filename傳入為空,則dfd被解釋成需要查詢的文件。因此,statx()替代了stat()和fstat()的功能。

atflag參數修飾該係統調用的行為。它處理了幾個當前內核已存在的標記:AT_SYMLINK_NOFOLLOW,用於返回符號連接信息而不是進一步跟蹤它;AT_NO_AUTOMOUNT,用於阻止遠程文件係統自動掛載。一組statx()專用的新標記控製著與遠程服務器的數據同步,允許應用程序調節IO和精確結果之間的平衡。AT_STATX_FORCE_SYNC將強製與遠程服務器的同步,即使本地內核認為其信息是最新的;而AT_STATX_DONT_SYNC則隱含著快速獲取遠程服務器的查詢結果,但可能已過時甚至完全不可用。

因此,atflag參數控製著statx()將如何獲數據;而mask則控製著獲取何種數據。可用的標記允許應用程序請求文件權限,類型,連接數,所有權,時間戳等。特別值STATX_BASIC_STATS返回stat()將返回的所有信息,而STATX_ALL則返回所有可用的信息。降低請求的信息量可能減少執行該係統調用的IO數,但一些檢視者擔心開發者將直接用STATX_ALL以避免需要更多的思考。

最後的參數buffer包含需要填充關聯信息的結構體;該補丁版本中結構體如下:


<source lang="c">struct statx { __u32   stx_mask;   /* What results were written [uncond] */ __u32  stx_blksize;    /* Preferred general I/O size [uncond] */ __u64 stx_attributes; /* Flags conveying information about the file [uncond] */ __u32 stx_nlink;  /* Number of hard links */ __u32 stx_uid;   /* User ID of owner */ __u32    stx_gid;    /* Group ID of owner */ __u16   stx_mode;   /* File mode */ __u16   __spare0[1]; __u64  stx_ino;    /* Inode number */ __u64    stx_size;   /* File size */ __u64   stx_blocks; /* Number of 512-byte blocks allocated */ __u64 __spare1[1]; struct statx_timestamp stx_atime;  /* Last access time */ struct statx_timestamp   stx_btime;  /* File creation time */ struct statx_timestamp stx_ctime;  /* Last attribute change time */ struct statx_timestamp stx_mtime;  /* Last data modification time */ __u32 stx_rdev_major; /* Device ID of special file [if bdev/cdev] */ __u32    stx_rdev_minor; __u32   stx_dev_major;  /* ID of device containing file [uncond] */ __u32 stx_dev_minor; __u64  __spare2[14];   /* Spare space for future expansion */ };</source>

這裏stx_mask表示實際有效的域,它將是應用程序請求的信息與文件係統實際能提供的信息之間的交集。stx_attributes包含描述文件狀態的標記,他們表示文件是否被壓縮,加密,不可變,隻允許追加,不包含在備份中,或者自動掛載點等。
時間戳域結構體:

<source lang="c">struct statx_timestamp { __s64 tv_sec; __s32   tv_nsec; __s32  __reserved; };</source>

__reserved是第3版基於近期討論針對statx()的一個強烈反對觀點加進來的。Dave Chinner建議,在將來的某個時間點,納秒精度將不再適用,他指出該接口應當能夠處理飛秒時間戳。幾乎隻有他一個人持有該觀點,而其餘參與者,如Alan Cox,指出光速將保證我們永遠不需要納秒精度以下的時間戳。但Chinner堅持自己的觀點,因此Howells新增__reserved以備將來所需。
Chinner針對該接口還有若幹其他反對意見,其中一部分尚未處理。這些包括STATX_ATTR_標記的定義通過FS_IOC_GETFLAGS和FS_IOC_SETFLAGS ioctl()係統調用屏蔽了對一組現有標記的使用。重用這些標記給予statx()代碼的微小優化,但將繼續保持過去造成的部分接口錯誤,Chinner說。Ted Ts‘o在檢視2015版補丁集時提供過類似的建議,但第3版保持著同樣的標記定義。

Chinner的最大反對意見在於,statx()缺少綜合測試用例。他認為這些代碼在測試用例提供之前不能進入主幹。

非常坦白地說,我認為綜合測試用例對這樣一個通用、可擴展的新係統調用功能來說是無條件的。要麼我們在合入前做到測試覆蓋,要麼我們不合入。我們一次又一次地證明沒有經過測試的垃圾是無法工作的(shit doesn’t work if it’s not tested),並且無法被獨立的文件係統開發者廣泛驗證。

該態度近期也被其他人所回應,如Michael Kerrisk。內核確有一段很長的曆史合入過無法像其宣稱那樣工作的新係統調用,並得到了應有的懲罰。Howells將提供類似的測試用例,但目前沒有。

在此之上發生的眾多有爭議的話題(bikeshedding),我很慶幸我目前還沒有完成這個測試套件,它將至少有著兩倍以上的工作。我仍然不知道最終的形式將會是什麼。

該補丁集的變更看上去慢下來了,也許最終的版本開始成為焦點。但是,該項工作的曆史暗示我們預期它在近期合入是不明智的。stat()係統調用伴隨了我們很長時間,因此期望statx()將持續同樣長時間也是合理的。一些額外做好該接口的“具有爭議的話題(bikeshedding)”,也是可以理解的。

Topics in live kernel patching

在內核主線提供熱補丁的能力是個長期的過程。4.0版本開始就已經集成了基本的熱補丁功能,但是更多的支持阻塞在一致性模型(代碼保證一個熱補丁應用到一個正在運行的內核上是安全的)該如何工作上麵。此外,kernel stack validation一文提出了最大的反對意見,因此,現在是時候向前走了。在2016年Linux Plumbers Conference上麵,熱補丁方向的內核開發者們坐在一起討論內核熱補丁當前的挑戰以及未來的方向。

這篇文章並不是那個為時半天的討論的一個總結,而是想通過展示熱補丁開發者所麵臨的一些挑戰以及他們準備如何應對這些挑戰來聊一聊一些更有趣的話題。

幫倒忙的編譯器優化

一個聰明的編譯器對於每個想要他們的代碼獲得可觀的性能的人來說都很重要,但是,當編譯器變得過分聰明的時,就會有另一些問題。譬如,有時候跟並發執行的內核打交道的開發者不得不擔憂編譯器的過分優化。Miroslav Benes說,內核熱補丁的開發者也得擔心這些問題。編譯器優化可能會在一些微妙的方式上改變代碼編譯的方式,以至於當熱補丁應用上的時候會導致錯亂。

我們從最簡單的問題開始,之後在討論那些比較tricky的。Benes發現當需要對一個內聯函數打patch時,自動inlining功能會是個問題。不過這種情況的解決方法相對比較簡答,對於所有的要patch的內聯函數的調用者統統都需要變。gcc的-fpartial-inlining選項會導致這個問題變得複雜,但是並不會改變這個問題的本質。

-fipa-sra選項相對來說更加微妙一些,它可能導致刪除一些沒有用到的函數參數,或者改變函數參數傳遞的方式。也就是說,對於函數的調用者來說,它改變了函數的ABI。對於這種函數的熱補丁會改變這種-fipa-sra優化的工作方式(譬如之前優化是會刪除某個沒有用到的參數,打了熱補丁之後,這個參數就需要用到了,優化就會保留這個參數),因此可能會導致ABI出現奇怪的改變。好消息是,當這種情況發生的時候,GCC同時會改變被編譯的函數的名字,這樣,被破壞的ABI會立馬顯現出來。但是,這樣導致不能直接給一個有bug的函數打補丁,這個函數的調用者也就得跟著打patch了。

帶-fipa-pure-const編譯的代碼可能會改變一個函數工作的方式;如果一個函數看起來並不會訪問主存,編譯器會假設主存的狀態在這個函數調用前後不會發生變化。當一個熱補丁改變了這個函數的行為時,這些假設可能不在成立;再一次,這個函數的所有調用者又得全部跟著都打一次補丁。

一個更加”瘋狂“的選項是-fipa-icf,這個選項會執行相同代碼折疊(Identical Code Folding)。簡單來說,它會把擁有相同功能的函數合並成一個,這個功能可以減小代碼量,但是很難檢測某個函數是否被”折疊“了。代碼折疊對於內核stack unwinder來說也是個問題。另外,還有一些其他問題。譬如,當一個GCC認為一個函數不會改變某個全局變量時。當一個函數打了patch後會改變這個全局變量,調用這個函數的代碼可能就不對了。這種類型的問題,同樣很難檢測。或者如果GCC有一個選項,可以要求它對於它做的優化創建一個日誌會更好一些。

或許最嚇人的選項是-fipa-ra,這個選項追蹤調用的函數所使用的寄存器,並且避免去保存這些不會被改變的寄存器的值。對於這種函數打補丁很容易會導致這個函數使用新的寄存器,從而導致調用者數據錯亂,並且很可能大幅減少熱補丁開發者期望熱補丁工作的時間。這個選項很難被檢測到,同時,它也可以看做是一種的ABI的改變,但是函數名卻沒有變。當前,這個優化選項在GCC開啟了-pg時候會被禁用,並且Ftrace子係統需要-pg選項來支持熱補丁。但是-pg和--fipa-ra選項並沒有內在的原因導致兩者不兼容,因此,這個行為可能在任何某個時候會被修改。

Miroslav說,上述幾點,隻是編譯器優化對於熱補丁技術所帶來的問題的一個小的子集。隨著編譯器開發者追求更加激進的優化,這些問題會變得越來越嚴重。

補丁構建

內核有一個標準的方式應用一個熱補丁,但沒有任何類型的主線機製來創建熱補丁。Josh Poimboeuf給出了一些補丁製作工具的簡要總結,著眼於選擇一個用於upstream。

第一個工具是kpatch-build。它的工作原理是構建有補丁應用和沒補丁應用的內核,然後比較二進製diff看哪些功能改變。所有改變功能的提取和包裝成一個“Josh Poimboeuf 內核模塊”,而這個模塊是隨附著熱補丁的。他說,這是一個給力的係統,具有許多優點,包括它自動處理前麵提到的大多數優化問題。另一方麵,kpatch-build相當複雜。它必須知道內核所使用的所有特殊部分(special sections),以及它對某些類型的更改是有問題的.。目前它隻能運行中x86_64體係結構,對於不同的體係結構,這些特殊部分(special sections)也不同,所以把kpatch-build變成一個多架構的工具並不容易。而且,他說,kpatch建立是易碎的,甚至是一場噩夢。

另一種方法是使用常規的內核構建係統和它的模塊構建基礎設施。將改變的函數複製並粘貼到一個新的模塊,增加一些樣板並用熱補丁API注冊函數,最終完成任務。這種方式很容易,但有它自己的問題,特別是這個模塊是無法訪問非導出的符號,而這些符號可能會被打補丁的函數使用到。這個問題可以通過使用kallsyms_lookup_name()身邊工作,但是這個解決方案容易出錯,速度慢,還有點“惡心”。

第三種方法有點新,事實上,他在會議召開前一周就發布了這個建議。這種方法使用複製和粘貼的方法,但增加了一個API和後處理工具,可以使生成的模塊能夠訪問非導出的符號。目前這種方法有效,盡管現在還有許多可以改進的地方,包括自動連接到非導出符號的過程和檢測編譯器優化的幹擾。

在演講結束時的簡短討論中,很明顯可以發現,人們對新的工具沒有太多的關注,因此這可能就是要發展的方向。

模塊依賴

內核熱補丁技術可以修改內核模塊以便修複其中的代碼錯誤。而這帶來了一個有趣的問題:如果需要修複的內核模塊在補丁模塊加載時沒有到加載係統中,而後又被人為加載了該如何處理。當前內核熱補丁技術實現了一套複雜的基礎架構來監測這一問題並在內核模塊加載時一並加載補丁模塊。Jessica Yu是當前內核模塊加載子係統的維護者,她在會議上介紹了當前這一機製對內核模塊加載的影響。

補丁模塊自己其實也是一個內核模塊。允許這樣一個補丁模塊加載時先加載問題模塊本身需要模塊攜帶很多額外的信息,並需要一套複雜的架構來實現這一功能。此外,這樣一套基礎架構還跳過了當前內核模塊加載的依賴機製,包括在熱補丁相關代碼中幾乎重寫了一遍內核模塊加載器。

當然還有其他的方法來解決這一問題。一種方式是簡單的要求問題模塊必須在補丁模塊加載前先行加載。這樣做當然是可以解決問題的,但缺點是這樣會加載很多無用的內核模塊。另外一種可能的解決方法是將補丁模塊根據修複的問題模塊做拆分,拆分成多個不同的補丁模塊,根據當前係統中加載的問題模塊加載對應的補丁模塊。

第一種方式可以極大地簡化內核熱補丁模塊的相關代碼,同時減小代碼冗餘。但有個問題缺不容易解決。即如何強製加載需要打補丁的所有問題模塊。depmod工具無法識別這種依賴。FreeBSD係統上有一個MODULE_DEPEND()宏可以完美的處理此問題,但是該宏定義在Linux上並沒有實現。

第二種拆分補丁模塊的方式對於影響麵很大的補丁並不適用。以CVE-2016-7097為例,該補丁修複了一處位於VFS層API上的安全漏洞。但這一補丁會涉及所有文件係統模塊。如果將這一補丁模塊進行拆分,結果則是生成一長串模塊列表。

在一係列的討論中,Steve Rostedt提出了一個觀點,既然問題模塊並沒有加載,為什麼不直接將磁盤上的問題模塊替換成修複後的新模塊。而Jiri Kosina的回答是這樣會造成發行版軟件包管理上的混亂。即內核模塊和其對應的軟件包版本並不一致。而這樣的不一致會造成很多麻煩。同時新模塊並不像補丁模塊一樣可以隨時回退,把磁盤上已經替換掉的問題模塊恢複回來也是個麻煩事。盡管如此,Rostdt依然堅持認為直接替換磁盤上的內核模塊才是問題的解決之道。

討論的最後大家達成的一致意見是當前內核熱補丁對於模塊依賴問題的解決方法已經足夠了。

其他話題

Petr Mladek討論了關於數據結構修改的問題。對於全局變量這類數據結構的修改還是比較容易的。困難的是串聯在多個鏈表上的數據結構的修改,這幾乎是不太可能直接通過類型的轉換來做到的。對於這類數據結構的修改要十分小心。現在有一些技術來嚐試解決這一問題,比如對於需要修改的數據結構添加影子結構。但是這樣會造成額外的性能開銷。同時當補丁模塊卸載時的處理也十分複雜。

Miroslav Benes討論了關於一些調度器相關代碼的修改。比如schedule()就是一個很棘手的函數。因為這個函數在返回前後可能調用者的調用棧已經改變了。

他給出了一個解決的方法是在上下文切換時將指令指針保存到上線文中。這樣補丁模塊在加載後可以通過這一信息判斷schedule()前後的上線文信息是否一致。這一方式被證明是可以工作的,但是是否值得就另當別論了。畢竟schedule()極少出現問題。

Jiri Kosina討論了內核熱補丁未來的工作。其中之一是一致性模型。現在的棧驗證工作已經完成,內核棧回溯機製是可信的,因此一致性模型的相關工作可以繼續開展了。

特別是混合一致性模型可以繼續推進。但是還有另外一個問題,為了構建確保棧信息可靠,需要內核編譯時打開棧針。而這會帶來約10%的性能開銷。目前還不知道打開這一選項為什麼會帶來如此巨大的性能損失。Mel Gorman正在跟進這一問題。

Kosina說當前正在將內核熱補丁移植到arm64平台上。而其他相關工作也在有序開展中。
此外會中還討論了PowerPC的相關問題,以及用戶態熱補丁的問題。

Balbir Singh提出是否真的需要在集群中使用熱補丁技術?如果一個集群可以分批下線進行升級,那麼熱補丁技術其實並不是必須的。當然對於用戶來說熱補丁技術可能確實是需要。

最後一個問題是關於rootkits的。一個可以將指定代碼注入到運行的內核中的工具。Kosina說他並不擔心這個工具。因為內核熱補丁本身就是內核模塊,如果攻擊者可以加載一個內核模塊,那他也可以做任何他想做的事情。但Singh說,如果熱補丁本身存在缺陷怎麼辦。如果修了一個安全問題卻引入了新的安全問題呢。

A discussion on virtual-memory topics

VM子係統現在還有哪些問題? 基於這個話題,Mel Gorman(目前工作在suse的performance group,主要關注在提升小塊內存分配性能提升上)、Rik van Riel(主要關注點在qemu裏麵通過使用persistent-memory技術將page cache從虛擬機裏麵移到宿主機上,便於管理和共享)、Johannes Weiner(主要工作是提升page-cache的抖動檢測)、Vlastimil Babka(主要關注點是高order頁的分配,一方麵是減少不必要的高order分配,virtually mapped kernel stacks這套patch使得棧分配這塊已經不再需要使用高order內存,另一方麵是優化內存compaction和減少內存碎片)四位用了半個小時就此展開討論,總結下主要在如下兩個方麵:

  • swap相關困擾
  • 使用zram將內存swap到硬件設備時,其中有2/3的cpu消耗是在內存壓縮,而另外1/3的cpu消耗是其他部分導致的,當內存壓縮移植到硬件裏麵之後,另外的這1/3就是大頭,需要定位清楚這部分消耗並做相關優化。
  • swap對快速設備路徑支持這塊當前存在一係列問題(Johannes):
   a. 換出路徑的全局鎖
   b. vm子係統即使在page cache頻繁被訪問時也還是會傾向於回收page cache,而不是考慮進行swap
   c. 換出路徑對hugepage的拆分

Mel對這塊總結是swap現在已經running into walls,需要重新思考下,這類問題大概需要6~24個月來進行fix。

  • 詭異的shrinker

brtfs一位開發者做了一個實驗,將係統80%的內存用來緩存inode和dentry,同時注冊了shrinker保證這部分內存能夠管理,結果觀察到的現象是大部分時候都在掃描page cache,而當前係統中並沒有page cache,導致這些掃描都是無用的,同時注冊的shrinker並沒有被告知盡可能多的釋放內存。ext4的開發者在extent status slab cache這塊也碰到類似的問題,導致這個cache可能變得特別越大;同時在多個shrinker並發執行時還會有spinlock競爭問題。

Rik談到VM子係統對此唯一能做的是調用相應的shrinker,但是對slab緩存,一個page可能包含很多個object,隻有當這個page上麵所有的object都free了,對應這個page才能夠被free,有可能大量的object被回收了,但是實際係統可用內存並沒有增加多少。針對這個,有人提出需要有方式能夠區分幹淨的object(能夠立即回收的)和髒的object(需要先回寫),同時需要有一個基於page的shrinker,能夠快速找到所有object都是幹淨的page。

Mel建議需要增加一個幫助接口,給shrinker用來找到屬於同一個page的所有object,這樣可以一起釋放。對於鎖競爭,一種方法是限製direct reclaim並發線程數,另外一種就是發現有人持鎖時快速的回避(持鎖者能夠回收出相應的內存)

還有一個問題,有一些object會將其他的pin在內存中,比如inode會pin對應的dentry,dentry會pin父目錄的dentry。linus建議將葉子節點(類似普通文件)的dentry和目錄的dentry分開來,葉子節點的dentry更容易被回收,因此將它們放到一塊,這樣可以增加釋放整個page的概率。這樣做唯一的問題是:內核在分配內存的時候並不能區分dentry的類型。Mel建議文件係統在發現放置錯誤之後重新再分配另外一個dentry,然後將數據拷貝過去。

最後Tim Chen介紹了下他的swap optimization work,由於persistent memory可以直接尋址,內核可以將換出的page直接映射到進程地址空間,避免重新swap回內存。但是這種方式如果對於頁訪問太頻繁會有性能影響,因此需要有個機製能夠評判什麼時候需要swap回內存。對於普通內存有lru表,單對於persistent memory隻有page table裏麵的access bit。到討論最後這塊目前還沒有解決方案。

最後更新:2017-06-07 16:01:49

  上一篇:go  6月7日雲棲精選夜讀:Spring-beans架構設計原理
  下一篇:go  第三屆 PHP 全球開發者大會 2017·北京