阿里linux內核月報2014-07-08

Capsicum for Linux
Capsicum: 一種基於文件句柄的新安全模型
Capsicum是一種源自FreeBSD的安全模型,與Linux下衆多LSM的相同之處在於它們都是基於權限管理的,而不同之處在於LSM針對的操作對象非常豐富,有進程、VMA、端口、帶有標籤的文件等等,而Capsicum操作的對象非常單一:文件句柄。例如,一個fd必須帶有CAP_READ才能被讀取,必須帶有CAP_SEEK才能被lseek(),必須帶有CAP_MMAP_W才能被mmap()建立可寫映射,針對ioctl()和fcntl()它還有一些特殊約定的權限。
可以想象,既然這些限制都是綁定在某些fd上的,那麼如果一個被限制的進程可以隨意地打開新的fd操作文件,這些限制自然就沒什麼用處了。爲解決這個問題,Capsicum引入了一個名爲cap_enter()的操作,一個進程執行cap_enter()之後它基本就不能再訪問文件系統的全局名字空間了,因此只能使用在cap_enter()之前已經打開的並且被設置好了權限約束的句柄。但是cap_entery()這個操作在Capsicum的第一版patchset中並沒有被實現,只是提出了這個概念而已。
在內核裏,用戶空間傳來的fd會餵給fdget(),再由它返回struct file *,Capsicum的hook點就在這裏。作者定義了一個struct file的wrapper結構,包含原始的struct file和額外的一些權限信息;然後提出了一個fdget()的新變種,形式如下:

struct file *fdgetr(unsigned int fd, int caps …);
注意到它還是一個參數數量可變的函數,所有的cap會由最後一串參數傳入。內核中原先調用fdget()的大約100個調用者都需要改成這個新接口,同時調用者還得處理新接口的返回值。因爲原先的fdget()在出錯時只返回NULL,不會有進一步的錯誤值返回,而fdgetr()的錯誤返回值要豐富得多,這意味着這個patchset侵入性相當強,估計很難被接受。 目前Capsicum是基於LSM框架之上實現的,有評論認爲Capsicum與LSM的耦合性很低,完全可以抽出來獨立實現。另有評論認爲Capsicum完全可以由新的seccomp-bpf實現,不需要額外加patch。考慮到用seccomp-bpf寫代碼很麻煩,實現這些功能肯定不會簡單,但這麼做應該是可行的。總的來說,大家普遍覺得Capsicum這套patchset想被接受是相當困難。主要的優勢在於FreeBSD既然已經有了這種安全模型,那麼可能會方便一些FreeBSD上的代碼移植到Linux上來,如此而已。

Control groups, part 1: On the history of process grouping
即便Control groups不是Linux中最具爭議的特性,在各種郵件列表和論壇上隨處可見對control groups熱火朝天的討論,甚至完全否認特性的價值.這一系列的文章介紹圍繞control group(cgroups)的爭議.

要理解這些爭議,既需要廣闊的視野,也需要對詳細分析.前兩篇文章通過介紹Unix的歷史,分析cgroups給進程組帶來了什麼問題.然後分析cgroups的層次結構,藉助Unix和Unix之外的系統,爲衡量cgroups的層次結構提供標準.
後幾篇文章深入分析cgroups.

第六版的Unix
歷史上,Unix對進程組的管理經歷了一些演變.瞭解這些演變對我們很有幫助,這裏不從最初介紹演變過程,而是從第六版的Unix開始(後面叫”V6 Unix”).

V6 Unix誕生自20世紀70年代中期,是走出Bell實驗室獲得巨大關注的第一版.它支持兩種進程分組管理,開始之前我們先說明”進程分組”的含義.

(作者借數論裏面的羣group介紹這裏的process groups)在數論中,不是所有的集合都是羣(group).一組能用質數唯一標識的進程是一個組.然而在Unix中(無論是當時或是現在),不存在把這種進程組和其他以合數標識的進程組區分開的方法.另外的進程,既不能用質數標識也不能用合數標識的進程,其行爲與這兩組也有區別.由於它僅僅包含1號進程,不單獨把它考慮爲一個進程組.

數論中的羣,包含基於羣中元素的操作.類似的,對於進程組來說也需要與進程組相關的操作.
另一種不太直接的分組方法是根據進程所有者的ID(UID).這並不是V6 Unix一個真正的組,儘管有些操作對一些組和另外的組的影響是有區別的,但它們不是一個整體.
一種真正意義上的組是進程的子進程組.V6 Unix中與子進程組相關的唯一操作是wait()系統調用,它檢查這個組是否爲空.如果wait()系統調用返回ECHILD,那麼這個組爲空.如果wait()不返回或者正常返回,那麼系統調用時這個組不爲空.

類似的操作也可以在進程的後代進程上定義,即進程的子進程和其他後代進程.當且僅當沒有這樣的進程,wait()系統調用返回ECHILD.需要注意的是,子進程組中進程只能在執行完成的時候才能從組中退出.在後代組中,它的任何一個祖先進程執行完成,這個進程就可以退出組.
進程能否退出組是不是一個重要特點取決於具體情況.在V6 Unix中,1號進程的後代不能退出,而其他進程的後代可以.這種情況延續到了類Unix系統Linux,一直持續到Linux3.4在prctl()中加入了PR_SET_CHILD_SUBREAPER選項.它允許限制子進程退出組.一個進程停止時,它的子進程被這時這個選項的進程繼承.

V6 Unix中的另一種控制組,是進程結構中的p_ttyp域指定的”controlling tty”.當進程打開一個tty設備時(通常是一個串口數據連接),如果這個域沒有被設置,它會被設置指向一個新打開的設備.這個域可以通過fork()或者exec()繼承而來,因此一旦進程打開了設備,它的子孫後代也會繼承這個設備.

p_ttyp的作用是定向所有到/dev/tty的I/O到controlling tty.由於它對每個進程的影響是獨立的,這不是使進程成爲組的原因.controlling tty使進程成爲組的原因與信號處理有關係.當DEL或則FS(control-)被輸出到tty,SIGINT或者SIGQIT信號被送到組中所有使用這個tty作爲controlling tty的進程.類似的,如果鏈接中斷,SIGHUP信號也被送到組中的所有進程.還可以使用sigkill()系統調用發送信號.發送到0號進程的信號也會被髮送到與發送進程具有同樣controlling tty的進程.

這種組策略已經與control groups非常類似了.儘管只通過信號傳遞體現出來,進程的分組和管理已經非常明顯.組是自動產生的,與行爲有關,並且是永久的(一旦成爲組成員,進程不能脫離).但這並不完美,下一節介紹其改進.

第七版Unix
雖然V6 Unix已經支持process groups,卻沒有提出這一術語.V7 Unix中不僅提出這個名詞,還豐富了group的含義.p_ttyp域仍然存在,它對/dev/tty訪問的管理被限制了.它被重命名爲u_ttyp,並且被移動到struct user中,user結構體可能隨進程的其他部分被換到磁盤中.proc結構體使用新的p_pgrp域來管理process groups.在第一次使用open()打開tty的時候p_pgrp域被置位,它被用來傳遞SIGINT,SIGQUIT和SIGHUP信號,也被用來向0號進程傳遞信號.V7帶來很多複雜的變化.

最大的變化是使process groups有了獨立的名字,至少與tty無關.一個沒有controlling tty的進程第一次打開tty時,會以進程ID爲名字創建一個process group.當進程退出時,這個組仍然可以存在.活動的子進程可以防止ID被重用.

這麼做的結果是,從tty退出後重新登陸時,會新創建一個process group,tty結構體中的t_pgrp域會被修改.與V6 Unix不同,送到一個進程組的信號決不會被送到同一個tty已經登陸的進程上.

另一個影響是process groups可以被用在tty之外.第七版的Unix有一個短暫存在的”multiplexor driver”,現在仍在stat()的變更手冊裏

3000 S_IFMPC 030000 multiplexed character special (V7)
[…]
7000 S_IFMPB 070000 multiplexed block special (V7)
multiplexor和socket接口類似,運行不同的進程相互連接.它還提供了到多個互聯進程的接口,允許管理進程發送一個信號到group中的其他進程.

V7 Unix的process groups仍然是封閉的,在一個group中的進程無法脫離group.mpxchan的確允許進程離開原本group加入一個新group,但不能確定這是設計者有意爲之的結果.

Unix Berkeley第四版
Fourth Berkeley Software Distribution(4BSD)

從V7 Unix到4BSD經歷了巨大的跨越,進程組也發生了很大改變.在BSD4.3中,擁有同樣UID的進程變成一個組,可以將一個信號發送到這個組的所有進程.向PID -1的進程發送的信號則會傳遞到與當前進程擁有同樣UID的進程中(特權進程向PID -1發出的信號則會被送到所有的進程).更重要的是,在4.4BSD中出現了進程組的層次結構.

Berkeley版本Unix的一大貢獻是”作業控制”.這裏的作業指的是完成某個任務的一個或多個進程.Unix能夠把一些進程放到後臺,但其實現方式相當特別.這些進程會忽略任何信號,shell也不能等待進程完成.大多數情況下時沒有問題的,但是進程不能從後臺恢復到前臺,和後臺程序的輸出會和前臺程序混在一起.這些都是存在的問題.

在BSD”作業控制”下,shell可以控制哪個作業在前臺,哪個作業放到後臺.

在BSD之前,進程組與每次登陸之間是對應的,從與AT&T開發的”System V”Unix兼容的角度來說,這是有意義的.在4.4BSD中,這些與login對應的進程組被定義爲”會話”.進程屬於進程組,進程組屬於會話.每個終端有一個前臺進程組t_pgrp和一個控制會話t_session.

會話和V7 Unix的進程組有點類似,但它們也有區別.一個區別是,不能向一個會話的所有進程組發送信號.另一個區別是進程可以離開當前會話,用set_sid系統調用創建新的會話.

這些區別使logout時殺死所有進程的操作變得複雜.其中有些問題在當時的版本中無法解決.

在現代基於窗口的桌面系統中,會話和進程組仍然存在,但與當時的含義不完全相同.用ps命令可以看到sess和pgrp域

ps -axgo comm,sess,pgrp
進程組和login會話之間再也沒有直接的關係.每個終端窗口擁有自己的會話,包括其他產生終端的應用.每個從shell提示符下啓動的job擁有其進程組,停止這些job的需求小的多.不需要停止當前終端上的活動任務.

要呈現現代桌面系統的進程分組,需要更復雜的層次結構.其中一層表示login會話,一層表示運行在會話上的進程,一層表示某個應用的作業.從4.4BSD發展來的Linux只提供了其中兩層,我們可以在cgroups中尋找第三層.

問題
開始總結.在形成對cgroups的認識前,需要考慮一些問題

groups命名:V6 Unix中唯一的名稱來自tty,並且與進程ID的命令空間相同.這種共享有點笨拙,雖然看起來很方便.
overlapping:信號傳遞和向/dev/tty的I/O請求最初用同樣的機制.但是很快被區別開,它們並不完全相同.
進程能否脫離進程組?歷史上經過了從”不能”到”能”的轉變.二者各有利弊.
層次結構起了什麼作用?
最後一個問題,層級結構至關重要.cgroups最近的一些改變和其反對意見都跟層級結構有關.現在離真正理解層級結構還差很遠,接下來的文章中將要繼續介紹.

Control groups, part 2: On the different sorts of hierarchies
滿世界都是各種層級(hierarchy)關係,作者舉了一堆例子,其中有一個挺有意思的: 如果你在 Wikipedia 上面打開一個文章,然後連續的點接下來的文章鏈接,94.52% 的概率你最終會找到一個詞,i.e. 哲學 (Philosophy)。

Hierarchy 在 Control groups (cgroups) 中的設計,其實是引起了很多的爭論和挑戰,[譯:可能是因爲 use case 太多,總要 balance 各種好處和壞處,而作者很 enjoy 這一點。下面是他給的一些 case,也提出了一些看法]

層級在賬戶權限中的應用
以前在澳洲一家大學的計算機學院做系統管理員的時候,總要針對學生,和教員以及其他後勤行政人員做相關的資源管理,權限分配。不通的部門和課程有很多交集,作者是在計算機支持組。而在這些交集中,角色 (role) 也不同,權限也不通。比如,教員可能比學生擁有更高的打印權限,或者有些打印機僅僅被小部分人使用來打印保密文件,或者有些需要彩色打印稿。
爲了管理這些,我們整了兩個層級,一個是“理由” (包含各種角色,基於這個來分配賬戶),一個是“組織” (角色所在的組織,比如:學校的部門)。然後每個賬戶也都是可以在不同的組中的,教員和學生都可以參與不同的課程,一些高年級學生可以代低年級的學生。在這些權限控制中,賬戶層級也是可以有繼承關係的。
[譯:只是一個例子,算是比較 high level 的,讀者有興趣也可以看下 RBAC (role based access control) 的規範,算是一個理論基礎吧]

可控的複雜度
多層級比單層級的好處,就是比較彈性,一旦做好了,這樣在添加一個賬戶的時候就不用太費心,說白了就是後面的操作會比較簡化。
後面就是說,其實系統稍微複雜的成本足夠 cover 後面操作的複雜度,雖然兩個層級,但是後面管理(增,刪,改,查)比較一個層級的容易很多。

兩種層級類型
這兩個層級在內在和細節上都有很多不同。

“Reason” 層級其實是一種分類,每個個體都有自己的角色,把相似的角色歸小類,然後再給小類劃分大類,比如:生物學中的,門,綱,目,科,屬,種。在樹這種結構中,葉子節點,就相當於種。

“Organisation“ 層級就很不一樣,很多時候分組方式,是基於方便的方式,所以這個應該是動態的,而不是有和 “Reason” 一樣的比較固定的從屬關係。還有一個屬性就是,學校或者科研項目的頭,基本就代表這一個學校或者項目,而不用把他們在關聯到具體的項目的課題中。

/sys 下面的設備
Sysfs 文件系統(通常掛載在 /sys 下面), 這裏只關注一下設備信息,不 care 其他的(模塊,文件系統細節等)。主要有三個設備相關的層級關係在 sysfs 裏面,設備一般是目錄樹,Unix 文件系統一般不支持一個目錄再擁有多個父目錄,所以一般用的是軟鏈接。

設備根在 /sys/dev 。早期主要就是塊設備和字符設備,設備文件一般在 /dev (串口,並口,磁盤等),主次設備號來標識。

設備樹分爲三個層級,例如:/sys/dev/block/8:0,最後一個層級用冒號分隔主次設備號,而不是用斜槓,這是一個軟鏈接。 (譯:指向目錄 /sys/block/sda)

一個用處是,如果你只有一個 /dev 下面的設備名或者一個打開的設備文件描述符,可以用 stat() 或者 fstat() 系統調用拿到設備類型,主從設備號等信息,然後就可以轉換到對應 /sys/dev 下面,再拿到其他的需要的信息。

[譯:下面這段好像有點問題,subsystem 指向的不一定是 class/bus, 還要具體看]

/sys/class 目錄裏面就是一堆設備分類,(用的是設備名,不是設備號);/sys/bus 裏面的信息比較多,有其他的例如模塊信息什麼的。比如 塊設備通過 usb 掛在 pci 下,這種物理層級就可以被描述出來。

簡單分層很難描述出所有設備的內在連接,有的設備很可能從一個地方拿到控制信號,而從另外一個地方獲得 power。這個在今年內核峯會之前,已經被廣泛討論過了。

從分類的角度,/sys/dev 是比較簡單的,而 /sys/class 其實也是比較簡單的,雖然它包含更完整的信息。

/sys/bus 也是一個只有兩層分類。class 層級主要是功能層面的(例如,設備提供net, sound watch dog 功能),而 bus 層級主要是訪問的角度 (如何被訪問的,比如:物理順序)。

這麼理解的話,基本上就還是兩個分類。

/sys/devices 分類包含所有類型的設備,有的簡單的掛在物理總線下,否則(沒有對應的物理總線)就在 /sys/devices/virtual

Linux 源代碼樹
Linux 源代碼樹的層級出發點很不一樣,我們來看看 (其他代碼樹類似)。這種層級更加關注於組織而不是分類 (相比:sysfs) 。[譯:作者一直強調 classification 和 organization 的區別,大概就是一個傾向於靜態(分類之後的成員比較固定),一個傾向於動態(分類之後的成員尚可調整)]

頂層目錄,fs 爲文件系統,mm 爲內存管理等 [譯:kernel 目錄我理解就是個雜項]。有些子系統比較小,比如 time ,就放在 kernel 裏面,如果變得足夠大了之後,就可能有自己的目錄,但是不會跑出 kernel 目錄。

fs 子樹多半包含的都是不同種類的文件系統,也有一些輔助的模塊,如:exportfs 用來輔助文件服務器,dlm 鎖定集羣文件系統,ad hoc 實現高層的系統調用,沒有專門的雜項目錄在 fs 目錄裏,都是直接放在 fs 下面。

具體怎麼分類也不一定有準確的答案,不過在對 cgroups 分類爭論的時候,不妨參考一下。

源碼樹也包含其他的分類,比如:scripts, firmware, 頭文件在 include。最近幾年也有在討論把頭文件放到相應的 c 文件附近。考慮一下 nfs 和 ext3 文件系統。 每個文件系統有自己的 c 文件和頭文件,問題是這些頭文件應該都放在 include 下面嗎? 要不要把 .c 和 .h 對應起來放在一起? 這種情況在 3.4 版本內核上面已經變了, ext3 的 4 個頭文件已經從 include/linux/ 搬到了 fs/ext3/ext3.h

層級分類的話只有當需要並且必要的時候才做。

進程層級
理解 cgroup 纔是這一系列文章的真正目的,依賴於層級如何來管理這些不同進程的角色。上面中的例子沒有一個是關於進程的,不過他們引出了很多問題在深入討論 cgroups 之前:

單層級的簡化比多層級的靈活重要?是否他們是獨立的還是相互作用的?
主要目標是給進程分類還是簡單的組織一下他們?或者兩個目的都有,如何把這兩者有機結合起來?
我們可否允許一些非層級機制,例如:符號連接,或者文件名後綴[譯:某種程度上,打破了分級]來提供分類或者組織一些元素?
可否把進程附加到在分層的內部節點上?或者應該強制他們在葉子節點上?即使葉子代表雜項?
上次一個進程組的例子,是一個單一的分級,先會話進程,然後是工作組,這樣的進程組裏面的進程都是葉子,比如從來不會打開 tty 的系統進程就不在分層裏面。

找到這些問題的答案需要理解 cgroups 怎麼來組織這些進程的,並且這些分組是用來幹嘛的,後面會分析 subsystems 包含資源控制和其他的操作來回答這些問題,看下回分解把。。。。

Control groups, part 3: First steps to control
July 16, 2014 This article was contributed by Neil Brown
在cgroup之前就已經有nice用於控制每個進程可以使用的CPU time,以及通過setrlimit()系統調用限制內存及其他資源的使用。然而這些控制只能針對每個獨立的進程,無法對進程組進行控制。

Cgroup子系統
Linux的cgroup能夠允許設立獨立的子系統用於對進程組控制,更適合的術語是“資源控制器”。

Linux3.15現已經有12個cgroup子系統。我們主要關注下這些子系統–特別是子系統之間的層級組織,如何工作。

在進入細節前,先快速描述下一個子系統能做的事情。每個子系統能做的包括: 1. 在每個cgroup中存儲一些任意的狀態數據 2. 在cgroup文件系統裏提供一些屬性文件用於查看或者修改狀態數據,或者其他狀態細節 3. 接受或者拒絕進程加入一個給定的cgroup的請求 4. 接受或拒絕在一個已經存在的組裏面創建一個子組的請求 5. 一些cgroup有任何進程創建或者銷燬時獲得通知

這些只是用於和進程組進行交互的通用接口,實際上子系統還會有其他一些方式和進程組以及內核交互,以實現期望的結果。

A simple debugging aid
debug子系統並不實際“控制”任何東西,也不移除bug(很不幸)。它既不給任何的cgroup添加額外的數據,也不拒絕”attach”或者”create”請求,甚至也不關心進程創建和銷燬。

開啓這個子系統唯一的影響是使得許多獨立的組或者整個cgroup系統的內部細節能夠通過cgroup文件系統的虛擬文件查看到。細節包括部分數據結構當前的引用計數,以及內部標識項的設置情況。這些細節只有cgroup工作者可能全部關注。

Identity - the first step to control
Robert Heinlein首先向我表達了這樣一個想法:讓每個人帶上ID是走向控制他們的第一步。雖然這樣做對於控制人類來說會很不受歡迎,但提供明確的身份認證對於控制進程組是很實際和有用的。這是net_cl和net_prio cgroup子系統最主要關注的。

這兩個子系統都含一個小的標識數字,組內的進程創建socket的時候會將該數字拷貝給創建的socket。net_prio使用每個cgroup的id(cgroupo->id)作爲sequence number,並將這個存儲在sk_cgrp_prioidx中。這個對每個cgroup都是獨特的。net_cl允許爲每個cgroup設置一個特點的數字,然後存儲在sk_classid裏面。這個數字並不需要每個cgroup都不同。這兩個不同cgroup的標識被三個不同進程使用。

net_cl設置的sk_classid可以被iptables使用,根據包對應socket屬於那個cgroup對包進行選擇性過濾。sk_classid也可以在network調度用於對包進行分類。包分類器能夠基於cgroup以及其他一些細節作出一些決定,這些決定會影響很多調度細節,包括設置每個信息(message)的優先級。 sk_cgrp_prioidx這個是單純的用於設置網絡包的優先級,使用這個之後將會覆蓋之前通過SO_PRIORITY socket選項或者其他方式設置的值。設置這個之後的效果和sk_classid及包分類器共同完成的類似。然而根據引入net_prio子系統的commit所說,包分類器並不總是能夠勝任,特別是對開啓了data center bridging(DCB)的系統。

同時擁有兩個不同的子系統對socket以三種不同方式進行控制,而且還有相互重合的地方,這看起來很奇葩。各個子系統是否需要變得更加輕量級,以使得添加它們比較容易,或者各個子系統需要更加的強大,這樣一個子系統就可以用於各種場景–這個在目前還並不是很清晰。後面還會碰到更多的子系統之間有交集的情況,也許能夠幫助更加清晰的認識這個問題。
當前,最重要的問題還是這些子系統如何實現cgroup原有的層與層之間的交互。答案是,大部分都沒有。

無論是net_cl設置的class ID,或者net_prio設置的優先級(per-device)只是應用於對應的cgroup以及所有和這個cgroup裏面進程相關的socket。這些設置並不會對子cgroup裏面進程的socket產生影響。因此對於這些子系統,嵌套關係是無意義的。
這種對層級的忽視使得cgroup樹看起來更像是一個組織層級關係 – 子組並不是子類,而不是分類層次結構。

其他子系統對層級結構更加註重,值得看的三個子系統是device, freezer和perf_event。

Ways of working with hierarchy
從整體上考慮cgroup的話,一個挑戰是不同使用場景會有存在非常大的差異,給當前架構帶來很多不同的需求。下面三個子系統都使用了cgroup的分層,但是控制流如何影響到進程這些細節上卻有很大不同。

devices
device子系統將訪問控制託管給設備相關文件。每個組可以運行或者禁止所有的訪問,隨後給出一組訪問被禁止會在運行的異常信息列表。

更新異常列表的代碼會保證子組的權限不會比他父親的多–設置或者傳播父親組不允許的權限給子組時會被拒絕。因此需要進行權限檢查時,只需要檢查自己組內的,並不需要遍歷檢查祖先是否允許這個訪問,也不需要檢查祖先組的規則是否已經在每個組裏面。

當然這樣也是有一定代價的,權限檢查簡單帶來的是更新進程的權限會變得更加複雜。但由於權限檢查相比權限更新更加頻繁,這個代價是值得的。

對device子系統,Cgroup裏面的配置會影響所有子cgroup,一直到層級的最下面。每個進程需要檢查訪問權限時,仍然回到相應的cgroup中。

freezer
freezer子系統的需求和device的完全不一樣。這個子系統在每個cgroup裏面提供了一個freezer.state文件,用於寫入FROZEN或者THAWED。這類似發送SIGSTOP和SIGCONT個ie一個進程組,這樣整個進程組將會stop或者restart。

freezer子系統在對進程組進行freeze或者thaw的時候,也跟device子系統的類似,會遍歷所有子cgroup至最低層級,設置這些group爲frozen或者thawed。然後還需一步,進程並不會定期檢查所在的cgroup是否frozen,因此需要遍歷所有cgroup裏面的進程,顯示的將它們移動給freeze處理者或者移出。爲了保證沒有進程逃離,freeze要求進程fork的時候會得到通知,這樣就能夠得到新建立的進程。

因此freeze子系統對cgroup層級管理提出另外一個要求,需要能夠讓配置一直下發到裏面的每個進程。 perf_event還有另外一個需求。

perf_event
perf收集某組進程的各種性能數據,這個可以是系統裏面的所有進程,或者某個特定用戶的所有進程,或者某個特定父進程派生的所有進程,亦或perf_event cgroup子系統的所有進程。
爲了檢查是否在一個group裏面,perf_event使用cgroup_is_descendant()函數簡單的遍歷->parent直到找到一個匹配的或者是root。這個操作並不是特別開銷大,特別是在層級不深的情況,當然相比簡單比較兩個數字的開銷肯定更大些。網絡代碼的開發者在對添加任何有性能開銷代碼的敏感性方面是出名的,特別是這些開銷是給到每個包的。因此網絡代碼不使用cgroup_is_descendant()也不會有啥讓人驚訝。

對於perf,配置並不會下發到各個層級。任何時候當需要一個控制抉擇(比如這個事件是否需要統計),會從進程開始遍歷這個樹以找到答案。

讓我們會到net_cl和net_prio,試問它們怎麼放進這個圖譜裏面–將配置從cgroup一直到進程接受到控制,和device子系統一樣。進程在創建socket的時候是能夠找到對應的cgroup,但是並不按層級往上回溯。區別是下發配置留給用戶態,而不是讓內核提供。

cpuset: where it all began
最後的一個cgroup子系統是cpuset,也是Linux最早加入的一個子系統。

和net_cl不同的是對於cpuset子系統,當一個進程從一個cgroup移動到另外一個cgroup時,如果兩個cgroup允許運行的處理器集不一樣,進程可以簡單的被放置到一個新的被允許的處理器對應runqueue上,而當允許的內存node修改了的話,將內存從一個node遷移到另外一個node就不是那麼簡單。

和device子系統不同的是,cpuset cgroup裏面每個進程都保存有自己的CPU集,另外cpuset子系統需要跟freeze子系統一樣將新的配置下發到每個獨立的進程。還有一個不同的是,cpuset並不需要在fork的時候被通知。

此外,cpuset有時也需要往上遍歷層級以找到一個合適的父親。其中的一個例子是當一個進程發現所在的cgroup沒有可以運行的CPU(可能是由於一個CPU已經offline)。另外一個是一個高優先級的內存申請發現mems_allowed裏面所有node的內存都已經耗盡。這兩種例子裏面,從祖先節點裏面借一些資源可以用於度過當前的緊要關頭。

可以看到有的子系統需要配置下發,有的卻需要沿着cgroup樹往上搜索,對於cpuset來說這兩種都需要。

The story so far
對當前這7個子系統,可以看到部分子系統會提供控制(device, freezer, cpuset),部分子系統僅僅給一個標識用於啓動特定的控制(net_cl, net_prio),而部分子系統並不引入任何控制(debug, perf_event)。一些子系統(device)提供的控制是要求內核代碼檢查cgroup子系統裏面的每個訪問,同時另外一些子系統(cpuset, net_cl)提供對內核數據結構(threads, sockets)進行設置,其他一些內核子系統從那裏獲取設置。 一部分控制是沿着層級樹往下分發給子cgroup,一些是沿層級樹往上檢查父親節點,也有一些是兩種都有或者兩種都沒有。

沒有太多細節直接和我們在層級系統裏面發現的許多問題相關,儘管如此the emphasis on using cgroups to identify processes perhaps suggests that classification rather than an organization is expected.

對這些子系統,稍微更傾向於使用分類層級,但是對於多層級並沒有特殊的需求。

當然,我們還沒有結束,後面我們還會對其他幾個子系統:cpu cpuacct blkio memory hugetlb進行分析,看是否能夠從這些子系統中學習到什麼樣的層級會更適合他們。

Control groups, part 4: On accounting
Linux和Unix對資源使用計數並不陌生,即使在V6 Unix,每個進程使用的CPU時間都被計數且運行總時間可以通過times()系統調用獲得。這也擴展到了進程組,V6 Unix裏一個進程派生出來的所有進程可以組成一個組,當組內的所有進程都退出時,使用的總CPU時間可以通過times()獲得。在進程退出或者等待前,它的CPU時間只有自己知道。在2.10BSD裏,被計數的資源種類擴展到包括內存使用、缺頁中斷數、磁盤IO等,和CPU時間統計類似,當子進程等待時這些計數會被加進父進程裏。getrusage()調用可以訪問這些計數,現在的linux裏還存在。

getrusage()後有了setrlimit(),它可限制資源的使用數目,如CPU時間和內存。這些限制只能加在單獨的進程上而非組:一個組的計數只能在進程退出時累加,但顯然這時太晚了而沒法達到限制的目的。

cpuacct–爲統計而統計
cpuacct是最簡單的統計子系統,部分原因是因爲它只做統計,而不施加任何限制。cpuacct的出現最初是爲了證明cgroup的能力,並沒有想合進mainline,但它和其他cgroup代碼一起被合進了2.6.24-rc1,但由於最初設計初衷馬上又被移出去了,最後又因爲看起來很有用又被重新加進了2.6.24-final。知道了這段歷史,我們可能就不會期望cpuacct能滿足那些大而全的需求。

cpuacct有兩種不同的統計信息,第一個是組內所有進程的總CPU時間,它被調度器統計且精度是很高的納秒級。這個信息以per-CPU來統計,且可以per-CPU和總時間兩種形式呈現。第二個是組內所有進程的總CPU時間被拆分成“user”和“system”(從2.6.30開始),它們的統計方式和times()系統調用相同,都是以時鐘滴答或“jiffies”爲粒度。因此它們和CPU時間的納秒級別相比沒有那麼精確。

從2.6.29開始按層級進行統計。當一些計數被加到一個組裏時,它也會被加至這個組的所有祖先組裏。因此,一個組內的使用統計是當前組和所有子組裏進程使用之和。這是所有子系統的一個關鍵特點:按層級統計。雖然perf_event也做一些統計,但是這些統計只加進當前組,而不會向祖先組裏累加。

對於cpuacct和perf_event兩個子系統而言,按層級統計是否必要尚不清楚。內核並不使用總的統計,只對應用程序可用,但是它也不太可能以很高的速率頻繁讀取數據。這就以爲着對於需要整個組計數信息的程序而言,一個有效的辦法就是遍歷所有子組並累加得到總和。當一個組被刪除後,可以將它的使用計數累加近父組,就像進程退出後將cpu時間加進父進程一樣。更早地累加也沒有什麼好處。

即使在cgroup文件裏應該直接顯示總和,內核是在需要而不是每次變化的時候計算總和更加切實可行。是應用程序在需要的時候遍歷各個組得到總和,還是內核在每個計數時都遍歷每個祖先加進總和,這之間存在明顯的權衡。對這種權衡的分析需要將樹的深度和更新的頻率考慮在內,對於cpuacct,每個調度器事件或時鐘滴答都會產生一次更新,即在一個繁忙的機器上每毫秒都會產生一次或更多。雖然這種事件已經很頻繁了,但還有其他更頻繁的事件。

無論cpuacct和perf_event的計數方法是否合理,這對理解cgroup都不是那麼重要,值得關注的是如何權衡不同的選擇。這些子系統可以自主選擇方法,因爲內核內部並不使用統計數字。但對於其餘需要控制資源使用的子系統而言,它們需要準確無誤的統計。

內存相關
有兩個cgroup系統是用來對內存使用進行計數和限制的:memory和hugetlb,這兩個子系統使用通用的數據結構記錄及限制內存:”resource counter” 即 res_counter。res_counter的定義在include/linux/res_counter.h,實現在kernel/res_counter.c,它包含一些內存資源的使用計數和兩個限制:limit和soft limit,還包含一個內存使用的歷史最高值、申請失敗的請求次數。同時,res_counter包含一個用來防止併發訪問的spinlock和指向父組指針,這些父指針一起組成了一個樹狀的結構。

memory cgroup有三個res_counter,一個用來記錄用戶程序的內存使用,一個用來記錄總內存和swap使用,另一個用來記錄因爲該進程使得內核方面的內存使用。hugetlb也還有一個res_counter,這意味着當memory和hugetlb都開啓時共有四個父指針,cgroup的這種層級式設計也許並不能滿足用戶的需求。當進程申請一種內存資源時,res_counter需要向上遍歷每個父指針,檢查每個祖先的內存限制並更新當前使用量。這需要拿每層的spinlock,因此代價比較大,特別是層級比較深的情況下。Linux在內存分配做了很好的優化,除了per-cpu的空閒鏈表,還有分配釋放的批量操作來減小單次分配的代價。內存分配有時候會很頻繁,性能需要足夠好,因此在每次內存申請時都要拿一系列的spinlock來更新計數顯然不是一個好主意。慶幸的是,memory子系統不是這麼做的。

當內存申請少於32個頁時(大多數請求都只有1個頁),memory cgroup會從res_counter一次請求32個頁。如果請求成功,多申請的部分會被記錄在一個per-cpu的“存量”裏,它會記錄每個cpu上最後申請的是哪個cgroup及剩餘多少。如果請求不成功,它會只申請需要的頁個數。當同一個進程在同一個cpu上有後續的內存分配時就會使用存量,直到用完。如果另外一個cgroup的新進程被調度到當前cpu上分配內存,原來的存量會被退回同時會爲該cgroup創建一個新的存量。內存釋放同樣也是批量進行,但是是不同的機制,這是因爲釋放的量經常會更大且不會失敗。批量釋放使用per-process計數器(而不是per-cpu),且需要在代碼裏顯式地被開啓,調用順序是:

mem_cgroup_uncharge_start()
repeat mem_cgroup_uncharge_page()
mem_cgroup_uncharge_end()
這可以使用批量釋放,單獨一個mem_cgroup_uncharge_page()則不行。
以上可以看出對資源使用的計數代價可能會很大,而在不同的環境下有不同的方法來減小代價,因此不同的cgroup對這個問題應保持中立態度,並根據自己的實際需求找到最合適的辦法。

另一個CPU子系統
有幾個cgroup子系統和CPU相關,除了之前提到的用來限制進程可運行cpu的cpuset,記錄cpu允許時間的cpuacct,第三個相 關的子系統就叫做cpu,它是調度器用來控制不同進程和不同cgroup間的運行時間比例。

Linux調度器的設計思想很簡單,它的模型是基於一個設想——CPU是理想的多任務調度,可以同時跑任意多個線程,隨着線程數目的增多運行速度遞減。在這個模型下,調度器可以計算出每個線程應該得到多少CPU時間,同時選擇實際運行時間最少的線程服務。如果所有的進程平等,且有N個可允許進程,那麼每個進程會有1/N的實際運行時間。當然如果調度優先級或者nice值分配的權重不同,進程會有不同比例的運行時間,它們的時間比例總和是1。如果用cpu cgroup進行組調度時,運行時間比例就是基於組層級進行計算,因此一個上層組會被分配一個時間比例,並在該組進程和子組中共享。

另外,一個組的運行時間應該等於該組所有進程運行時間的總和,但是如果有進程退出,它多使用或少使用的時間信息就會丟失,爲了防止這個因素導致的組間不公平,調度器會和每個進程類似也記錄每個組的使用時間。“虛擬運行時間”就是記錄理想和實際允許時間的偏差。爲了管理不同層級上的值,cpu子系統建立了一套並行於層級的sched_entity結構,調度器用它來記錄不同的權重和虛擬運行時間。每個CPU都有一套此層級結構,這意味着運行時間可以無鎖地向上推送,因此比memory cgroup使用的res_counter更加高效。

CPU子系統還允許對每個組限制最大的CPU帶寬,帶寬的計算方法是CPU時間除以牆上實際時間。CPU時間(quota)和牆上實際時間(period)都需要設置,當設置quota和period時,子系統會檢查父組的限制是否允許子組能充分使用這些quota,不行的話就會拒絕設置。帶寬限制大多是在sched_entity下實現的,當調度器更新每個sched_entity使用了多少虛擬時間時,也會一併更新帶寬使用並檢查是否需要進行限制。

從我們提到的例子中可以看出,限制通常是從上到下檢查層級,而資源計數是從下到上遍歷層級。

blkio
Linux 3.15裏blkio有兩種策略:“throttle”和“cfq-iosched”,和cpu子系統的兩種策略很類似(帶寬和調度優先級),但是實現細節差別很大。許多想法在其他子系統中都已經提到了,但是另外兩個點值得一提:

一個是blkio子系統爲每個組增加了一個新的ID。之前提到cgroup框架爲每個組分配了一個ID且net_prio用它來區分組,blkio增加的新ID也是類似的作用但是有一點區別。blkio ID是64位且從不重用,但cgroup框架的ID是int類型(32位)且可以被重用。唯一的ID是一個通用的特性,更應被cgroup框架提供。增加了blkio ID一年之後,cgroup框架也提供了一個非常類似的serial_nr,但是目前blkio還沒有修改去重用這個域。注意當前的代碼下,blkio也被稱爲blkcg。

另外一個特性是關於blkio的cfq-iosched策略。每個組都被分配一個不同的權重,類似於CPU調度器的權重,它用來平衡本組和兄弟組進程請求的調度。但是blkio還有一個leaf_weight,用來平衡組內進程和子組進程的請求。當非葉子cgroup包含進程時,cfq-iosched策略會將這些進程當作在一個虛擬組裏並用leaf_weight作爲它的權重。CPU調度器沒有這個概念,兩種調度行爲也沒有正確或錯誤之分,但如果他們行爲一致是最好不過了,其中一個辦法便是非葉子cgroup不能包含單獨的進程。

Control groups, part 5: The cgroup hierarchy
July 30, 2014 This article was contributed by Neil Brown

Control groups
在之前的文章裏面,我們已經看過一般情況下的層級,以及特定cgroup子系統如何處理層級。現在是時候將這些彙總起來,以理解那種層級是需要的,已經如何在當前的實現中進行支持。如我們最近報道的,3.16 Linux內核正在開發對“統一層級”的支持。那個開發引入的新想法在這將不進行討論,因爲我們暫時沒法知道它們可能帶來的意義,除非我們已經完全知道我們擁有的。後面還會有文章剖析統一層級,先前我們先開始理解我們稱之爲”classic”的cgroup層級。

Classic cgroup hierarchies
在classic的模式,會有許多單獨的cgroup層級。每個層級都會有一個root cgroup,所有進程都包含在這個root cgroup裏面。root節點是通過mount一個cgroup虛擬文件系統實例創建的,所有的修改都是通過操作這個文件系統進行的,比如通過mkdir創建cgroup,rmdir刪除cgroup,以及mv對cgroup進行重命名。一旦cgroup創建,進程可以通過將pid寫入特定的文件在cgroup之間移動。如一個特權用戶將PID寫入一個cgroup的cgroup.procs文件裏面,那麼這個進程就被從當前的cgroup裏面移入到目標cgroup裏。

這是一種有組織的層級管理:創建一個新的組,然後找到相應進程的放進這個組裏面。這種方式對於基於文件系統層級組織來說是很自然的,但不能說這就是最好的管理層級的方式。在4.4 BSD裏面的基於會話和進程組的簡單層級組織工作方式就是相當不一樣。

classic層級方式最大的問題是專制的選擇。子系統之間有着大量不同的組合方式:一些在一個層級,一些在另外一個,也有的一個也沒有。問題是這些選擇一旦作出,影響是系統級的,很難改變。假如有的人需要某個特定的子系統組合方式,而同時另外一個人需要另外一種,這個時候兩個需求可能並無法同時在一個宿主機上得到滿足。這對基於container實現的在一個宿主機上同時支持多個各自獨立的管理域來說是個嚴重的問題。所有的管理域只能看到相同的cgroup子系統層級組織。

顯而易見的選擇是隻有一個層級(“統一層級”方式的目標),或者每個子系統有一個獨立的層級(比如只有cpu和cpuacct兩個是組合在一起的)。根據我們所學習到的關於cgroup子系統的知識,我們可以試着理解一些保持子系統相互獨立或者相互組合的具體實現。

有一些子系統並不做統計,或者雖然做統計,但並不利用統計進行任何控制。這些子系統包括:debug,net_cl,net_perf,device,freezer,perf_event,cpuset,以及cpuacct。它們都沒有重度使用層級,在幾乎所有的用例中層級提供的功能可以獨自實現。
這些子系統裏面有兩個使用層級的地方不是很好移除。第一個是cpuset子系統。這個子系統在緊急情況下會沿層級向上查看,以找到額外的資源進行使用。當然正如之前有提到的,類似的功能可以不依賴於層級關係進行提供,因此這只是個小問題。

另外一個是device子系統。它對層級的使用不是在控制方面,而是在配置授權上:子組不允許訪問父組禁止的配置。區域層次結構(administrative hierarchy)在權限分配方面是很高效的,無論是對用戶分組,或者針對獨立的使用者,亦或對有自己用戶集的container。對於非統計類(non-accounting)子系統,提供一個唯一的區域層次結構是很自然的選擇,也很適合。

Network Trafic Control – another control hierarchy
網絡流量實際是由一個獨立的層級進行管理,這個層級甚至獨立於cgroup。爲了便於理解,需要簡單介紹下網絡流量控制(Network Traffic Contro,NTC)。NTC機制是通過tc進行實現。這個工具允許爲每個網絡接口添加一個排隊模型(或稱爲qdisc),有些qdisc是有類的(classful),它們允許有其他針對不同類別包的qdisc掛在下面。如果第二層的qdisc也是有類,那麼意味着qdisc也是能夠按層級組織的,甚至可以有很多層級,每個網絡接口一個。

Tc也允許配置過濾器,這些過濾器用於指導網絡包如何分配給不同的類(也意味着不同的隊列)。過濾器可以使用很多值,包括每個包的大小、包使用的協議、產生這個包的socket。net_cl cgroup子系統能夠每個cgroup裏面進程創建的socket設定一個類ID(class ID),通過這個ID將包分類到不同的網絡隊列中。

每個包經過諸多過濾器被分類到某個隊列,然後向上傳遞到根,也許會被限流(比如Token Bucket Filter,tfb, qdisc),或者被競爭調度(比如Stochastic Fair Queueing, sfq, qdisc)。一旦到達了根,包就被髮送出去。

這個例子說明了層級對於資源調度和使用限制的意義。它也向我們展示獨立的cgroup層級並不需要,本地的資源層級就能很好的滿足需求。

對於CPU、memory、block I/O和network I/O,每個資源主控制器都維護有自己的獨立層級。前三個都是通過cgroup管理的,但網絡是單獨管理的。這樣看有兩種不同類型的層級:一些用於組織資源,一些用於組織進程。

Cgroup層級其中有一個在NTC層級中不是很明顯能做的是,在使用container時將層級分到一個獨立的區域域。部分container的名字空間中僅僅掛載一個cgroup層級的子樹,這個container被限制只能影響這個子樹,這樣的話container裏面就沒法實現類ID被設定給不同cgroup。

對於網絡,這個問題通過虛擬化或者間接的方式能夠解決。虛擬網絡接口”veth”能夠提供給container,這樣就能夠按照自己喜歡的方式進行配置。Container的流量都會被路由到真實的接口,並能夠根據流量源自哪個container進行分類。同樣的機制也對block I/O有效,但是CPU和內存資源的管理沒辦法,除非有類似KVM這樣的全虛擬化。

How separate is too separate
正如我們上次提到的,資源統計控制器需要對祖先cgroup的信息可見才能夠高效的實現限速,也需要對相鄰cgroup的可見以實現公平共享,因此完整的層級對於這些子系統來說是很重要的。

以NTC作爲例子,可能會引發爭論的點是這些層級需要爲每種資源分離開。NTC做得比cgroup更深遠,能夠允許每個接口擁有一個獨立的層級。blkio可能也會想要對不同的塊設備提供不同的調度結構(swap vs database vs logging),但這個目前cgroup還不支持。

儘管如此,過多的資源控制隔離會帶來一定開銷。統一層級支持方的Tejun Heo指出這部分開銷是由於缺少“有效的合作”。

當一個進程往文件中寫數據時,數據先到page cache,這樣會消耗內存。在之後的某個時間,內存將會被寫出到存儲設備,這樣會消耗一些塊設備I/O帶寬,或者也有可能一些網絡帶寬。因此這些子系統並不是完全分開的。

當內存被寫出時,很可能這動作並不是由寫這部分數據的進程執行,也可能不是由這個cgroup裏面的其他進程執行。那麼如何能夠使得塊設備I/O的統計更加精確呢?

memory cgroup子系統爲每個內存頁附加了一些額外的信息,這樣能夠在頁被釋放時知道應該找誰退款。似乎當頁被寫入的時候,我們可以在這個cgroup裏面統計I/O使用。但是有一個問題,這個cgroup是和memory子系統一起的,因此有可能在完全不同的層級裏。這樣的話,這個cgroup裏面對內存的統計可能對blkio子系統來說完全沒意義。

還有其他一些方式來解決這種分離:
在每個頁裏面記錄進程的ID,由於兩個子系統都知道PID,因此可用這個計算內存和塊設備的使用。這個方法有一個問題是進程可能存活時間很短,當進程退出時,我們要麼需要將進程未歸還的資源轉移給其他進程或者cgroup,要麼直接丟棄。這個問題和CPU調度裏面的類似,只對進程進行統計很難實現合理進程組的公平性。合理的保存未歸還的資源是一個挑戰。 引入一些其他的標識,要求能夠存活任意時間,能夠和多個進程關聯起來,能夠被每個不同的cgroup子系統。這種間接法衆所周知能夠解決任何計算機科學的問題。 用於連接net_cl子系統和NTC的class ID就是這樣一個標識。當有很多層級,每個接口一個時,只有一個class ID標識的名字空間。

爲每個page存儲多個標識,一個用於內存使用,一個用於I/O吞吐。當前用於存儲額外的memory控制器信息的page_cgroup結構體在64位系統上每頁消耗128字節–64字節是一個執行歸屬cgroup的指針,另外64字節用於做標識位(目前已經使用3位)。假如能夠用一個數組的索引替換指針,十億個組目前看是足夠的,這樣兩個索引和一個額外的bit能夠存儲在之前一半的空間中。是否一個索引就能夠提供足夠的效率,這個留給感興趣的讀者練習。

對這個問題的解決方式也許能夠使用與其他情況:任何有一個進程代替其他進程消耗資源的地方。Linux裏面的md RAID驅動通常會在初始化該請求的進程上下文中將I/O請求直接下傳給下層設備。但其他一些情況下,一些工作需要由一個協助進程完成,用於在將來提交請求。目前,完成這部分工作的CPU時間和該請求消耗的I/O帶寬都被算到md而不是最初的進程上。假如能夠爲每個I/O請求加上消耗者標識,md和其他類似的驅動將有可能據此分配資源使用。

不幸的是,目前的實現下這個問題沒有好的解決方式。過度的隔離會帶來性能損耗,這些損耗並不能通過簡單將所有子系統放到一個相同的層級得到減緩。

目前的實現,最好是將cpu blkio memory和hugetlb這些統計子系統放入單獨的層級,而網絡方面謝謝NTC使得已經有一個獨立的層級,同時也最好將所有非統計類的子系統一起放在一個區域層級。這樣需要的時候依賴於智能的工具對這些獨立的層級進行有效的組合。

Answers …
現在需要回答一些以前文章提到的問題。其中一個是如何命名組。正如我們上面看到的,這個是執行mkdir命令進程的職責。這個和任務控制進程組及會話組不一樣,這些組是在進程調用setsid()或者setpgid(0,0)時內核會爲組設定一個名字。這之間的區別能夠得到解決,不過這裏需要闡述下期望的權力結構。對於任務控制進程組,形成一個新組的決定來自於新組的一個成員。而對cgroup,這個決定更期望的是來自於外部。先前我們已經觀察到在cgroup層級裏面包含一個區域層級看起來很有道理。而與這個觀察一致的是名字是從外部給予的這個事實。

另外一個問題是,是否允許從一個組移入到另外一個組。移動一個進程需要將進程ID寫入cgroup文件系統的一個文件中,這個有可能由任意有權限對這個文件進行寫的進程執行,這樣需要更進一步檢查執行寫操作進程的所有者是否也是將被添加進程的所屬者,或者是更高權限的。這意味着任何用戶能夠將他們的任意進程放入任意他們有對cgroup.procs寫權限的組裏面,而不顧跨越了多少個層級。

換句話說,我們可以限制一個進程可以移動到哪,但是對於進程從哪裏移動過來的控制卻很少。

… and questions
這個討論引出的最大問題是,是否真的有對不同的資源使用不同層級管理的需求。NTC提供的靈活性是否很好的超越了需求,或者它是否爲其他提供一個可以追隨的有價值的模型?第二個問題關心的是假如不同的需求都使用一個層級,是否會引起組合爆炸,同時這個帶來的開銷是否和其價值成比例。在任意情況,我們需要清楚的知道如何計算請求實際發起者的資源消耗。

這些問題中,中間可能是最早的:擁有多cgroup在實現上有哪些開銷?下面一個topic我們需要講的就是這個。

Control groups, part 6: A look under the hood
這篇文章扼要地介紹了cgroup subsystem設計的背後原因,老哥還蠻謙虛的,說僅僅是霧裏看花,錯了莫怪。

首先,在配置上cgroup subsystem面臨着組合爆炸的可能,可以算個流水帳:如果某個系統上有Q個管理員,而每個管理員則希望用N種方式分割M種資源,於是就需要Q x M x N個cgroup。但如果我們能夠把這種層次從水平方向上切成多個層次就可以緩解這個現象了,比如把M個資源的N種切換方式與Q個管理員的層次分別獨立出來,就只需要Q + M x N個cgroup了。

無論怎麼說,cgroup的組合都顯然是一種樹狀結構,相信讀者瞬間就可以在腦子裏畫出那圖像來,單就樹狀結構來看,確實沒有什麼新鮮的。這裏的複雜性其實在於cgroup以線程爲控制單元並非進程,以及cgroup是如何與線程關聯起來的。

然後,作者從500年前開始講起,好吧,其實是1975年的UNIX v6講起,一步一步地介紹了UNIX是如何逐步爲進程增加維度的:session, process group, process, thread。對於Linux,process實際上是所謂的thread_group,而PID其實是其中leader線程的TID。無論是進程還是線程,在Linux下都是用task_struct(任務)表示的。Linux的PID namespace又把事情變得更復雜了,一個任務在不同的PID namespace下的ID很可能並不一樣。所以,每個任務的PID並不是一個簡單的數字,而是三個鏈表:


enum pid_type { PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX };
struct hlist_head tasks[PIDTYPE_MAX];
struct pid_link
{
struct hlist_node node; struct pid *pid;
} pids[PIDTYPE_MAX];

後面作者介紹了不少社區爲了做到高併發所做的N多努力。
OK,該是cgroup內部結構登場的時候了:

每個cgroup結構都有cset_links字段,把其中的所有threads串起來。
每個css_set結構則都有一個cgrp_links字段,把其中thread涉及到的cgroup串起來。另外,對每一個層次結構還有一個cgrp_cset_link字段。
這裏面使用了大量內核中的鏈表技巧,親們google之?

文章裏講了以上這些數據結構中鎖機制的一些設計細節,也值得一看的。

Control groups, part 7: To unity and beyond
這一系列文章的目的是爲了讓我們理解linux Cgroup,能夠參與到我們周圍關於cgroup的討論中去。 現在是檢驗我們成果的時候了,我可以參與到cgroup的討論裏,並發表自己的建議和質疑。 (譯:下面就是作者自己的一些評價和質疑)。

The unified hierarchy: A score card

Unification of hierarchy 毫無疑問傳統的cgroup允許太多的繼承個數。將個數減少到一個是理想化的情況。在調查中,我們發現兩種截然不同的繼承用法:一些子系統利用control向下,而另外一些則相反。根據涉及 到不同的實現所關注的繼承又很大的不同。 令人欣喜的是統一繼承正朝着去除多餘的重複的方向努力。看起來它不像要承認不同的子系統可能有真的不相容的需求,但是它完全關閉分割繼承的大門。 得分B 有待進一步提高。

Processes only permitted in the leaves 統一繼承要求只能在進程退出時才能退出。這個強制看起來很不合理。葉子是”不能把子系統擴展到孩子節點的節點“,在繼承裏創建新的level需要分兩部走。 進程首先被下移,讓後子系統被往下擴展。這個問題給個 C。這裏要強調一個設計上的缺陷。進程被排除在內部cgroups外,除了root cgroup,顯然root 需要特殊對待。基於這一點可以給個C+

Taming the chaotic subsystems 我們已經看到了cgroup子系統和各個功能單元之間是相當混亂的。這不是新觀點,2011年的內核開發大會上, Paul Turne就提高了這一點: google 基於自己的經驗,以更好的形式重新組織了許多控制器。 明確的列表使得 cgroup.controllers 裏的子系統使得問題更糟。 所以這一點給個D

Providing a resource-consumer ID

在part 5裏我們看到當內存的頁被釋放時,能夠標識出誰獲得了它們。但是在內容被寫出時候,不知道誰負責IO。所有的子系統使用一個繼承,一個cgroup可以作爲所有資源類型的資源消耗 者ID。這是解決這個問題的一個清晰的方案,但很難說他是一個很好的方案(不同的資源可能差別很大)。 所以我給B

Processes or threads 傳統的cgroup允許一個進程裏的線程屬於不同的cgoups。想象一個可靠的應用場景很困難,但是也不是一點可能都沒有。 cpusetcgroup能夠限制進程到多個cpu或者numa系統裏的內存節點。前者可以通過在任一個線程上使用系統調用sched_setaffinity()或者程序taskset,而不用涉及到cgroups。但是內存節點 只能通過cgroups配置。 cgroup可以更細粒度的控制單個的線程優先級,它允許單個線程或者cgroup有100000個weight分級,而不像傳統linux調度器40個nice分級那樣。統一繼承只允許進程(線程集合)能夠在不同 的cgroups。這個主意看起來不錯,但是又引發了一個問題:我們是使用控制?線程還是進程?或者其他別的呢? 無論結果如何,不再支持單個線程的轉移是個好主意。 A

Code simplicity 統一繼承只是漫長過程中的一步,還有很多要提高的地方。我們很明確只有進程而不是線程最終在cgroups裏,並且他們只需要呆在一個單獨cgroup裏, 這當然會帶來簡化。 所以給A

Summary: 統一繼承所依賴的基礎還在建設中,現在還不能要求太多。

Auto-group Scheduling 正如2010年總結的,除了cgroups,還有其他的方法去批量控制進程。使用爲cgroups開發的group調度,Mike Galbraith創建了一個不同的自動的把程序分組調度的機制。標準的unix調度器和 許多追隨者試圖公平的對待進程,但是進程並不是很需要公平的對待。 進程組自動調度有兩個相關的問題: 1. Linus Torvalds提出的。他建議爲了這個目的而使用進程組,粒度有些細了。創建一個新的調度組有一定的花銷,所以做的太頻繁會引入難以接受的遲鈍。可惜沒有任何人做任何的測試來 說明這一點。最終的實現使用了”sessions”而不是“process groups”, 這就不會被創建的太頻繁。但並不能完美解決這個問題。 2. 第二個問題是有Lennart Poettering提出的。“在桌面系統上,這是完全不相干的”。auto-group現在是基於“sessions”做的,許多桌面會話管理都沒有把每個應用程序放到不同的會話 裏。一個正在開發中的會話管理:systemd 使用setsid()。 當時,Lennart的言論在當時沒有引起重視。

與最初cgroups開發者們相比,我們有自己的優勢,幾年的經驗和可運行的代碼。這是一筆可以轉化成爲我們優勢的財富。所以,去測試你對資源管理最新的認識。挑戰是受到自動分組調度的 鼓舞,你怎麼在linux上實現進程控制和資源管理。

Hindsight groups: highlighting some issues through contrast.

Hindsight groups 進程組是基本的控制單元,可通過交互式shell被創建,通過systemd,或通過其他session管理模塊。也可以通過prlimit或者類似的命令控制單個的進程,但是組控制沒有比進程組更細粒度的控制。在pid繼承中引進了一個新的level,來提供一個管理這些進程組的管理結構。“process domain”被引入到“session”和進程組級別上。通過domains組織的繼承很好的限制了進程。per-process-group, 是新的數據結構,定義了進程組的角色,很像signal_struct被分配給每個進程。它裏面包含了對組裏的進程各種限制, 例如可訪問的設別列表,可被使用的進程的集合。這些限制可以被任何有合適用戶id或者超級用戶許可的進程改變。各種可被共享的資源:內存,cpu,網絡可塊設備io。每個都有特殊需求,並被單獨管理。網絡和塊設備io比較類似,他們通常涉及覆蓋或者共享數據,他們很容易被虛擬化,所以一個子域可以被授權訪問一個虛擬設備,這個虛擬設備有可以把數據收發到一個真實設備。他們可以管理多個單獨的設備,不僅僅是進程所涉及到的。網絡系統需要管理自己的鏈路控制流量和從另外設備轉發來的流量。塊設備io子系統已經內部區分了metadata(使用REQ_META)和其他數據,對不同情景進行分類。結果,這兩個系統有他們自己的隊列管理結構。各種不同的度列算法可以根據原始的域將請求進行分類。或者支持標記單個的進程,這已經超出了hgroups的範圍。

內存使用管理跟其他的資源共享很不一樣,因爲它是通過空間而不是時間進行測量的。一個進程可以啓停使用這三種資源(網絡,塊設備io,cpu),或者暫時脫離擁有這些資源,而沒有任何 負面影響。而內存不能這樣。

Croups內存控制引入了兩個限制:硬性的限制(絕對不允許超越)和軟性的限制(只有在內存非常緊張的時候才能超越)。 cpu的限制跟內存的限制很類似,唯一的不同是對本地進程組的限制也可以在域上使用。任何有合適特權的進程都可以發出限制。 cpu調度可能是最複雜的資源管理。調度組大體有域,進程組,進程繼承組成,但是在每個級別都有組選項。

Filesystem notification, part 1: An overview of dnotify and inotify
文件系統通知API提供了一個讓應用程序監測一個文件打開、修改、刪除、重命名等操作事件的方法。過去,Linux中共有三種不同的文件系統通知API,瞭解和掌握這三種API之間的區別是十分有用的。同時在瞭解這些API的過程中,我們也能夠學習到很多API設計上的經驗。

本文是一些列文章的第一部分。我們首先介紹最原始的API:dnotify以及這個API的諸多不足。然後我們將討論inotify以及它對於dnotify的改進。在着一系列的最後,文章將介紹fanotify。

Filesystem notification use cases
爲了比較這三種不同API之間的區別,首先我們來看一下這些API的常見用例。

緩存文件系統對象模型
應用程序經常需要在自己內部維護一個精確反映文件系統當前狀態的模型。比如一個文件管理器就需要通過圖形界面來反饋文件系統當前的狀態。

記錄文件系統活動
應用程序希望記錄當前監控的文件系統中發生的某類事件。

監控文件系統操作
應用程序希望在某些事件發生或採取必要的措施。此類經典應用是反病毒軟件。當另外一個程序嘗試去執行一個文件的時候,反病毒軟件首先建廠文件的內容是否存在惡意代碼,從而判斷是否運行該文件被執行。

In the beginning: dnotify
在沒有操作系統相關API支持的時候,應用程序需要自己來完成對文件系統事件的監控工作。其中比較常用的是通過輪詢的方法來檢查文件系統的狀態。比如重複調用stat()和readdir()系統調用。這種實現方法顯然效率低下。此外,這種方法僅僅能夠監控一部分文件系統的事件。

爲了解決這些文件,Stephen Rothwell在Linux 2.4.0上實現了第一班的dnotify接口。由於這是第一次嘗試實現文件系統通知API,所以dnotify天生存在許多的不足。dnotify通過複用fcntl()系統調用來實現相應的功能。而隨後的inotify和fanotify均實現了新的系統調用。爲了開啓dnotify,需要使用如下系統調用: fcntl(fd, F_NOTIFY, mask);

其中的fd是一個需要監控的目錄的文件描述符。這種使用方法造成了dnotify只能對整個目錄進行監控,無法對某個特定文件進行監控。第三個參數mask用來指定需要監控的事件。詳細說明可以參考fcntl(2)的man page。

另外一個比較怪異的設計是dnotify在監控的事件發生的時候會嚮應用程序發送信號來進行通知(默認爲SIGIO)。這個信號本身並不能反映到底是哪個被監控的目錄發生的事件。需要使用sigaction()通過SA_SIGINFO來建立信號處理函數。在隨後的信號處理函數中會接收到一個siginfo_t參數。在該參數中有一個si_fd域,通過該域可以獲取到發生事件的目錄。同時應用程序需要遍歷整個監控目錄列表來了解對應的目錄信息。

這裏是一個使用dnotify的簡單程序

Problems with dnotify
正如上面的介紹,dnotify在設計上就存在諸多的不足。比如:只能監控整個目錄而不是單獨文件,可以監控的事件不全,無法監控文件的打卡或者關閉。

上面這些其實並不是最嚴重的問題。使用信號作爲通知的方法造成了dnotify使用困難。首先信號的投遞是異步的,這樣獲取信號的處理函數就很容易出錯,儘管可以使用sigwaitinfo()來同步獲取信號。同時信號必須要被及時處理,應用程序處理速度不夠時,就會有信號丟失的問題。

信號的使用還有其他的問題,不如在事件發生時,應用程序無法得知具體發生事件的類型和具體文件,這就需要應用程序進行復雜的處理流程。同時如果其他庫函數也會處理相同的信號,這樣就會造成信號的衝突。

最後的問題是,只能監控目錄會造成應用程序需要打開大量的文件,造成文件描述符使用很多。此外,打開大量文件描述符會造成文件系統無法被卸載。

儘管如此,dnotify仍然提供了一種高效的監控文件系統事件的方法,並且dnotify已經被廣泛使用在一些應用程序中,比如:Beagle桌面搜索。但是顯然涉及一種更晚上的API將會讓程序員的日子更好過。

Enter inotify
inotify由John McCutchan在Robert Love的協助下開發完成,並於Linux 2.6.13版本發佈。inotify的發佈解決了dnotify中的一系列明顯的問題

inotify中使用了三個新的系統調用:

inotify_init(); inotify_add_watch(); inotify_rm_watch();
inotify_init()創建一個inotify實例——一個內核數據結構用來記錄需要被監控的文件系統對象,並且維護該對象相關的事件列表。該調用會返回一個文件描述符用來完成其後的相關工作。

inotofy_add_watch()允許用戶修改被監控對象的相關事件集合。當然inotify_rm_watch()操作與其相反。

inotify_add_watch()函數原型如下:

int inotify_add_watch(int fd, const char *pathname, unint32_t mask);
其中mask參數指定了需要監控的事件集合。pathname參數指定了需要監控的文件路徑。下面的例子展示了監控mydir目錄中文件創建、刪除事件的相關代碼:


int fd, wd;
fd = inotify_init();
wd = inotify_add_watch(fd, “mydir”,
IN_CREATE | IN_DELETE | IN_DELETE_SELF);

詳細信息和可以參考inotify(7)的man page。inotify可以監控的事件集合是dnotify的超集。最顯著的是對於文件打開和關閉的監控。

inotify_add_watch()返回一個監控描述符。該描述符是一個整數類型用來唯一標示inotify監控的一個特定的文件系統對象。在監控的事件發生後,應用程序可以通過read()來獲取對應的信息。其結構如下:


struct inotify_event {
int wd; /* Watch descriptor */
uint32_t mask; /* Bit mask describing event */
uint32_t cookie; /* Unique cookie associating related events */
uint32_t len; /* Size of name field */
char name[]; /* Optional null-terminated name */
};

數據結構中具體的說明請參考相關手冊。

inotify最重要的問題是沒有提供地櫃監控功能。當然我們可以通過爲每個子目錄來創建監聽事件來解決。

Example program
下面的程序是inotify的演示程序。

int
main(int argc, char *argv[])
{
struct inotify_event *event

inotifyFd = inotify_init(); /* Create inotify instance */
for (j = 1; j < argc; j++) {
wd = inotify_add_watch(inotifyFd, argv[j], IN_ALL_EVENTS);
printf(“Watching %s using wd %d\n”, argv[j], wd);
}
for (;;) { /* Read events forever */
numRead = read(inotifyFd, buf, BUF_LEN);

/* Process all of the events in buffer returned by read() */
for (p = buf; p < buf + numRead; ) {
event = (struct inotify_event *) p;
displayInotifyEvent(event);
p += sizeof(struct inotify_event) + event->len;
}
}
}

How inotify improves on dnotify
inotify相比於dnotify有多項改進:

可以監控目錄和文件
通過read()取代信號
不需要打開被監控目錄
更多的事件
豐富的重命名事件
IN_IGNORED事件
Concluding remarks
本文我們主要介紹了dnotify和inotify,以及inotify相對於dnotify的改進。下面的文章將會更詳細的介紹inotify以及如何使用inotify來構建健壯的應用。

Filesystem notification, part 2: A deeper investigation of inotify
在這個系列的第一篇文章中,我們簡單瞭解了Linux文件系統通知API dnotify以及該接口的各種不足。隨後文章介紹了它的繼承者inotify,它是如何解決此前dnotify遺留的各種問題的以及帶來了哪些好處。在上一篇文章中,我們看到了如何使用inotify來創建一個簡單的文件系統狀態監控程序。然後,inotify的使用並不像看起來那麼簡單。

現在,我們來深入瞭解inotify。我們將通過一個監控目錄樹狀態的應用程序來深入瞭解inotify接口。一方面,通過該程序我們可以瞭解inotify是如何完成這一工作的;另一方面,我們也將看到inotify的一些不足。

程序僅僅用來作爲演示使用,並沒有對性能有任何考量。程序的使用方法如下:

./inotify_dtree …
這個程序的功能是動態的監控命令行指定的目錄及其子目錄的狀態。這個程序的作用類似於一個GUI的文件管理器。

爲了控制程序的大小,我們做了必要的簡化:

目前程序僅僅監控一個指定目錄下的子目錄的狀態,對於其他文件則不予關注。儘管監控其他類型的文件十分簡單,但是我們僅僅把注意力放在子目錄的監控上,因爲這是該程序最具挑戰的部分;
程序目前僅僅記錄了目錄的名字和對應的監控描述符。一個實用程序還會監控其他信息,比如文件屬主,權限和修改事件;
目前保存狀態的數據結構爲一個鏈表,這樣做是爲了簡單。真是情況下,需要使用更加有效的樹形數據結構。
經過上述簡化後,我們依然可以看到這樣一個監控目錄狀態的簡單程序對inotify來說仍然是個挑戰。

Challenge 1: recursively monitoring a directory tree
inotify目前不能遞歸監控目錄結構。也就是說,inotify可以監控目錄mydir以及它的直接孩子,但是不能監控子目錄的孩子。

因此,爲了能夠監控整個目錄樹,我們需要遍歷所有子目錄。這就需要程序遞歸遍歷每個子目錄,然後監控這個子目錄本身,而這樣會有可能造成競爭。舉例來講:

我們掃描mydir目錄,監控mydir的所有子目錄;
然後監控mydir目錄。
加入在監控mydir目錄前,有個新的目錄被創建,比如mydir/new,或者有個目錄被移動到mydir目錄中,那麼這個事件應用程序是無法感知到的,因爲程序已經完成了對子目錄的掃描,同時mydir目錄本身還沒有被監控。

上面問題的解決方法是改變添加監控的順序,即先監控父目錄,然後再掃描添加子目錄的監控。但是這樣仍然會有問題。在父目錄監控建立後,如果有目錄被創建,那麼應用程序會接收到兩次事件。一次是新目錄創建時父目錄監控接收到的事件,另外一次是掃描子目錄時對子目錄建立監控時的事件。當然這是無害的,因爲對相同文件對象調用inotify_add_watch()兩次返回的監控描述符是相同的。

此外,還有其他的競爭,比如在掃描子目錄過程中有目錄被刪除了,那麼我們最好的處理方法就是忽略這一錯誤。

如果想完成上述工作,可以使用nftw()庫函數,該函數對相關的操作進行了必要的封裝,在遍歷目錄過程中,針對每個文件對象會調用用戶提供的毀掉函數,這樣事情就簡單了許多。
下面就是一個nftw()回調函數的例子:

static int traverseTree(const char *pathname, const struct stat *sb, int tflag,
struct FTW *ftwbuf)
{
int wd, slot, flags;

if (! S_ISDIR(sb->st_mode))
return 0; /* Ignore nondirectory files */

flags = IN_CREATE | IN_MOVED_FROM | IN_MOVED_TO | IN_DELETE_SELF;
if (isRootDirPath(pathname))
flags |= IN_MOVE_SELF;

wd = inotify_add_watch(ifd, pathname, flags | IN_ONLYDIR);
if (wd == -1) {

  /* By the time we come to create a watch, the directory might
     already have been deleted or renamed, in which case we'll get
     an ENOENT error. In that case, we log the error, but
     carry on execution. Other errors are unexpected, and if we
     hit them, we give up. */

  logMessage(VB_BASIC, "inotify_add_watch: %s: %s\n",
          pathname, strerror(errno));
  if (errno == ENOENT)
      return 0;
  else
      exit(EXIT_FAILURE);

}

if (findWatch(wd) > 0) {

  /* This watch descriptor is already in the cache;
     nothing more to do. */

  logMessage(VB_BASIC, "WD %d already in cache (%s)\n", wd, pathname);
  return 0;

}

slot = addWatchToCache(wd, pathname);

return 0;
}
Challenge 2: handling overflow events
inotify的消息隊列需要佔用內核內存,因此這個隊列有大小限制。具體的大小可以參考intofy的手冊或者查看/proc目錄下的對應文件。當隊列達到上限後,inotify會添加一個溢出事件到隊列中,並且丟棄後面的所有事件,直到程序開始讀取隊列中的事件。
這一行爲導致的結果就是應用程序會丟失某些事件,換言之,inotify不能生成一個十分精確的文件系統狀態。

事件溢出對於我們的演示程序來講意味着文件系統的狀態和程序目前保存的狀態不一致了。在這一個問題發生後,我們唯一能做的就是關閉所有已經打開的監控描述符,然後重新對緩存狀態進行初始化。這裏的相關代碼請參考reinitialized()函數。

儘管我們可以通過增大隊列的大小來儘量避免溢出的發生,但是所有使用inotify來監控文件系統狀態的程序都應該小心的處理溢出問題。

此外,其他一些邊角問題或者程序bug也可能造成狀態的不一致。程序應該處理好這些問題。

Challenge 3: handling rename events
如前所述,inotify對於dnotify最大的改進是對於重命名事件的處理。當一個文件對象被重命名後,inotify會產生兩個事件:IN_MOVED_FROM和IN_MOVED_TO。IN_MOVED_FROM表示文件移動前的目錄,IN_MOVED_TO表示文件移動的目標目錄。應用程序在接收到這兩個事件後,name域表示新、舊文件名。這兩個事件有相同的cookie域用來讓應用程序進行識別。

重命名會對應用程序帶來一些列的挑戰,比如我們的演示程序中,一個重名名操作會讓我們對緩存進行三次操作。

當然,更加智能的緩存設計能個避免和消除這個問題。比如用樹形結構來保存文件系統的狀態,這樣在處理重命名操作時,我們只需要修改一個對象的指針就可以了。

重命名事件還會帶來其他的問題,不如當我們僅僅監控目標目錄的事件時,我們只能接收到IN_MOVED_TO事件。而對應的FROM事件應用程序就是收不到了。此外,如果程序僅僅監控了源目錄的事件,那麼我們將僅僅接收到IN_MOVED_FROM事件,從而造成我們把這個事件作爲刪除目錄事件來進行處理。

此外,當程序接收到IN_MOVED_FROM事件後,並不能確定後面還會有一個IN_MOVED_TO事件,而且inotify不保證IN_MOVED_FROM/TO事件是連續發送個程序的。這就造成重命名事件的處理十分複雜。當然,有人會問,爲什麼不把IN_MOVED_FROM作爲刪除事件來處理,把IN_MOVED_TO當做重命名事件來處理呢。這樣就讓程序簡單了很多。但是當有大量重命名操作發生的時候,程序要不斷的刪除某些目錄的監控,然後再建立新的監控,這樣會造成程序的效率非常低。

上面重命名事件處理中的問題在現實中的解決方法是:檢查IN_MOVED_FROM事件後面是否有連續的IN_MOVED_TO事件。如果有,則按照重命名事件來進行處理。否則按照刪除事件來進行處理。

演示程序中的processInotifyEvents()函數提供了一種處理重命名事件的方法。通過在read()操作中間插入2ms的延遲,我們可以解決99.8%的問題。

Challenge 4: using pathnames for notifications
在程序監控文件系統狀態的時候,inotify的一個相對於dnotify的優點是可以提供文件的路徑信息。但是這個信息處理起來是十分困難的。因爲一個文件可能有多個路徑,因爲一個文件可以有多個硬鏈接。

加入我們需要監控如下的一個目錄樹:一個文件對象有兩個硬鏈接,一個是mydir/abc/x1;另外一個是mydir/xyz/x2。我們只監控了mydir/abc/x1。

當我們對這個文件進行操作的時候就會出現問題,當我們打開mydir/abc/x1文件時,程序會接收到信號,但是當打開mydir/xyz/x2文件時,程序就不會接收到信號。概括來說,inotify僅針對監控的路徑產生信號。

Other limitations of the inotify API
inotify除了上面詳細介紹的問題外,還有其他的限制:

目前的事件中沒有包含產生事件的進程信息。這樣當一個監控程序自己產生了一個事件後,程序自己無法建議區分;
inotify沒有提供一個看門狗的功能。也就是說inotify僅僅發送事件,但是不會阻塞事件的發生。所以無法使用inotify來實現諸如反病毒程序的功能;
inotify僅僅報告通過文件系統API產生的事件,也就是說通過遠程文件系統和虛擬文件系統產生的事件,程序是無法接收到的。同時通過文件影射對文件進行的修改,程序也是無法接收到的。這就需要程序不斷調用stat()和readdir()來進行監控。
相對於dnotify,inotify已經有了很大的改進。但是inotify自身依然存在一些不足,後面的文章,我們將一些來看一看fanotify API。

Anatomy of a system call, part 1
深入剖析系統調用 (一)
By: David Drysdale
系統調用是用戶空間與內核空間交互的首要機制。很有必要探索清楚系統調用的細節。比如,內核如何實現了系統調用的跨平臺與高效性。 作者曾經將FreeBSD的Capsicum security framework機制移植到Linux上,並且在該工作中爲Linux增加了幾個新的系統調用(包括不常使用的execveat())。故而對系統調用的細節非常熟悉。這個文章系列共包括兩篇文章,剖析了Linux系統調用的實現細節。第一篇文章分析了系統調用的基本實現機制:以read()爲例介紹了系統調用的基本實現以及用戶空間調用它的方法。第二篇文章將介紹其它一些不常用的系統調用,以及其它系統調用的實現機制。 系統調用不同於常規的函數調用,因爲被調用的代碼在內核中執行。需要用到一條特殊的指令將CPU切換到Ring 0(特權模式)。而且,被調用的內核代碼通過系統調用號標識,而不是函數地址。

利用SYSCALL_DEFINEn()定義系統調用 探索Linux系統調用機制時,read()系統調用是很好的入門範例。它在fs/read_write.c中實現。這個函數很簡單,它將絕大部分工作傳給了vfs_read()。從調用的角度看,該代碼的關鍵部分是SYSCALL_DEFINE3()宏定義的函數。但是,僅從代碼來看,並不容易弄清楚誰調用了這個函數。

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* … */
SYSCALL_DEFINEn()是一系列的宏定義,n爲正整數,表示參數的個數。這些宏爲Linux內核定義系統調用的標準方式。對每個系統調用而言,這些宏(include/linux/syscalls.h)都有兩個不同的輸出:

SYSCALL_METADATA(_read, 3, unsigned int, fd, char __user *, buf, size_t, count)
__SYSCALL_DEFINEx(3, _read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* … */
宏SYSCALL_METADATA()爲系統調用定義了一組ftrace需要用到的元數據集合。這個宏只在CONFIG_FTRACE_SYSCALLS被定義時纔會展開,展開後會定義用於描述該系統調用的數據以及該系統調用的參數。(另一篇文章http://lwn.net/Articles/604406/ 更加詳細地描述了這些定義) __SYSCALL_DEFINEx()更有趣,因爲它包含了系統調用的實現。將該宏以及GCC的類型擴展展開後,我們會看到一些有意思的特性:

asmlinkage long sys_read(unsigned int fd, char __user * buf, size_t count)
attribute((alias(__stringify(SyS_read))));
static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count);
asmlinkage long SyS_read(long int fd, long int buf, long int count);
asmlinkage long SyS_read(long int fd, long int buf, long int count)
{
long ret = SYSC_read((unsigned int) fd, (char __user *) buf, (size_t) count);
asmlinkage_protect(3, ret, fd, buf, count);
return ret;
}
static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
/* … */
首先,這個系統調用的真正實現爲函數“SYSC_read()”。但是這個函數是static的,不能在其它代碼塊中訪問它。SyS_read()是對SYSC_read()的封裝,這個函數有個別名叫sys_read(),並且在外部可見。仔細看一下這些函數別名,他們的參數類型是不同的。sys_read()聲明的類型更加嚴格(如第二個參數加了前綴__user*),而SyS_read()則聲明瞭一組整數類型(long)。從歷史角度看,聲明成long,可以確保在64位的平臺上正確地符號擴展32位的值。 針對SyS_read()封裝,還需要注意GCC的指示符asmlinkage,以及asmlinkage_protect()調用。Kernel Newbies FAQ中介紹,asmlinkage表示該函數傾向於將參數放在棧上,而不是寄存器裏。asmlinkage_protect()表示編譯器不應該假設可以安全複用棧上的這些區域。 除了sys_read()的定義,include/linux/syscalls.h中也有相應的聲明。這是爲了讓內核中的其它代碼能夠直接調用系統調用的實現(大概有半打的位置直接調用了sys_read())。不過,最好不要在內核中的其它位置直接調用系統調用,而且這種行爲是不常見的。

系統調用表項 對sys_read()的調用者進行尋根溯源,能讓我們搞清楚從用戶空間到達這個函數的具體路徑。“generic”體系結構沒有提供系統調用函數的重載,include/uapi/asm-generic/unistd.h中包含了sys_read()的引用入口:

#define __NR_read 63
__SYSCALL(__NR_read, sys_read)
首先爲函數read()定義了系統調用的編號__NR_read(63),並且通過宏__SYSCALL將這個編號與sys_read()關聯起來。這種關聯是體系結構相關的。比如ARM-64在頭文件asm-generic/unistd.h中定義了一張表,該表映射了系統調用編號與相關函數的函數指針。(譯者注:多數操作系統教材都採用了這種方法來解釋系統調用的實現) 後面我們繼續關注X86_64體系結構,X86_64沒有使用這張通用的映射表。而是在arch/x86/syscalls/syscall_64.tbl中定義了自己的映射表。sys_read()對應的表項如下: 0 command read sys_read 第一項(0)表示read()在X86_64上的系統調用編號爲0(不是63),第二項(common)表示對X86_64的兩種ABI均有效。最後一項sys_read表示系統調用函數的名稱。(X86_64的兩種ABI將在下一篇文章中解釋)腳本syscalltbl.sh可以根據系統調用表syscall_64.tbl生成頭文件arch/x86/include/generated/asm/syscalls_64.h。該文件爲sys_read調用了宏__SYSCALL_COMMON()。反過來,這個頭文件也可以用來生成系統調用表sys_call_table,這張表是用於映射系統調用號與sys_name()函數的關鍵數據結構。

X86_64系統調用的調用過程 下面我們來解釋用戶空間的程序是如何調用系統調用的。這個過程跟體系結構密切相關,本文的剩下部分僅針對X86_64(其它X86體系結構的情景,將在下一篇文章中描述)。這個過程涉及到幾個步驟,下圖能夠幫助大家理解。

在上一節裏,我們提到了系統調用的函數指針表。在X86_64上,這張表的結構如下所示(利用GCC關於數組初始化的特性,能夠保證所有未聲明的表項,都能指向函數sys_ni_syscall()):

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 … __NR_syscall_max] = &sys_ni_syscall,
[0] = sys_read,
[1] = sys_write,
//
};
在64位的代碼路徑上,arch/x86/kernel/entry_64.S中的彙編函數system_call會訪問這張表。該函數利用RAX寄存器保存系統調用的編號,並隨後調用相應的函數。system_call首先會調用SAVE_ARGS宏將寄存器現場壓進堆棧。這跟前文提到的asmlinkage就對應上了。 繼續往調用路徑的外層走,彙編函數system_call在syscall_init()中被調用。這個函數在內核初始化的早期就會被執行。

void syscall_init(void)
{
/*
* LSTAR and STAR live in a bit strange symbiosis.
* They both write to the same internal register. STAR allows to
* set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
*/
wrmsrl(MSR_STAR, ((u64)__USER32_CS)<<48 | ((u64)__KERNEL_CS)<<32);
wrmsrl(MSR_LSTAR, system_call);
wrmsrl(MSR_CSTAR, ignore_sysret);
/* … */
wrmsrl指令用於將一個值寫入MSR寄存器。在我們的情景裏,通用的系統調用處理函數system_call()被寫入了寄存器MSR_LSTAR(0xc0000082)。在X86_64中,這是一個專用於處理SYSCALL指令的MSR。 把這些點串起來連起來,我們就能理清用戶空間到內核空間的脈絡了。在標準的ABI中,用戶空間調用系統調用時,需要先將系統調用編號放入RAX寄存器,其它的參數放入指定的寄存器(RDI,RSI,RDX用於存儲前3個參數),然後觸發SYSCALL指令。這條指令將CPU轉換到Ring 0,並且調用MSR_LSTAR中存儲的函數,system_call()。system_call()的代碼首先將寄存器壓入內核棧,再利用RAX中的值在sys_call_table表中查找函數指針,並調用之。這個函數指針封裝在SYSC_read()中,這層asmlinkage的封裝很薄。 OK,我們已經瞭解了最常見的平臺上實現系統調用的標準方法。下篇文章將繼續深入介紹其它體系結構上的情況,以及一些不太常見的情景。

Anatomy of a system call, part 2
深入剖析系統調用 (二)
By:David Drysdale
上篇文章描述了內核實現系統調用的最普通的方法,在最常見的X86_64平臺上解釋了一個普通的系統調用(read())。在這個基調上,本文將更深入地探索系統調用,涉及到其它X86體系結構以及其它的系統調用實現方法。我們從介紹X86體系結構的各種32位變種開始。下圖能夠幫助大家理解本文的內容。

X86_32:利用SYSENTER指令實現系統調用

在32位的X86_32系統中,系統調用的實現方法與X86_64系統類似。表格arch/x86/syscalls/syscall_32.tbl中關於sys_read()的項爲:

3 i386 read sys_read
X86_32中,read()的系統調用編號爲3。入口爲sys_read(),調用方式是i386。對這個表格處理之後,會在arch/x86/include/generated/asm/syscalls_32.h文件中生成對宏__SYSCALL_I386(3, sys_read, sys_read)的調用。當然,這個文件也可以反過來用於構建系統調用表:sys_call_table。

arch/x86/kernel/entry_32.S中的彙編函數ia32_sysenter_target會訪問這個表。不過這裏調用的SAVE_ALL宏壓入了不同的寄存器集合(EBX/ECX/EDX/ESI/EDI/EBP 而不是 RDI/RSI/RDX/R10/R8/R9)。這是由於該平臺的ABI不同於X86_64。

在內核初始化階段,將ia32_sysenter_target的位置寫入了MSR。此時用到的MSR爲MSR_IA32_SYSENTER_EIP(0x176),這是SYSENTER指令專用的MSR。 這基本解釋了從用戶空間下來的路徑。標準的現代ABI規定,X86_32程序需要先將系統調用編號(read()的編號是3)放入EAX寄存器。其它的參數放入指定的寄存器(EBX, ECX,與EDX用於存儲前3個參數),然後觸發SYSENTER指令。

該指令將CPU轉換到Ring 0,並且調用MSR_IA32_SYSENTER_EIP寄存器中指向的代碼(ia32_sysenter_target)。ia32_sysenter_target將寄存器壓入內核棧,根據EAX中的值在sys_all_table中查找對應的函數指針,並調用之。該指針指向sys_read(),這個函數僅僅對SYSC_read()中真正的實現代碼做了一層很薄的包裝。

X86_32: 通過INT 0x80調用系統調用

表格sys_call_table仍然被arch/x86/kernel/entry_32.S中的彙編函數system_call訪問。這個函數將寄存器保存在棧上,隨後利用EAX寄存器的值查找sys_call_table中對應的表項,並調用之。只是,system_call函數的位置需要通過trap_init()獲得:

#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif
這個函數將系統調用向量SYSCALL_VECTOR的處理函數設置爲system_call。隨後可以通過軟件中斷INT 0X80來觸發系統調用。

這是用戶空間觸發系統的最原始的方法。但在現代的處理器上則被避免使用,因爲它的執行速度比系統調用指令(SYSCALL與SYSENTER)要慢。

在這個較老的ABI中,程序在觸發系統調用前,需先將系統調用編號放入EAX寄存器,將其它參數放入指定寄存器(EBX,ECX與EDX用於存儲前3個參數),隨後觸發INT 0X80指令。該指令將CPU轉換到Ring 0,並隨後調用軟件中斷INT 0x80的處理函數,system_call()。System_call()中的代碼先將寄存器壓棧,並利用EAX的值中sys_call_table中查找到相應函數,如sys_read()。而sys_read()是對SYSC_read()中真正實現代碼的封裝。這個過程與利用SYSENTER的過程很相似。

X86的系統調用機制小結
上文描述過以下幾種用戶空間觸發系統調用的方法:
1.64位程序使用SYSCALL指令觸發系統調用。這條指令最初由AMD引入,Intel的64位平臺隨後也實現了它。出於跨平臺(Intel/AMD)兼容性的考慮,這條指令是最佳選擇。

2.現代32位程序使用SYSENTER指令觸發系統調用。Intel在IA32體系結構上最先引入這條指令。

3.古代32位程序使用INT 0x80觸發軟件中斷,進而實現用戶空間對系統調用的觸發。但是在現代32位處理器上,這種方法要遠慢於SYSENTER指令。

在X86_64平臺上觸發X86_32系統調用(兼容模式)
現在我們考慮一種更加複雜的情景:如果在X86_64平臺上執行32位程序,會發生什麼?從用戶空間的角度看,沒有任何不同。因爲執行的用戶代碼是完全相同的。

當使用SYSENTER時,X86_64內核會在寄存器MSR_IA32_SYSENTER_EIP中註冊一個不同的函數。該函數與X86_32內核中的相應函數同名(ia32_sysenter_target),但是實現卻有區別(實現在文件:arch/x86/ia32/ia32entry.S)。這個函數雖然也保存了舊式風格的寄存器(32位),卻使用了不同的系統調用表,ia32_sys_call_table。這張表由32位的表項構造,比如sys_read()的編號爲3(與32位系統相同)而不是0(64位系統中sys_read()的系統調用編號)。
當使用INT 0x80時,X86_64對trap_init()的實現如下:

#ifdef CONFIG_IA32_EMULATION
set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
這段代碼將中斷向量IA32_SYSCALL_VECTOR(仍然是0x80)映射到了函數ia32_syscall。這個彙編函數(arch/x86/ia32/ia32entry.S)使用了ia32_sys_call_table而非64位系統的sys_all_table。

更復雜的例子:execve()與32位兼容性處理

現在爲大家描述一個更復雜的系統調用: execve()。我們依然從這個系統調用的內核實現一步步向外探索,並在這條路徑上對比與read()這種簡單系統調用的不同。下圖可以幫助大家理解這個過程。

與read()類似,execve()定義在文件fs/exec.c中(read()定義在fs/read_write.c)。不同的是,就在這個函數之後,還定義了一個有趣的函數(如果打開了CONFIG_COMPAT)。

SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user const __user , argv,
const char __user const __user , envp)
{
return do_execve(getname(filename), argv, envp);
}
#ifdef CONFIG_COMPAT
asmlinkage long compat_sys_execve(const char __user * filename,
const compat_uptr_t __user * argv,
const compat_uptr_t __user * envp)
{
return compat_do_execve(getname(filename), argv, envp);
}
#endif
在調用路徑上,兩種實現最終都會通過do_execve_common()函數執行真正的工作(sys_execve()->do_execve()->do_execve_common() vs compat_sys_execve()->compat_do_execve()->do_execve_common())。在這兩條路徑上,主要工作都是爲user_arg_ptr賦值。這些數據結構通過傳遞指針,保存了系統調用的參數,並且標示了這個參數是否爲32位的兼容ABI。如果是,相應的指針就指向一個32位的用戶空間地址,否則便是一個64位的值。這是從用戶空間拷貝數據時必須考慮的。
read()不需要區分調用者是32位的程序還是64位的程序,因爲它的參數傳遞方式是指針到值。而execve()必須區分調用者的類型,因爲它的參數傳遞方式是指針到指針。這是一種常用的手段。其它利用指針傳遞參數的系統調用(或者傳遞的數據結構中包含指針,如struct iovec或struct aiocb)也採用了此方法。
對X32 ABI的支持
爲了兼容不同平臺,execve()同時具有兩種實現,稍有一些複雜。系統調用表也是如此。X86_64平臺上,64位系統調用表中有兩個與execve()相關的表項:

59 64 execve stub_execve

520 x32 execve stub_x32_execve
這張64位的表格中,系統調用編號爲520的表項是專爲使用X32 ABI的應用程序而設的。使用X32 ABI的程序雖然運行在X86_64處理器上,但是卻使用32位的指針。這兩個表項表明,64位程序使用函數stub_execve(),而X32 ABI程序使用stub_x32_execve()函數。

之前在介紹read()時,並未提及X32 ABI。因爲read()沒有使用指針傳遞參數,它默認就兼容32位程序。使用read()時,32位程序的系統調用路徑與系統調用編號均可以與64位程序共享。

stub_execve()與stub_x32_execve()均定義在arch/x86/kernel/entry_64.S中。這兩個彙編函數分別調用sys_execve()與compat_sys_execve(),同時將額外的寄存器(R12-R15,RBX與RBP。“額外”是指與SAVE_ARGS相比。)保存到內核棧。文件arch/x86/kernel/entry_64.S中還有很多以“stub_*”開頭的函數封裝,封裝了其它的系統調用(如rt_sigreturn(),clone(), fork(), vfork())。這些系統調用一旦被執行,那麼返回用戶空間時,應用程序的指令地址(IP)以及(或者)用戶棧可能會發生改變(將執行其它的進行/線程)。

在X86_32平臺上,32位的系統調用表中只有一項與exevce()相關。該表項在形式上與read()略有不同:

11 i386 execve sys_execve stub32_execve

首先execve()在32位平臺上的系統調用編號爲11,64位平臺上是59(或520)。這張表的表項多出了一個字段,用以保存兼容入口stub32_execve。如果內核被編譯爲32位,該字段會被簡單的忽略掉,sys_call_table的表項“11”的入口函數就是sys_execve()。
不過,如果內核被編譯爲64位,IA-32的兼容代碼就會將stub32_execve()插入ia32_sys_call_table的表項“11”。這個函數定義在arch/x86/ia32/ia32entry.S中:

PTREGSCALL stub32_execve, compat_sys_execve
宏PTREGSCALL使得stub32_execve()調用compat_sys_execve()(將這個函數的地址存入RAX中),並將額外的寄存器(R12-R15,RBX與RBP)保存到內核棧(與上文提到的stub_execve()類似)。

gettimeofday(): vDSO
有些系統調用僅從內核中讀取少量信息,如果爲這些系統調用變換特權級別,開銷就顯得特別大。vDSO(Virtual Dynamically-linked Shared Object)通過將包含這些信息的頁面映射到用戶空間,加速了這些只讀系統調用的執行。而且,這個頁面使用了ELF共享庫的格式,可以直接與用戶空間的程序進行鏈接。

用ldd查看一個gcc編譯出的程序,可以看到vDSO通常被處理爲依賴的共享庫linux-vdso.so.1 或 linux-gate.so.1(當然這兩個庫沒有對應實際的文件)。在進程的內存映射中也能找到vDSO的蹤影(/proc/PID/maps中的[vdso])。

內核在過去曾使用vsyscall來做這件事,由於安全上的原因才被vDSO取代。Johan Petersson寫過一篇文章,描述了vsyscall將頁面映射爲用戶空間ELF對象(映射在一個固定的地址上。譯者注:映射在固定的地址上,存在非常大的安全漏洞。vDSO將自己映射在一個隨機地址上)的方法。
這篇Linux Journal上的文章講述了vDSO的細節(略有過時)。我們在本文中僅描述vDSO的基本概念,並解釋一下vDSO中的系統調用gettimeofday()的實現。

首先,gettimeofday()需要訪問數據。內核將相關的數據結構vsyscall_gtod_data導出到了一個特殊的數據段.vvar_vsyscall_gtod_data。鏈接器可以將.vvar_vsyscall_gtod_data鏈接進內核的__var_page段。在內核的啓動階段,函數setup_arch()調用函數map_vsyscall()將_var_page段映射到了一個固定的地址上。

函數_vdso_gettimeofday()實現了vDSO版的gettimeofday()。這個函數被標記爲notrace,從而阻止編譯器增加用於性能分析的輔助代碼(比如mcount())。定義在該函數之後的gettimeofday(),被簡單定義爲_vdso_gettimeofday()的別名(weak alias)。爲了是最終的頁面看起來像一個ELF共享對象,gettimeofday()與_vdso_gettimeofday()同時被導出到vdso頁面。(*)

爲了使新創建的vDSO進程能夠訪問vDSO頁面,setup_additional_pages()(譯者注:在upstream中,這個函數貌似已改名爲arch_additional_pages())中的代碼將vDSO頁面映射到了一個隨機的地址(譯者注:用戶空間的地址)上,這個地址由vdso_addr()函數在進程啓動時選定。使用隨機地址在一定程度上解決了vsyscall的安全問題,但是帶來了一些不便。用戶空間的進程需要自己去獲得vDSO頁面的位置。頁面地址作爲ELF文件的輔助向量曝漏給用戶空間。ELF文件的加載器(load_elf_binary())使用宏ARCH_DINFO設置AT_SYSINFO_EHDR輔助向量。用戶空間的程序通過函數getauxval()查找相關的輔助向量,即可找到vDSO頁面的地址(實際上,這些工作由glibc代勞了)。

出於完整性考慮,我們再略提一下vDSO機制中用於32位程序的重要特性。內核在啓動階段會決定哪種系統調用機制爲最佳。並將合適的機制(SYSENTER,INT 0x80, 或者AMD處理器的SYSCALL)封裝進_kernel_vsyscall()函數。用戶空間的程序可以調用這個封裝,並選擇一種最快的方式進入內核。Petterson的文章中有更加詳細的說明。

ptrace():系統調用的跟蹤機制
ptrace()系統調用本身採用常規的方法實現。但是它可以干預其它被追蹤系統調用的行爲。PTRACE_SYSCALL可以使被追蹤者在進入或退出一個系統調用時暫時停止執行。 請求PTRACE_SYSCALL後,相關線程的線程信息標記中的TIF_SYSCALL_TRACE會被置位。(線程信息標記是thread_info中的flag字段)之後的行爲是體系結構相關的,在此我們僅考慮X86_64的情況。

如果再次仔細閱讀系統調用路徑在彙編語言中的入口函數(包括X86_32, X86_64與IA32),我們會發現一個之前被忽略的細節:如果線程信息標記中有任何_TIF_WORK_SYSCALL_ENTRY標記(這是一組標記,包括TIF_SYSCALL_TRACE)被置位,系統調用的處理過程將進入另外一條完全不同的路徑。此時會調用syscall_trace_enter()函數(包括X86_32, X86_64與IA32),該函數根據標記組_TIF_WORK_SYSCALL_ENTRY中被置位的標記,進一步調用不同的函數。
TIF_SINGLESTEP: ptrace()單步執行指令。

TIF_SECCOMP: 在系統調用的入口進行安全檢查。

TIF_SYSCALL_EMU: 執行系統調用仿真。

TIF_SYSCALL_TRACE: ptrace()的系統調用追蹤。

TIF_SYSCALL_TRACEPOINT: ftrace的系統調用追蹤。

TIF_SYSCALL_AUDIT: 生成系統調用的統計信息。

換句話說,syscall_trace_enter()是各種系統調用攔截機制的控制點,包括TIF_SYSCALL_TRACE在內。將ptrace_stop()的why參數設爲CLD_TRAPPED,可以結束追蹤過程。ptrace_stop()會向在系統調用入口點被暫停的進程發送一個SIGCHLD信號。
結語:

幾十年以來,系統調用都是用戶程序與UNIX內核交互的標準方法。Linux內核又發展了一系列機制使得系統調用的實現更加方便,使用起來更有效率。儘管在不同的平臺上調用系統調用的方法略有不同,同時還存在一些特例。但這並不影響系統調用在調用機制上具有高度同構的特性。這種穩定與同構的特性使得很多有用的工具(strace,seccomp-bpf)實現起來非常方便。

Handling ARM architecture changes
目前x86處理器上的Linux系統可運行90年代創建的二進制程序。主要原因可歸結爲重視不破壞用戶態ABI兼容性,其實還有另一因素起作用:x86硬件架構同樣關注保證舊程序能夠正常運行。ARM架構就不同了,它的演進導致一些老的應用不能正常運行(或者根本不能運行)。這對那些不想破壞應用同樣想保持內核長期可維護性的內核開發者來說,是個難題。

最近,Google的Colin Cross提出一個問題。ARM架構不時會發佈一個主版本,最新的是ARMv8。該版本增加了64位支持以及其它的一些改動;ARMv8也提供了對老版本架構下的程序的兼容支持。但是這個兼容性也只能做到這個程度;特別是缺少對SWP指令(原子的讀-改-寫操作),SETEND指令(用於修改數據訪問的字節序)以及某些barrier類型的支持。這些還不足爲奇;這些特性在ARMv6或者ARMv7都已經被廢止了。ARM在去除這些指令上已經有明確的計劃。

當然,問題在於,已存在的程序仍然在使用這些指令。任何當前編譯的程序不會使用這些廢除的指令,但是仍然有大量的蹲在Google Play Store角落裏的程序最近沒有被編譯過,而且有可能永遠不會被重新編譯。目前來看,如果一個用戶在ARMv8設備上加載了這類程序,它根本就跑不起來。這與基於ARMv8的Android設備期望兼容已有程序相悖。

解決這個問題的辦法很直接:內核trap嘗試使用並模擬這些指令。使用這些指令的程序就可以繼續運行,儘管它們會運行的慢很多。Colin問內核社區是否願意接受一組實現這個模擬的補丁。他說,另一種選擇是Google用單獨的代碼樹爲所有Android客戶維護這些補丁;這樣,主線內核可能會跑不通Android兼容性測試。

鑑於Android對ARM的重要性,有人可能認爲這種方式的反對聲音可能會很小。但是ARM開發者Will Deacon反對這種做法,一旦對諸如SWP之類指令的模擬進入內核,它將會被維護很長時間。他同樣指出在程序裏使用SWP指令基本上就是個bug。Will認爲不動內核而是簡單的修改相關程序是更好的解決方法。

Catalin Marinas 補充道,任何換到ARMv8設備的人都將會從Google Store下載這些應用。他的觀點是,Google應該去推動這些舊程序被重新編譯,這樣當用戶在他們嶄新的ARMv8設備上下載應用時,就輕鬆愉快了。但是Grant反對這個做法,認爲強迫開發者重新編譯他們的應用會使得平臺變得不友好。並且很多這種老應用已經找不到維護的公司或者開發者了;想重新編譯這些程序基本不可能。但他們目前來說可以正常工作,不應該被破壞。

經過一陣交鋒,討論達成一個共識,就是破壞已有的應用程序不好。但是這個怎麼轉化成補丁進入主線內核仍然還不明瞭。開發者們同樣重申了向後兼容的規則:就是在沒有使用者的時候再刪除被廢止的功能。由於ARM在未來很可能繼續廢除硬件特性,內核開發者需要找到一種辦法,爲繼續使用被廢止特性提供最小支持。沒有人願意把內核轉成支持老ARM架構的模擬器。

最後,Catalin針對如何處理被廢止功能給出一個建議時間表。第一步出現在硬件廢除特性時,但是當前硬件仍需要支持它。內核社區需要找到辦法引起對這些被廢除特性的關注,並且鼓勵開發者去掉這部分內容。正確的方法應該是,給出替換這些被廢除特性的方法。

第二步需要在下一個硬件版本發佈時,根據ARM的實踐,被廢除的特性仍然存在但是可以被關閉。這時,內核將關閉這些特性並且軟件模擬它們;並且將警告發給使用這些廢除特性的應用程序。程序仍可正常工作(可能會變得更慢)。這裏有個挑戰是,在移動設備上,很難讓人注意到這些內核警告;用戶不會經常讀日誌文件。那麼對這類設備,警告最好實現在其它層;例如Play Store。

在第三階段,額外的硬件修訂完全刪除這些特性。Catalan認爲,這時內核需要停止模擬這些指令並且發送SIGKILL給相關進程。模擬的代碼可以仍然在內核中,但是默認情況不應該被使用。最終,第四階段,模擬支持也應該被去除。

儘管在處理當前已有的問題可能還有些異議,ARM內核社區已經普遍認同這套處理硬件廢止功能的規則。ARMv6中廢除的SWP指令,還不算是處於第三階段,因爲讓使用者繞開SWP指令的工作還沒有做到位。所以,儘管部分開發者希望在ARMv8上看到使用SWP指令的程序被SIGKILL掉,對SWP指令的模擬還是有必要保留一陣子的。

但是,未來ARM廢除的硬件特性,將遵循上面的時間表。只要相關信息能夠及時的傳達給開發者,在相關支持完全刪除時,這些特性應該不會再有使用者。這樣在硬件架構演進時,可以更好的保證內核可維護性。那些希望從老G1手機上移過來的程序能夠正常跑在新設備上的用戶可能會失望了。

讓人民羣衆擁有更好的隨機數:記一個新系統調用的誕生
正如我們所知,大多數的隨機數算法都需要一個種子或者說seed,並且不管你的隨機算法多麼厲害,只要給定的種子和其他外部輸入是確定的、重複的,它吐出的隨機數序列就必然也是確定的、重複的。這裏的原因很好理解—-馮諾依曼結構的計算機是一個確定性的系統,你不可能指望它產生“真正”的隨機數。從實用主義的觀點來看,只要產生的隨機數序列別人不容易猜測到,或者更直白點說,產生隨機數序列的種子還有其他輸入參數別人不容易猜測到,這就已經足夠好了。LibreSSL也正是這麼做的,和其他很多做加密的庫一樣,它從/dev/urandom取出隨機數做種子,如果/dev/urandom不可用(什麼情況下/dev/urandom會不可用?太多了,攻擊者消耗光了所有fd以至於你打不開一個設備;程序被放到一個看不見/dev的被嚴格限制的container裏跑;程序被chroot了等等等等)它就退回到一個自己定義的隨機數生成器上去,這個隨機數生成器試圖用一些用戶態能接觸到的有隨機性的事件來生成隨機數序列(比如pid,時間戳等等,看起來也是個有趣的算法)。

那麼,/dev/urandom的數據是怎麼來的呢?目前的Linux內核在內部維護一個隨機數池,它通過一些對於用戶來說更不容易預測的事件,例如鍵盤的敲擊間隔、網卡數據包的到達間隔等等做爲種子來生成隨機數。一般場景使用/dev/urandom就足夠了,如果是那些更加敏感的場景,比如生成PGP key,或者只在某些初始化的時候用一次,以後就不再用的場景下,也可以使用/dev/random。它的要求更高,如果沒有足夠的隨機事件發生,就會讓read一直阻塞在那裏。應用這時往往就要提示一下用戶:敲敲鍵盤!

這樣做是不是足夠好了?LibreSSL的開發者們顯然不同意,他們抱怨說OpenBSD有一個叫getentropy()的系統調用,可以方便地向用戶返回想要的長度的隨機數。而在Linux世界裏就必須得和/dev/urandom打交道,由於上邊說的種種限制,很可能那些應用打不開/dev/urandom,這太不方便了。爲了響應這個需求,Tso決定添加一個類似的系統調用,相關的patchset已經出到第四版了【http://lwn.net/Articles/606202/】。

這個調用實現的功能比它的OpenBSD對應者還要更多些,比如:儘管/dev/urandom會在內核啓動的早期完成初始化,但你仍然有可能在它初始化完成之前調用這個系統調用,因此這個系統調用的語義中加入了表示未初始化完成的返回值;同時,它也允許用戶使用非阻塞的方式來取得隨機數,在隨機數不足時返回-EAGAIN而非阻塞在那裏,如此種種。

和以往一樣,這個patchset當然也收到了一些反對的聲音,不過沒有人從根本上反對添加這個系統調用,多數是一些細節修正,例如Christoph Hellwig認爲沒有必要添加額外的那些功能,那些功能使得這個系統調用的接口還有語義變得與OpenBSD不同了,這沒有必要。Tso的迴應是OpenBSD式樣的接口完全可以通過在Glibc中包裝一層來達到,這不成爲問題。

總之,到目前爲此沒有大的反對聲音,這個patchset有望最早在v3.17進入主幹。

友提:本文的lwn.net原貼下邊的討論相當精彩!

Two paths to a better readdir()
通常文件系統的工作遵守一定的模式:在一個目錄下查找文件,使用stat()獲得每個文件的信息。“ls -l ”就是以這樣的模式工作的典型例子,當然還有其它很多都是這樣工作的。這樣的工作模式在linux系統中運行的通常比開發者們想象中的的要慢,解決這個問題的方法發展的也同樣緩慢。 最近,Abhi Das提出了幾個可能的解決這一問題的方法或許可行。

“ls -l”這一類型的工作的模式很簡單:這種工作模式通常需要兩個系統調用,一個是getdents()(通常由C庫中的readdir()函數調用)獲得目錄文件中特定名字的文件。然後調用stat()獲得文件的更多元信息。stat()會有很大的開銷,每次調用都會迫使相應的文件系統進行必要的I/O去獲得需要的信息。在某些情況下,這些信息可能會分散到磁盤中的不同的地方,這就需要更多的I/O以完成請求。然而調用者並不需要由stat()返回的全部信息。這樣,也就是沒必要使用stat獲得全部的信息。如果能夠有一種方法讓應用開發人員可以設定需要獲得的信息,從而減小需要I/O的數據量,這樣就好了。

這個問題並不是新提出的。事實上,它是個很老的問題,在2009年Linux Storage and Filesystem Workshop就討論過這一問題。曾經,有人提到過使用一個xstat()的系統調用來解決這一問題,但是後來這種方法也沒能最終做到。目前,一些文件系統使用各自的方法來避免這種模式帶來較高的I/O。但是,內核中並沒有一種通用的方法來應對這一問題。近年來似乎很少人關注並解決這一問題。

Abhi提出了兩個獨立的方法,他希望獲得大家對這兩種方法的反饋,然後選取一種較好的並希望可以併入upstream。

xgetdents()
第一種方法建立在由David Howells在2010年提出的xstat()系統調用的基礎上。他添加了兩個新的系統調用:

   int xstat(int dirfd, const char *filename, unsigned int flags,
               unsigned int mask, struct xstat *info);
   int fxstat(int fd, unsigned int flags, unsigned int mask, struct xstat *info);

第一個函數由文件名字符串查找文件,而第二個函數由進程以打開的文件號獲得文件的信息。 flags參數可以調成函數的行爲(很少使用)。mask告訴內核哪些信息需要獲 取給進程。這裏只可以設置其中的很少的幾個bit。如XSTAT_MODE(權限),XSTAT_UID(文件所有者),XSTAT_RDEV(文件所在設備號),XSTAT_ATIME(最後一次訪問時間), XSTAT_INO XSTAT_ALL_STATS獲取全部信息。函數成功會添充info結構。

Abhi在此基礎上添加了另外一個:

int xgetdents(unsigned int fd, unsigned int flags, unsigned int mask,
void *buf, unsigned int count);
這裏fd是目錄的一個描述符,flags和mask和上面的一樣。但是mask擴展到可以支持文件的擴展特性了。 返回的信息放在了buf中, buf是count長的數組。xgetdents會將盡可能多的文件的信息放在buf中,直至buf填滿。
buf結構有些複雜。如下:

struct xdirent_blob {
unsigned int xb_xattr_count;
char xb_blob[1]; /* contains variable length data like
* NULL-terminated name, xattrs etc */
};
struct linux_xdirent {
unsigned long xd_ino;
char xd_type;
unsigned long xd_off;
struct xstat xd_stat;
unsigned long xd_reclen;
struct xdirent_blob xd_blob;
};
幾乎沒有資料對上面的結構進行說明, 我們必須查看源碼來了解這些結構的意義。每個文件的信息放在一個linux_xdirent中,文件名保存在xd_blob,中,如果存在xattr的話,之後是xattr的信息,這個結構需要費些功夫理解,但它確實可以使得只用一次系統調用就返回足夠的信息。

dirreadahead()
另一個方法很簡單,只需要增加一個系統調用:

   int dirreadahead(unsigned int fd, loff_t *offset, unsigned int count);

這個函數初始化對文件信息的讀取, 讀取從offset開始的count個目錄fd中的文件,offset會在函數使用後更新,表示實際讀取的文件個數,所以可以對一個目錄使用多次dirreadahead,內核會維護offset的信息。

在這種方法中,用戶還是需要調用getdents()和stat()來獲取所需的信息,但是,區別在於, 這些信息已經被填充到了內部的cache中了,所以這樣並不會再進行I/O了,這樣速度快了很多,一次讀取多個文件信息可以被成羣的處理,這樣及時不同文件的信息很分散,I/O會被按照最佳的順序進行。

在這兩種方法的patch的介紹中包含了在GFS2中的benchmark測試結果。在大量使用與”ls -l”類似的要調用getdents()和stat()的系統中,使用這兩種方法都會比mainline kernel的表現好。有些人可能會奇怪,dirreadahead()的表現比xgetdents()要好很多。這可能說明不了xgetdents()或GFS2的實現不好,但是卻說明,更加簡單的基於預讀的方法更值得考慮。

這種預讀的方法很容易就就讓人想到內核可不可以自動進行這種預讀,就像普通文件的預讀那樣,Trond Myklebust說NFS嘗試監測到要使用這種預讀的地方一邊自動進行預讀,更一般的情況下,這種情況很難監測,所以到目前,還是要靠用戶空間來觸發,上文提到的兩種方法都可以被使用,但是,即使沒有更好的benchmark測試,看起來相對於簡單的dirreadahead()方法更適合使用。

The RCU-tasks subsystem
RCU-task是類似RCU的機制,只是直到沒有進程引用舊數據時才釋放. 爲了證實可行性,Paul Mackenney(這鳥人是rcu方面的權威) 已經提交了一個驗證性的模型.

通常RCU使用一個指針指向被保護的數據.當被RCU保護的數據需要改變時,RCU首先做一次copy,在副本上做改動,而後指針指向副本.之後,通過新賦值的指針不會再訪問舊的數據.但在數據被改動之前,目前正在運行的代碼可能已經獲取了取得了舊數據的指針.所以現在舊數據不能馬上被釋放. RCU使用規則要求只能夠在一個原子的上下文中引用數據.每個CPU經歷一次上下文切換才能保證舊數據不再被任何cpu引用,進而可以被安全的釋放掉.因此RCU必須等待每個cpu都經歷了一個上下文切換或者空閒.

通常,一個cpu上最多隻有一個進程引用被rcu保護的數據.rcu關注什麼時候會cpu不再引用被保護的數據. 相比RCU來說,進程有可能在使用舊數據的過程中會被搶佔,而且一個cpu上可能有一個或者多個進程引用rcutask保護的數據.所以關注點就不一樣了. rcu-task機制是被用來描述沒有進程(not cpu)引用被保護的數據.rcu-task需要更慢的鎖機制,並稍微改變一下使用規則.

其api:

void call_rcu_tasks(struct head *rhp, void (*func)(struct rcu_head *rhp));

一旦度過了安全期,調用func()釋放相應的數據結構. rcu task沒有成對的rcu_read_lock()來保護被訪問的數據.

經過這麼多年的發展,rcu爲了儘量的可擴展,變的很複雜.而第一版的rcu task很簡單. 調用all_rcu_tasks()的進程被鏈到一條鏈上.有個內核進程負責維護這個條鏈,每秒鐘(後續版本會使用等待隊列)都會檢查是否有新的被加入到這條鏈上. 如果有,那麼這條鏈會被移動到一個單獨的鏈上,並等待安全期結束.

只有runnable的進程保留rcu task引用.每個持有引用的進程都會被打上一個特殊的標誌"rcu_tasks_holdout".當進程主動放棄cpu或者返回用戶空間時,放置在調度器裏的鉤子會清除這個標誌.有個單獨的內核線程每秒鐘循環10次去檢查鏈上的進程,被清除了特殊標記的進程會被從這條鏈上刪除.當鏈變成空的時候,執行釋放操作函數.並開始新一輪的循環.

隨着patch的完善,代碼也變得更加複雜,最近的大改動時跟進程退出相關的.進程可能會在被檢查到之前就已經退出了,顯然不能訪問退出的進程的特殊標誌位.新加代碼很大一部分時在處理這種情況.

目前還沒有模塊使用這種機制,patch裏的大多數評論來自與Peter Zijlstra.他很關注polling和相關的考慮.rcu-task看起來時對rcu一個很有用的補充,但是不要期望在3.17版本里見到它.

Year 2038 preparations in 3.17
在2038年1月19日這一天,32位的time_t變量將會溢出,帶來類Unix系統的末日.雖然2038看起來很遙遠,是時候開始關注這個問題了;需要保證代碼在未來能夠工作,現在開發的某些系統在24年之後也會存在.保證32位系統在2038年能夠正常工作的系統方案需要一段時間才能實現.但是一些最初的修改已經被加入到3.17內核中.

需要進行的改動與兩個數據結果密切相關:union ktime(ktime_t)和struct timespec.ktime_t結構類型隨2006高分辨率定時器而引入.它被設計爲內核內部的時間表示類型,ktime_t太不透明瞭,以至於它的定義隨底層體系結構的不同而有區別.

在64位系統中,ktime_t一直用一個整數記錄了納秒數.對這種格式數據的管理和算數運算非常方便,只要體系結構支持對64位操作.由於32位系統中通常不存在64位操作,ktime_t的定義也與64位系統中的定義不同.32位系統中分別用2個32位的變量記錄秒數和納秒數.內核代碼通過一系列經過包裝的函數來操作ktime_t變量,把32位系統和64位系統的區別隱藏起來,不影響內核其他部分.

在2038年,記錄秒數的32位域將會溢出,32位系統和64位系統的差異則會表現出來.因此,爲了解決2038問題,ktime_t變量需要修改.3.17內核中的第一個修改就是取消階梯式的ktime_t表示,強制使用64位納秒計數.這樣可能會影響32位系統的性能,特別是影響時間表示之間的轉換速度.正如changelog中提到,ARM和x86體系結構已經使用了這樣的表示,它們不會變得更慢.

把ktime_t結構和其他時間表示轉換快慢的問題先放到一邊,減少不必要的轉換看起來是有效的優化手段.3.17內核中還修改了部分子系統對時間的使用方式,使它們直接使用64位納秒計數.結果通常是對代碼的簡化,使代碼執行更快.

另一個數據結構是timespec結構

struct timespec {
__kernel_time_t    tv_sec;            /* seconds */
long        tv_nsec;        /* nanoseconds */
};

__kernel_ktime_t類型只是當前內核中ktime_t類型的另一個名稱,在32位系統中它就是32位變量.與ktime_t類型不同,timespec變量也在用戶空間中被使用,它也是內核ABI的一部分,因此不能修改timespec結構.3.17內種增加了如下的數據結構定義

struct timespec64 {
time64_t    tv_sec;            /* seconds */
long        tv_nsec;        /* nanoseconds */
};

在64位系統中,這個數據結構與timespec是完全相同的.在timekeeping代碼中,所有timespec數據都被修改成了timespec64類型.timekeeping操作的接口都被修改,以隱藏timespec64數據類型的引入,使用timespec64的一套新接口也已經引入.修改之後,timekeeping代碼中不再使用32位變量計數秒.

當前的修改離解決2038問題還有很大差距.但確是非常重要的一步修改,timekeeping代碼中在2038年不會有時間溢出.通過其他一些修改,系統的解決方案有可能展現出來.其中第一步就是把timespec64的使用從timekeeping內部擴展到內核其他部分.解決方案可能需要大量工作,但這是內核社區非常擅長的改格式修改的一個例子.假以時日,內核代碼能夠完全避免2038問題.

更艱難的修改是,把在2038年安全的代碼擴展到內核ABI和推動用戶程序開發者修改應用代碼.這需要與C庫開發者合作,同時考慮怎麼以最小的代價完成修改.期望修改迅速完成是不現實的.但目前這個問題已經引起了開發人員足夠的重視,在最後時刻之前解決這個問題是有希望的.第一步已經邁出,希望後續修改很快可以完成.

Ftrace: The hidden light switch
在ftrace誕生前,Linux內核性能調優是個很有挑戰的工作。但是當ftrace誕生後,這一工作開始變得簡單起來。

最近在Netflix的一個Cassandra數據庫系統升級後,出現了磁盤IO增加的問題。到底是cache命中率降低了,數據庫中的記錄變大了,預讀數量增長了還是其他應用程序的問題呢?如何來確定問題的根源並且修復這一問題呢?

  1. iosnoop
    首先我們來對服務器進行一些必要的健康檢查。這裏作者使用的工具是iosnoop。iosnoop是一個shell腳本。在運行iosnoop後,結果如下(通過-Q參數來獲取IO隊列的影響):

    ./iosnoop -ts

    STARTs ENDs COMM PID TYPE DEV BLOCK BYTES LATms
    13370264.614265 13370264.614844 java 8248 R 202,32 1431244248 45056 0.58
    13370264.614269 13370264.614852 java 8248 R 202,32 1431244336 45056 0.58
    13370264.614271 13370264.614857 java 8248 R 202,32 1431244424 45056 0.59
    13370264.614273 13370264.614868 java 8248 R 202,32 1431244512 45056 0.59
    […]

    ./iosnoop -Qts

    STARTs ENDs COMM PID TYPE DEV BLOCK BYTES LATms
    13370410.927331 13370410.931182 java 8248 R 202,32 1596381840 45056 3.85
    13370410.927332 13370410.931200 java 8248 R 202,32 1596381928 45056 3.87
    13370410.927332 13370410.931215 java 8248 R 202,32 1596382016 45056 3.88
    13370410.927332 13370410.931226 java 8248 R 202,32 1596382104 45056 3.89
    […]
    從結果可以看到,IO隊列對磁盤負載有較大的影響。

  2. tpoint
    爲了深入調查這些磁盤讀操作,作者使用了tpoint來跟蹤block:block_rq_insert事件:

    ./tpoint -H block:block_rq_insert

    Tracing block:block_rq_insert. Ctrl-C to end.

    tracer: nop

    #

    TASK-PID CPU# TIMESTAMP FUNCTION

    | | | | |

      java-16035 [000] 13371565.253582: block_rq_insert: 202,16 WS 0 () 550505336 + 88 [java]
      java-16035 [000] 13371565.253582: block_rq_insert: 202,16 WS 0 () 550505424 + 56 [java]
      java-8248  [007] 13371565.278372: block_rq_insert: 202,32 R 0 () 660621368 + 88 [java]
      java-8248  [007] 13371565.278373: block_rq_insert: 202,32 R 0 () 660621456 + 88 [java]
      java-8248  [007] 13371565.278374: block_rq_insert: 202,32 R 0 () 660621544 + 24 [java]
      java-8249  [007] 13371565.311507: block_rq_insert: 202,32 R 0 () 660666416 + 88 [java]
    

    […]
    從結果看,磁盤IO並沒有什麼異常。接下來,作者通過-s參數打印IO的調用棧。

    ./tpoint -s block:block_rq_insert ‘rwbs ~ “R“’ | head -1000

    Tracing block:block_rq_insert. Ctrl-C to end.
    java-8248 [005] 13370789.973826: block_rq_insert: 202,16 R 0 () 1431480000 + 8 [java]
    java-8248 [005] 13370789.973831:
    => blk_flush_plug_list
    => blk_queue_bio
    => generic_make_request.part.50
    => generic_make_request
    => submit_bio
    => do_mpage_readpage
    => mpage_readpages
    => xfs_vm_readpages
    => read_pages
    => __do_page_cache_readahead
    => ra_submit
    => do_sync_mmap_readahead.isra.24
    => filemap_fault
    => __do_fault
    => handle_pte_fault
    => handle_mm_fault
    => do_page_fault
    => page_fault
    java-8248 [005] 13370789.973831: block_rq_insert: 202,16 R 0 () 1431480024 + 32 [java]
    java-8248 [005] 13370789.973836:
    => blk_flush_plug_list
    => blk_queue_bio
    => generic_make_request.part.50
    […]
    結果顯示,系統發生了缺頁中斷,造成系統啓動預讀機制。作者調查的系統是ubuntu,並且已經開啓了2MB大頁。這樣預讀的數據大小就變成了2048KB,而不是默認4KB頁下的128KB。儘管上面的預讀可能造成磁盤IO過多,但是通過關閉預讀,問題並沒有緩解。

  3. funccount
    作者爲了更好的瞭解整個IO調用棧的情況,使用了funccount工具。

    ./funccount -i 1 submit_bio

    Tracing “submit_bio”… Ctrl-C to end.

    FUNC COUNT
    submit_bio 27881

    FUNC COUNT
    submit_bio 28478
    […]
    隨後我們查看filemap_fault()的調用情況:

    ./funccount -i 1 filemap_fault

    Tracing “filemap_fault”… Ctrl-C to end.

    FUNC COUNT
    filemap_fault 2203

    FUNC COUNT
    filemap_fault 3227
    […]
    從結果可以看到,submit_bio()的調用次數是filemap_fault()調用次數的10倍。

  4. funcslower
    爲了確認目前的調查方向是正確的,作者使用了funcslower來查看filemap_fault()的調用時間:

    ./funcslower -P filemap_fault 1000

    Tracing “filemap_fault” slower than 1000 us… Ctrl-C to end.
    0) java-8210 | ! 5133.499 us | } /* filemap_fault */
    0) java-8258 | ! 1120.600 us | } /* filemap_fault */
    0) java-8235 | ! 6526.470 us | } /* filemap_fault */
    2) java-8245 | ! 1458.30 us | } /* filemap_fault */
    […]
    看來作者是正確的。

  5. funccount (again)
    作者再次使用funccount來獲取readpage和readpages的調用次數。

    ./funccount -i 1 ‘mpage_readpage

    Tracing “mpage_readpage“… Ctrl-C to end.

    FUNC COUNT
    mpage_readpages 364
    do_mpage_readpage 122930

    FUNC COUNT
    mpage_readpages 318
    do_mpage_readpage 110344
    […]
    似乎造成問題的原因還是readahead。

  6. kprobe
    作者爲了確認此前對readahead的調整是正確的,使用了Kprobe來查看__do_page_cache_readahead()中的nr_to_read參數的數值。

    ./kprobe -H ‘p:do __do_page_cache_readahead nr_to_read=%cx’

    Tracing kprobe m. Ctrl-C to end.

    tracer: nop

    #

    TASK-PID CPU# TIMESTAMP FUNCTION

    | | | | |

    java-8714 [000] 13445354.703793: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8716 [002] 13445354.819645: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8734 [001] 13445354.820965: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8709 [000] 13445354.825280: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    […]
    從結果看,確實是512個pages,也就是2048KB。

  7. funcgraph
    爲了瞭解整個調用流程,作者使用funcgraph。

    ./funcgraph -P filemap_fault | head -1000

    2) java-8248 | | filemap_fault() {
    2) java-8248 | 0.568 us | find_get_page();
    2) java-8248 | | do_sync_mmap_readahead.isra.24() {
    2) java-8248 | 0.160 us | max_sane_readahead();
    2) java-8248 | | ra_submit() {
    2) java-8248 | | __do_page_cache_readahead() {
    2) java-8248 | | __page_cache_alloc() {
    2) java-8248 | | alloc_pages_current() {
    2) java-8248 | 0.228 us | interleave_nodes();
    2) java-8248 | | alloc_page_interleave() {
    2) java-8248 | | __alloc_pages_nodemask() {
    2) java-8248 | 0.105 us | next_zones_zonelist();
    2) java-8248 | | get_page_from_freelist() {
    2) java-8248 | 0.093 us | next_zones_zonelist();
    2) java-8248 | 0.101 us | zone_watermark_ok();
    2) java-8248 | | zone_statistics() {
    2) java-8248 | 0.073 us | __inc_zone_state();
    2) java-8248 | 0.074 us | __inc_zone_state();
    2) java-8248 | 1.209 us | }
    2) java-8248 | 0.142 us | prep_new_page();
    2) java-8248 | 3.582 us | }
    2) java-8248 | 4.810 us | }
    2) java-8248 | 0.094 us | inc_zone_page_state();
    […]

  8. kprobe (again)
    作者再次使用kprobe來查看max_sane_readahead()函數。

    ./kprobe ‘r:m max_sane_readahead $retval’

    Tracing kprobe m. Ctrl-C to end.
    java-8700 [000] 13445377.393895: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
    max_sane_readahead) arg1=200
    java-8723 [003] 13445377.396362: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
    max_sane_readahead) arg1=200
    java-8701 [001] 13445377.398216: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
    max_sane_readahead) arg1=200
    java-8738 [000] 13445377.399793: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
    max_sane_readahead) arg1=200
    java-8728 [000] 13445377.408529: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
    max_sane_readahead) arg1=200
    […]
    從結果可以看到此前的調整確實沒有生效。於是:

    ./kprobe -s p:file_ra_state_init

    Tracing kprobe m. Ctrl-C to end.
    kprobe-20331 [002] 13454836.914913: file_ra_state_init: (file_ra_state_init+0x0/0x30)
    kprobe-20331 [002] 13454836.914918:
    => vfs_open
    => nameidata_to_filp
    => do_last
    => path_openat
    => do_filp_open
    => do_sys_open
    => sys_open
    => system_call_fastpath
    kprobe-20332 [007] 13454836.915191: file_ra_state_init: (file_ra_state_init+0x0/0x30)
    kprobe-20332 [007] 13454836.915194:
    => vfs_open
    => nameidata_to_filp
    […]
    從結果看,作者需要重啓Cassandra來讓調整生效。

    ./kprobe ‘r:m max_sane_readahead $retval’

    Tracing kprobe m. Ctrl-C to end.
    java-11918 [007] 13445663.126999: m: (ondemand_readahead+0x3b/0x230 <- \
    max_sane_readahead) arg1=80
    java-11918 [007] 13445663.128329: m: (ondemand_readahead+0x3b/0x230 <- \
    max_sane_readahead) arg1=80
    java-11918 [007] 13445663.129795: m: (ondemand_readahead+0x3b/0x230 <- \
    max_sane_readahead) arg1=80
    java-11918 [007] 13445663.131164: m: (ondemand_readahead+0x3b/0x230 <- \
    max_sane_readahead) arg1=80
    […]
    成功!

Conclusion
上述所有工具都是藉助ftrace和相關功能來實現的。這些工具僅僅是ftrace的前端實現。讀者可以參考相關的文檔來了解ftrace的實現以及相關的信息。

作者目前十分希望eBPF能夠進入主線內核,這樣上述的很多工具都可以得到很大的簡化。

Error handling for I/O memory management units
2014 kernel summit有一個議題是討論如何處理IOMMU產生的錯誤信號。IOMMU的作用是在設備和CPU之間做內存地址的轉換,這樣外圍設備不僅可以看見簡化的的地址空間,也可以讓一些實際分散的緩存看起來連續,同時限制設備的地址範圍。雖然現在並不是所有的系統都有IOMMU,但是一個緩慢的趨勢是使更多的系統都包含。

David Woodhouse指出,在IOMMU上下文下,沒有一個標準的方法來反饋錯誤,IOMMU出錯時驅動無法簡單地得到通知。目前只有特定於具體架構的處理方案,PowerPC下有“extended error handling” (EEH),但是“只有Ben Herrenschmidt懂”,PCI子系統也有類似的錯誤處理機制。但是內核需要的是一套一致的處理方法來將錯誤從IOMMU反饋給驅動,而不管它們是怎麼連接到系統的。同時也需要有一個標準的機制來關閉出錯的設備從而防止大量的中斷搞垮整個系統。David提出了一個可能的方法,他參考並擴展了PCI的錯誤處理架構,不僅限於PCI同時增加了額外的功能,例如向驅動提供出錯信息和出錯地址。

Ben指出反饋一個錯誤的具體信息並不是那麼容易,對錯誤的處理經常設計到硬件,要隔離開出錯IOMMU後的整個設備,但是這樣就沒有辦法傳遞任何信息。驅動可以要求獲得錯誤通知,也可以嘗試恢復出錯設備,但是如果沒有驅動支持,默認的處理就是模擬設備的拔掉和重插事件。David指出,對於一些設備特別是圖像適配器而言,用戶並不希望出錯時停止整個設備,一條命令流可以出錯並被停止,但是其他並行的流應當可以繼續,因此需要一個更爲輕量的處理。

Josh Triplett詢問出錯時通常的反應是什麼,恢復路徑會做一些嘗試還是放棄並重置整個設備?對於多數設備而言,重置是一種足夠的處理,但是就像剛提到的,圖像設備有些不同,同樣網絡設備也需要一種更爲溫和的錯誤處理機制。但是David認爲在大部分情況下,整體隔離並重置設備是一個好的方法。

Andi Kleen問這種錯誤處理代碼如何被測試,在沒有全面測試的情況下,這些代碼很可能有問題。David說讓設備嘗試對一個錯誤地址進行DMA是比較容易做到的,而且也可以通過注入錯誤的方法。但是Ben指出即使有這些工具,EEH錯誤處理也依然可能頻繁出問題。
David問ARM是怎麼做的,Will Deacon說PCI之外並沒有真正的標準,他也沒見過ARM裏能很好的處理這些錯誤。他同時指出在hypervisor下這個問題會更復雜,一個IOMMU可能向guest提供受限的DMA訪問,向guest暴露潛在的IOMMU錯誤,guest可能會隔離出錯設備,從而讓host比較迷惑。

Arnd Bergmann認爲任何錯誤處理方案都不應只侷限於PCI設備,因爲在ARM里根本就沒有PCI總線。David說PCI現有的錯誤處理架構是一個很好的起點,可以把它做的更爲通用。雖然有些PCI特定的概念(如PCI設備)需要被保留,但是大部分都可以移至struct device結構並通用化。目前這個方法沒有收到反對的聲音,David會開始去實現。

Kernel performance regressions / 內核性能迴歸缺陷
內核性能迴歸缺陷差不多是折磨 Linux Kernel 的最讓人討厭的問題了。性能迴歸缺陷通常在人們不知情的情況下被引入,一段時間(可能長達數年)之後,一些用戶嘗試升級內核到新版本,結果卻發現上面跑的東西變得相當之慢。到那時候,最原始引入的那個性能迴歸缺陷可能會變得難以追溯。鑑於此,討論性能迴歸問題成爲了內核峯會的常駐話題,今年也不例外。和以往有所不同的是,在避免製造新的迴歸缺陷這件事上,內核社區做得遠比以前好多了。

Chris Mason 的開場白就提到,在他的公司(Facebook),Linux 無處不在,且跑得比 FreeBSD 快。Facebook 打算讓工作集駐留在內存中,這樣工作負載就受限於 CPU、內存和網絡。性能在當中是重要的一個考量點,所以公司制定維護了廣泛的用以衡量系統和應用性能的指標。

Facebook 大多數線上生產系統運行的是 3.10 穩定版內核,加上額外的約 75 個補丁(譯者:3.10! Facebook 對內核新技術的掌控力真是令人膜拜,對阿里線上系統有一定參考意義)。也有系統跑老內核,但是 Facebook 內核組在慢慢推動他們改用新內核,一個推動手段便是拒絕爲老內核修 BUG。

當 Facebook 第一次開始用 3.10 時,內核開發者們一如往常地擔心性能迴歸問題。最終發現這個版本的內核問題比預期少得多,當然還是冒出不少問題的。其中有一個 IPv6 協議棧性能下降 10% 的問題,不過在 Chris 去追蹤這個問題之前 upstream 就已經把這問題修了。除此之外,還有一些 CPU 頻率 governor 上的問題,CPU 會跑在不合適的低頻率上,製造不必要的延遲。所以 Facebook 當前使用 基於 ACPI 的 CPU 頻率 governor 策略(譯者:這個也可以給阿里線上系統提供參考),同時也在嘗試找出讓新的 intel_pstate 特性相關代碼正確工作的方式。還有一個問題是在新內核中更頻現的 futex bucket lock 爭鎖問題,在 Facebook 內部,Chris 已經通過把一些明顯的代碼移出臨界區的方式來解決這個問題。Rik van Riel 建議嘗試一下增加 buckets 數量對解決這個問題應該也有助益。

那麼,Chris 在 3.16 內核上嘗試跑 Facebook 的負載效果如何呢?他反饋說得到的數據相當樂觀。3.16 內核上跑的壓力得到了 2.5% 的 QPS 提升以及 5% 的延遲降低。不過整體負載跑完的時間延長了大概 4.5% 之多。當然這個是在 Chris 打了他修復 futex bucket lock 的補丁之後的結果,否則系統時間半數耗在了爭鎖上,整個系統幾乎不可用。

回到他們公司 3.10 的遷移,Chris 重複了他之前 session 裏的一個點:stable tree 裏的補丁引起的迴歸缺陷數爲0. (對於其他問題) 他們碰到過一些 OOM-killer 在殺基於 POSIX 線程的程序時把系統鎖死的情況,這個問題在 upstream 也已經被修復了。還有一個問題是,在一個文件上整合 direct I/O 和 buffered I/O 會造成數據污染,填0頁會留在 page cache 中。Chris 表示對現有測試沒有發現這個問題感到很驚訝,特別是這種問題居然沒有引起競爭條件。他打算去看看 xfstests 測試套件以發現爲何這個問題沒被捕捉到。

不過總體來說,他表示遷移到 3.10 是歷次內核遷移中最容易的一次。

主話題之外的發散環節,Arnd Bergmann 詢問了關於 Facebook 額外加的那約 75 個補丁的情況。Chris 回答說,一個顯著的點是其中有一些 patch 通過移動一些系統調用到 VDSO 區域來加速任務搶得一個線程的 CPU 的使用權。他說這批補丁很快應該會進 upstream,不過目前還要修點別的。另一個點是允許內存管理系統在發生缺頁時在內存映射區(memory-mapped region)避免產生0頁。他解釋了一下爲什麼這個補丁難以進 upstream 的原因。還有一個降低了由 /proc 接口導出 IPv6 路由表的數量。Facebook 整個內網都是用 IPv6 的,所以路由表很大。

回到性能問題,正在爲 SLES 12 發佈準備穩定化 3.12 內核的 Jan Kara 同意最近內核升級遷移變得容易了。他最大的顧慮是新內核中的一些爲優化負載開銷做出的行爲變化。只要這些變化沒讓你機器速度變慢,就不會是什麼糟糕的變化。但是他還是重點提出了幾個類似的例子,比如 CFQ I/O 調度模式,還有 NUMA 負載均衡相關的工作。

Andi Kleen 問 Chris 爲何他覺得內核遷移升級變得越來越方便了,不過畢竟內核進程是沒有慢下來的。James Bottomley 附和了這個問題,他疑惑我們好幾年沒有跟蹤迴歸缺陷而爲何缺陷數量會下降。看起來得從幾個方面來回答這個問題,不過關鍵因素很容易闡釋:相比以前現在有更多性能測試在進行,如果性能問題被引入了,在進入 stable kernel 之前就更可能被發現並修復了。

Chris 補充道,Red Hat 和 SuSE 最近都過了一遍他們企業發行版的穩定化週期,修 BUG 顯然有助於產品的穩定化。Mel Gorman 補充道,新硬件平臺已經引入一批硬件廠商來支持。他們致力於提速系統速度,但是所有人都從中受益了。儘管如此,他還是警告說,現在的好條件可能只是暫時,絕非一成不變。

最後,Chris 在結束語中總結道,3.10 是目前 Facebook 用過的最快的內核,或許那些長期受困於新內核引入新的性能迴歸缺陷問題的開發人員聽到這個消息能歡欣鼓舞吧。

Kernel self tests
2014 內核峯會上 Shuan Khan 在她的 session 開場白中說,她時不時會幫着做一些穩定內核發佈版本的測試過程,這類測試多數都是類似“編譯-構建-啓動”類別的測試,不過如果測得更全面徹底當然會更好。如果有一個簡單的健全測試(sanity test)集可供開發者運行,或許會有更多的迴歸缺陷還沒影響用戶便能被暴露出來。基於上述目的,她的工作是在內核構建系統中添加了一個新的 make 目標項,叫 “kselftest”。

現在這個功能有一個最小化的測試集,之後她將會豐富其中的測試項。她說:“我們在裏面已經加了許多測試代碼”,如果能多用善用這個測試功能是最好。不過她還是打算仔細決策哪些測試應該進 kselftest,因爲這個測試功能的目標是快速運行內核測試,這是一個基本的健全性測試,而非全面覆蓋的壓力測試。(譯者:爲什麼不直接用 LTP 的 sanity test set? 坑爹啊)

Ted Ts’o 問道,何爲“快速”,如何界定其範疇?Shuah 回答說,她不知道如何界定,當前的測試集跑完不會超過10分鐘,隨着測試增加,時間很可能會相應增加,但是這個時間不應該無限制增長到一個開發者都不願再跑的值。Mel Gorman (譯者:這哥們開發了一個 memtests 工具,還是挺好用的,不過包含了很多大型的壓力測試集) 指出,他自己的測試如果跑完整測試,大概要花個13天左右時間,這應該算是超出了“快速”的範疇了吧?(譯者:Mel 你是來賣萌麼,你跑個mm-tree花個13天時間看你還能好好幹活不) Paul McKenney 補充說,爲 read-copy-update 子系統做的 torture-test 測試套件,完整運行下來會超過 6 小時。聽了業內大家提供的例子之後,Shuah 認爲她能接受的目標差不多在 15 到 20 分鐘左右。(譯者:跑 LTP 呀跑 LTP 呀!爲啥峯會上沒有 LTP 的開發者去,讓我去呀!摔!)

Josh Triplett 表達了他對於內核樹自帶測試集的憂慮。如果測試代碼自己在變,當測試失敗的時候就挺難通過 bisect 來定位問題所在了,因爲不知道到底是測試代碼出問題,還是內核代碼出問題。他說或許不把測試代碼和內核代碼放一起會更好。不過 Shuah 說如果這麼做了,這就違背了她的初衷,即“快速”運行測試的目標(從別處拿測試代碼確實會更麻煩),而且很可能會因此運行這個測試的受衆羣體會減少。

Darren Hart 問這個測試集是否只關注功能測試,還是說性能測試也會包括在內。 Shuah 回答說,這沒有規定,如果一個測試跑得快速而有效,不管是什麼類型的測試都可以放進去。那驅動測試呢?這個可能會難一點,不過也許可以通過模擬真實硬件、BUG 場景和所有的外部環境來實現測試。

Grant Likely 說是否有一個標準化的輸出格式以便於生成統一的報告。由此又引發了一系列關於測試框架和測試工具的衍生討論。大家還建議與其大家討論一致選擇一個合適的框架,不如 Shuah 就從成熟框架裏挑一個。不過 Christoph Hellwig 指出 xfstests 測試套件也沒有一個標準框架,裏面的測試跑完之後只是生成一個和基準輸出不一致的 diff,這使得新測試能夠拋開測試框架和測試工具的限制,更自由地添加到測試套件中。Chris Mason 同意說這種策略纔是做事的“唯一可行之法”。

最後 Shuah 再次重複,她想要更多的測試能加入到 kselftest 裏來,並且歡迎大家獻計獻策如何把這個測試機制給運作起來。

Two sessions on review
如其他自由軟件項目一樣,Linux Kernel 也有一個很基本的問題:得不到足夠的 review. 有一些開發領域比其他領域更需要 review, 首當其衝則是用戶空間的二進制接口的創建,因爲這些接口必須得維護很長一段時間。不過這個問題遠非 ABI 定義這麼簡單。2014 內核峯會上有兩個 session 就 review 問題以及如何改進的話題發起了討論。

ABI 的變化
由 Michael Kerrisk 和 Andy Lutomirski 發起了第一個 session,他們主要擔憂的是 ABI 的問題。Michael 開場的時候就說,無論他什麼時候去測一個新的系統調用,他有一半時間都能發現 BUG。Christoph Hellwig 補充說“一半”這個數表示他測得還不夠深。由此引出的觀點便是:stable release 的代碼很明顯沒有經過足夠多的 review 和測試,事實上很多時候根本沒有經過一丁點兒測試。Michael 舉例說,recvmmsg() 這個系統調用在第一版裏,有一個 timeout 值,結果這個值其實設置得完全不合理。

有時候我們也去改改 ABI,比如說: inotify 接口,IN_ONESHOT 選項在早期內核中不會觸發 IN_IGNORED 選項,在新內核中這個行爲被改變了。

他說新的 ABI 沒有 spec 規範,是造成 ABI 難以 review 和 測試的一個事實。缺少規範還引起一些細微的代碼實現的問題。Michael 仍舊以 inotify 爲例,談論了跟蹤文件在目錄之間移動的問題時的困難,細節在這篇文章裏。大多數新的系統調用都沒有 man page 和足夠的 review 者,還引發了關於設計的質疑, Michael 說 O_TMPFILE 選項提供了一個很好的例子:且不論它的其他問題,至少這個選項從設計上來說,它的功能實現足以把它放到一個單獨的系統調用中。

Andy 補充說,spec 規範是個好事,不過對一個新的 ABI 做單元測試也是一個好事。從這點出發,Peter Zijlstra 問相比內核樹自己來說,Linux Test Project, LTP 是否用來做單元測試更合適。(譯者 & LTP 維護者:是的!absolutely!) 不過有人顧慮說 LTP 測的東西遠不止系統調用,還有的開發者嫌 LTP 整個測試工具不夠輕量,裝起來也麻煩。

Ted Ts’o 觀察到開發者手頭必須有他們開發的特性相對應的測試(代碼),要不然他們就不會那麼勤勉地去做測試。Dave Airlie 說這樣看來在內核樹裏放測試代碼是個好事。他又建議或許社區應該堅持新系統調用的准入制度裏必須得有 man page 這一條,否則不能進主線。Michael 迴應說以前這麼試過,不過沒成功。不過 3.17 加進去的四個新系統調用都有 man page. Ben Herrenschmidt 之處系統調用只是冰山一角。內核 ABI 還有其他方面,比如 ioctl() 調用,sysfs, netlink 以及其他。

之後有一些重複的話題,比如改了 kABI 的補丁必須得 cc linux-api 郵件列表一份,又或許 cc 給對應的郵件列表這事應該是對應子系統的維護者的職責。Josh Triplett 建議說 get_maintainer 腳本可以改一改以實現自動 cc 對應郵件列表的功能,不過這個觀點沒得到熱切的贊同,這個腳本可能會在發補丁郵件的時候加上很多不相關的收件人,內核開發者不太喜歡這個功能。

Peter Anvin 聲稱 linux-api 這個郵件列表不工作了,他說或許把 man page 合併到內核樹裏會更好,這樣代碼和文檔就可以一起發補丁。Michael 迴應說這個觀點以前提過 。這麼做好處壞處兼具,壞處就是 man page 裏的很多內容都不是描述內核接口的,它們是爲應用開發者準備的文檔,而不是內核開發者,所以 man page 裏有一堆 glibc 的接口,以及其他東西。

在一些重複的話題,比如 系統調用沒有 man page 不準進內核,改了 kABI 要發送補丁抄送 linux-api 列表等討論聲中,這個 session 結束了。開發者們都在努力改善現在的情況,只是目前來看還是沒什麼好的解決方案。

獲得更多的 review
James Bottomley 討論了一個更寬泛的話題,patch review. 他問道我們怎麼才能增加進 mainline 的代碼的 review 數量?有沒有什麼關於改進內核 review 流程的新鮮觀點?這個 session 沒有討論出確切答案,不過確實涵蓋了一些 review 操作機制上的內容。

Peter Zijlstra 說他一直收到不少帶有假的 Reviewed-by 標籤的補丁。他說這裏的“假標籤”是指代碼其實沒有經過深度 review,而有時候只是跟補丁作者同個公司的同事(有可能是隨意)打的 reviewed-by 標籤。(譯者:我記得我也幹過這事兒) James 說要是沒有靠譜的註釋和 reviewed-by 標籤一起,他會自動忽略郵件裏的這些 reviewed-by 標籤。

不過 Darren Hart 說,這些標籤可能是在補丁發出來之前已經經過內部 review 了,所以就不詳細列 review 說明了。至少在有些公司這類內部 review 是很嚴肅認真的,所以列出那些 reviewed-by 標籤還是一件靠譜的事情。Dave 反問爲什麼 review 的過程要內部進行,而不搬到社區來公開呢?(譯者:這個有點吹毛求疵了啊) Darren 回答說,差不多對於任何項目來說,面向公衆開放之前做小範圍檢查都是一件再自然不過的事情了。

James 補充說,他常常懷疑同個廠商的 review,不過它們當然不是說無效,只是該不該信任特定的 review 者這個大有關係。

他又問了一個泛泛的問題,一個補丁多大的改動值得讓人去 review 一次 reviewed-by 標籤的可靠性?一個空白格的變化當然不需要重新 review,不過一堆補丁做了一堆改動就有必要了。會場上關於怎麼劃分界限出現了一些不同意見,最後達成一致,這個界限由子系統的維護者來做主。

這個 session 的最後,Linus 大神冒泡說,Reviewed-by, Acked-by, 還有 Cc 標籤其實都是一個意思:如果這個補丁出問題了,後續的報告中應該把標籤裏的那個名字抄上。有些開發者用一類標籤,其他的人用其他的標籤,不過它們本質上沒什麼區別。在一些反對 Linus 大神的這個觀點的討論聲中,這個 session 結束了,也沒有人就如何讓內核代碼得到更多 review 這一問題提出新的觀點。

One year of Coverity work
去年 Dave Jones 在參與一個名爲 Coverity scanner 的項目,旨在發現並修復潛在的內核 BUG。和許多其他開發者類似,他也擔憂隨着時間推移,BUG 問題越來越糟糕,隨着項目代碼循序漸進全部進入內核之後,缺陷必然隨之而來。不過最後發現實際情況比想象的要好一丁點兒。

Dave 提供給 Coverity 的是一個“廚房水槽式構建”的東西,幾乎把所有的選項都打開了。這導致最後他編出來的內核有 6955 個選項之多,在這個內核跑整個掃描程序花了好幾個小時。他讓 Coverity 持續跑着,最後公司給他提供了專用的服務器讓他得以一天能跑上兩三次掃描。

Dave 掃了一遍 3.11 內核,他總結出一個“缺陷密度”值,即每千行代碼裏的缺陷數量。3.11 內核的缺陷密度是 0.68 —— 略高於公司的“開源平均值” 0.59。各內核版本的缺陷密度值如下:

內核版本 缺陷密度
3.11 0.68
3.12 0.62
3.13 0.59
3.14 0.55
3.15 0.55
3.16 0.53
當前在 3.17 合併窗口關閉之後的缺陷密度是 0.52。所以情況隨着時間推移其實在持續變好。

他總結了一個內核各領域的缺陷排名, 在排名頂端的是 staging 樹,他說這個情況是好的。如果其他子系統比 staging 樹還糟糕,那表示這個子系統真的有問題了。其次的條目是驅動樹,這一點毫不意外,因爲它的代碼量最大。

他說,用了 Coverity 之後暴露出來的最大問題,是死代碼。有時候程序裏的告警信息其實是有用的,不過並非總是正確。比如說,有些代碼在配置選項不同時可能會路徑不可達。列表裏排名第二的是檢查返回值的失敗情況,其中有相當大一部分並非真正的 BUG,而是分支環境的不同所致。排名第三的條目是指針被去引用(dereferenced)之後檢查出來的空指針情況,顯然這是一個糟糕的消息,需要被修復。

同樣可怕的問題是靜態緩衝區溢出錯誤。這個問題會變得很危險,儘管情況在逐步改善,但是還是存有很多這樣的問題。它們也並不總是 BUG,舉個例子,網絡層會在 skb 結構體裏玩這樣的小把戲使得緩衝區看起來溢出了,但是事實上沒有溢出。此外 Coverity 還標註了一大堆資源泄露,這也同樣不奇怪,它們是經常發生的錯誤。

有大量其他類型的潛在錯誤,比如“無效聲明”,往往是無害並且是故意這麼做的。比如:變量給自己賦值沒什麼效果,不過這可以達到屏蔽過去的編譯器的“possibly uninitialized”告警信息的效果。其他的,比如使用用戶控件未經檢查的數據,可能會更嚴重,這個例子中,功能檢查背後往往潛伏着非法使用的情況,並且不容易馬上發現。

Dave 說,好消息是現在內核中只有不到50個 “use-after-free” 錯誤了。另外一些其他的“啞巴”錯誤也幾乎從內核中消滅殆盡了。Dave 說他一直在關注那些錯誤,一旦有新的錯誤冒出來,他會去嘗試快速修復。

Ted Ts’o 問 Coverity 標出來的問題中有多少是真實的 BUG, Dave 的感覺是隻有一小部分是嚴重的 BUG。他說如果有人對安全問題感興趣,可以跑 Trinity 測試,它能比 Coverity 發現更多的問題。

那麼 ARM 的覆蓋率呢?商業版的 Coverity 產品有這個功能,不過免費的開源社區版本沒有。Dave 說如果一段代碼能在 x86 編譯器上編譯出來,Coverity 就會去掃描,所以他在考慮類似把 ARM 樹中的所有內聯彙編代碼給註釋掉然後讓 Coverity 在上面運行的做法。不過這應該是未來的一個項目了。

如果其他開發者想要幫忙修復 Coverity 報出來的問題,可以看一眼掃描結果。方法是先要登陸 [5] 然後註冊 “Linux” 項目,然後跟 Dave 打個招呼,他會授權查看結果。羣策羣力,更多人關注掃描結果從長期來看對內核肯定大有益處。

Axiomatic validation of memory barriers and atomic instructions
相對於內存屏障來說,原子操作的必要性和正確性相對更容易理解和使用。對於多數內核代碼來說,一般不需要直接接觸這麼底層的機制(都包裝好了),但如果真是要徒手決鬥的話,內核也提供了些武林祕笈—— Documentation/{atomic_ops.txt, memory_barries.txt}。 除了祕笈,本文還透露了兩個大殺器。

ppcmem/armmem
這兩個工具LWN在2011年就介紹過了,它對於內核代碼的小型臨界區的併發行爲驗證很有幫助。舉個例子,註釋內嵌了:

PPC IRIW.litmus # 表示這是一個PPC平臺上的測試,測試名字叫作IRIW.litmus
“” # 測試的別名,一般是空的
(* Traditional IRIW. *) # 測試的註釋
{
0:r1=1; 0:r2=x; # 處理器0,寄存器r0初始化爲1, 寄存器r2 初始化爲變量x的地址。
1:r1=1; 1:r4=y; # 以上類推
2:r2=x; 2:r4=y;
3:r2=x; 3:r4=y;
}
# 變量x和y的值,默認初始化爲0

P0 | P1 | P2 | P3 ; # 表示有4個進程,分別名爲P0, P1, P2, P3
stw r1,0(r2) | stw r1,0(r4) | lwz r3,0(r2) | lwz r3,0(r4) ; # P0執行 r1 = (&x), P1執行:r1=(&y), P2執行:r3 = (&x), P3執行:r3=(&y)
| | sync | sync ; # P2和P3執行 memory barrier
| | lwz r5,0(r4) | lwz r5,0(r2) ; # P2執行:r5 = (&y) , P3執行r5 = (&x)
exists # 表示後面一行是一個assert表達式。
(2:r3=1 /\ 2:r5=0 /\ 3:r3=1 /\ 3:r5=0) # 即表示P2和P3讀到了不一致的x和y的值。
使用以上輸入運行“完全狀態空間測試工具”ppcmem,結果會顯示以上斷言是不成立的。事實上,這個工具對調試內核非常有用,它在過去的幾年裏也的確協助解決了幾個內核問題。以上測試需要花14CPU小時和10GB內存,與手工分析需要的以月或者周計的時間相比,這已經是巨大進步了,但仍然有兩個牛烘烘的內核黑客連等這N個小時的耐心也木有。

herd
ppmmem的兩個作者Jade Alglave、 Luc Maranget和Michael Tautschnig最近發了篇論文:http://diy.inria.fr/herd/herding-cats-color.pdf

他們修改了ppcmem/armmem,原設計會搜索全部可能的狀態空間,現在則使用基於事件的公理方法(an axiomatic, event-based approach)。這種方法通過承認偏序關係避免了對總體上有序的大量等價關係的搜索,從而極大降低了算法的時間複雜度。held會構造出候選的執行流組合,再根據底層內存模型去掉其中不合理的組合。

如果使用held執行上面的測試,也可以得到相同的結果但只花了16毫秒,大約是3,000,000x的提升。雖然這個簡單測試的結果不具代表性,但論文給出的結果也是非常樂觀的,一般在45,000x左右。

如果將上面sync指令換成輕量級內存屏障指令lwsync,再運行一下測試,就會得到發現斷言中的條件發生了!

Test IRIW Allowed
States 16
2:r3=0; 2:r5=0; 3:r3=0; 3:r5=0;
2:r3=0; 2:r5=0; 3:r3=0; 3:r5=1;
2:r3=0; 2:r5=0; 3:r3=1; 3:r5=0;
2:r3=0; 2:r5=0; 3:r3=1; 3:r5=1;
2:r3=0; 2:r5=1; 3:r3=0; 3:r5=0;
2:r3=0; 2:r5=1; 3:r3=0; 3:r5=1;
2:r3=0; 2:r5=1; 3:r3=1; 3:r5=0;
2:r3=0; 2:r5=1; 3:r3=1; 3:r5=1;
2:r3=1; 2:r5=0; 3:r3=0; 3:r5=0;
2:r3=1; 2:r5=0; 3:r3=0; 3:r5=1;
2:r3=1; 2:r5=0; 3:r3=1; 3:r5=0;
2:r3=1; 2:r5=0; 3:r3=1; 3:r5=1;
2:r3=1; 2:r5=1; 3:r3=0; 3:r5=0;
2:r3=1; 2:r5=1; 3:r3=0; 3:r5=1;
2:r3=1; 2:r5=1; 3:r3=1; 3:r5=0;
2:r3=1; 2:r5=1; 3:r3=1; 3:r5=1;
Ok
Witnesses
Positive: 1 Negative: 15
Condition exists (2:r3=1 /\ 2:r5=0 /\ 3:r3=1 /\ 3:r5=0)
Observation IRIW Sometimes 1 15 # 注意這行,“Sometimes”發生吶!
Hash=a886ed63a2b5bddbf5ddc43195a85db7
你一定在想,如果我能知道是什麼原因就好了,好的,herd可以再幫你一把,它可以生成個狀態圖:

fr: From read, 即WAR(即write after read for same address)。
GHB: Global happens before,基於內存屏障指令或者數據流依賴的效果。
po: Program order.
propbase: 基於寫傳播關係(store-propagation relationship)
rf: Read from, 即RAW。
更爽的是,held工具是支持x86的呢!!好的,我們跳過另一個例子和對held的scalability的測試的討論,該是澆涼水的時候了。

Limitations and summary
限制

無論是ARM、PPC或者Intel都沒有官方宣稱認可held/ppcmem/armmem結果的正確性。畢竟這些工具都還在開發中!
然而,至少曾經有過一例硬件與這些工具的運行結果不一致的現象,後來被確認是硬件bug,畢竟處理器也都還在開發中!
ppcmem/armmem和held的結果有些也不完全一致。雖然論文中證明兩者應該是等價的,但還是可以構造出讓兩者不一致的反例,而且是held更加保守的結果。
held的時間複雜度仍然是指數級,雖然已經比ppcmem/armmem快了許多。
這些工具處理複雜數據結構時都還不夠方便,只能通過一些簡單指令模擬。
這些工具不能處理memory mapped-IO和設備寄存器。
形式方法是不能完全替代測試嘀,正如Donald Knuth所言,“Beware of bugs in the above code; I have only proved it correct, not tried it.”
The power-aware scheduling miniconference
By Jonathan Corbet August 27, 2014

Kernel Summit 2014的一個miniconference討論了power-aware scheduling。雖然這個問題離最終完美解決方案出爐還有相當長的路要走,但至少目前的進展都在朝這個方向努力。

2013年的會議提出需要一組標準和benchmark,用於評估提交的patch。今年Linaro開發的兩個工具已經可以使用。一個是用於運行特定的調度算法,同時觀察結果。目前有兩個可用的負載,Android系統上的音樂播放和一個web瀏覽器負載。

另外一個工具是”idlestat”。這個工具運行的數據來源於ftrace抓取的運行系統上進入sleep狀態和在sleep狀態持續時間信息,通過給定一個power模型描述處理器各個狀態下的能耗情況,該工具可以評估出這次運行的總能耗情況。

這些工具是一個好的開始,但也僅僅是一個開始,Morten這樣說到。現在的工作僅僅侷限在CPU能耗,其他的如gpu及外部設備的能耗目前看都是非常難解決的問題。

內核添加的load tracking對調度很有用,power-aware scheduling也需要對CPU利用率進行追蹤,以讓調度器更好的評估每個process將需要多少CPU時間,調度器依據此做出更加好的調度決策。Load tracking目前並沒有考慮CPU頻率變化的情況,這是個需要fix的問題。下一步的目標是開始讓調度器自己控制CPU頻率的變化,而不是對CPU頻率調節器的動作作出相應反應。

節能調度之前已經有一些簡單的技術(比如small-task packing),但只是在某些特定的場景下有意義,並不通用。一個可行方案是採用啓發式算法,這是目前最可能的一種比較完備的解決方案,但是這個實現會很痛苦。

供選擇的一個方式是給scheduler一個CPU平臺模型,對任何給定配置的處理器,這個模型能夠評估能耗將會是多少。這樣調度器就能夠來回調整處理器,同時評估能耗情況。平臺模型必須由architecture-specific代碼提供,基於處理器空閒和睡眠狀態實現。這塊已經有一組patch,目前看沒有很大的反對意見。

未來的任務之一,是使調度器感知CPU空閒狀態,如頻率調整。另外一個任務是,虛擬化情況下的能耗管理。Guest系統也會希望能夠運行在節能狀態,但這個工作主要是在host裏面,Guest可以將這種需求傳給hypervisor,而是否響應取決於host方面。

在Morten報告的末尾,一位開發者問power-aware scheduling是否會考慮thermal aware。Morten表示這個不會在這會做,power model現在需要保持儘量簡單,當前這塊的複雜度已經夠開發者處理。當這個簡單問題的解決方式已經可以預見了,大家纔會開始考慮其他的比如溫度管理。

A report from the networking miniconference
2014 內核峯會的第二天,包含一個網絡子系統開發者的小型會議,作者沒能參加,但是確實聽了 Dave Miller 的關於相關 topic 的簡單總結。下面的報告不可能很完整了,速記很難的,但是,幸運的是,它 cover 到了一些關鍵點。

Dave 快速總結了一些 topic, 其中一個是 Stream Control Transmission Protocol (SCTP),大體上,他是這樣說的:網絡層有很多高度抽象的代碼被 share 在不同的協議實現上, 但是對 SCTP 來說很難 share, 由於 associations,這導致了大量的重複的代碼存在於 SCTP 子系統,現在看起來有新的辦法來 rework SCTP 實現,並從很大意義上把代碼和網絡子系統統一。

網絡子系統代碼中,一個長期存在還沒達到最優的地方是啓動的時候和協議(比如:TCP)相關的大哈希表,這些表佔用了很多內存,其實沒有必要那麼大,但是還沒有辦法知道系統啓動的時候,這些表到底適當的大小是多大。現在,網絡層,有在 RCU 保護下的可變大小的哈希表了,這些表可以根據需要重新分配,因此,在整個的系統生命週期中,不再有必要保持那些大表了。

Dave 表示,extended Berkeley Packet Filter (eBPF) 相關的工作, 仍然有些爭議,最大的問題,eBPF 開發者 Alexei Starovoitov 有很大熱情,但是 reviewers 沒能跟的上節奏。因此 Dave 說,他會開始把那些 patch 打回去,讓 Alexei 慢點。

有一些 concerns 對於給 eBPF 增加訪問一些通用的指針的能力,另外,對於可能給 eBPF 虛擬機增加 backward branches 預測也讓一些嗯擔憂。沒有人不同意 Alexei 的主要目的:在內核中創建一個通用的虛擬機[譯:給別的 module 也用上,後面提到 nftables]。但是比較重要的是不能失去 eBPF 提供的執行保護環境;讓 eBPF 成爲 kernel 裏面的安全漏洞可不是什麼好事。因此很有必要有一些更加嚴格的指針訪問的規則,做很很多檢查,Dave 說。

Ted Ts’o 建議 SystemTap 開發者應該看看 eBPF,因爲 eBPF 可能在創建特殊目的的模塊上面提供了一個更好的參考。但是 James Bottomley 迴應說,SystemTap 需要更加通用的引擎,因爲它要訪問內核中跟多的地方,而 eBPF 不會顯式這麼做。

Dave 然後介紹了 Pablo Neira Ayuso 在最近法國舉辦的 Netfilter workshop 的報告,已經有很多工作放在刪除連接跟蹤代碼裏面的中心鎖這,讓代碼更加有效率。當 traffic 由很多小包組成的時候,在網口硬件全速工作的情況下,找出協議棧目前在哪脫了後腿的工作目前看應該也已經在進行中了。

對於英特爾的 Data Plane Development Kit (DPDK),也有一些興趣,這是一種把包直接推送到用戶空間的機制。一些 benchmark 的數據不錯,但是在內核中也有一些相似的方法來獲得相似的性能,Dave 說,他提到了 receive polling ,性能也不錯,而且也可以讓 network stack 全面工作。

對 nftables 也有一些討論,這個內核虛擬機試圖最終取代 iptables, 但是在 iptables 的兼容性方面有很多工作要做,需要讓網絡管理員儘可能不修改上層接口代碼或者腳本。nftables 取代 iptables 不會是一個很快的過程,在內核接口層上面,這兩者不兼容,在 workshop 裏面也有說用 eBPF 的 vm 來代替 nftables 虛擬機,有一個主要問題:nftables 允許部分取代防火牆規則,而 eBPF 目前不會。

之後,Dave 提到 encapsulation offloading, 無論什麼時候加密數據包,然後通過其他的傳輸協議隧道出去,你必須的考慮在哪裏做 checksum ,流分發是怎樣管理的。這裏有個很大問題,udp 加密無處不在,因爲網卡可以很容易的 checksum udp 包,但是流操作卻不容易,網絡開發者想要避免加密流的深度的包檢查;最後,他們用了一個 trick 的辦法,使用源端口號來標示並且操作流。其他的 tricks 在各個層中管理 checksumming, 叫做 “remote checksum offload”, 用接受端的內部 checksumming, 限制出包 checksumming。 [譯:聽起來蠻 tricky, 這裏缺乏細節,讀者自己 google 看]

一個普遍的對協議棧的興趣點是打包發送。網卡驅動設計是每次只發送一個包,而不知道後面是否有大量的包來,而概率上,常常是會的。如果驅動知道,大量包要來,它就會延遲發送,很大程度減少傳輸消耗,這個計劃是去增加一個 “transmit flush” 操作,如果驅動提供那個功能,再接收到一個包要傳送的時候,就不會立刻啓動硬件發包,而是延遲到直到 flush 操作調用。也有些 concern, 如:延遲發送會讓硬件 wire 變 idle, 不過也是可以解決的。

無線網絡
很多被討論的話題中,有一個是關於在 AC (access point) 上面做 arp proxying, 來節能。這樣 arp request 可以被 AC 直接回復,不用到目的端系統。這個已經被同意在網橋代碼中做,網橋本來就是幹這個的。

一個比較大的問題是 network function offloading, 網橋的硬件可以直接管理轉發而不用 cpu 的介入。這個是很好的功能,但是有一個問題:這些都只能被驅動或者用戶態(vendor-specific) 的二進制代碼所管理,這樣 OpenWRT 可能會瘋掉。一些工作已經被做了來 給 netlink 增加擴展接口,來讓 vendor follow 這些 generic 的工具和接口來開發,一個 qemu-based 的設備正在被開發來做測試。

Wireless 的 maintainer John Linville 站起來討論了一些無線峯會中的問題,無線開發者正在面對的一個問題是 Android 仍然在用 “wireless extensions” ABI,這個已經很多年就被不建議使用了。看起來對無線開發者來說很容易增加 vendor-specific 的操作擴展,因此,vendors 目前正在這麼做。從迴應上來說,無線開發者已經增加了很多選項來讓現有接口更加彈性,但是這個工作還沒有完全傳達到 vendor 那裏。現在的計劃是讓 google 鼓勵 vendors 不要使用 wireless extensions。

已經有一些工作來把 firmware dump tool 放在合適的位置,經過討論之後,開發者的想法是用 sysfs 來獲得相關的數據。

最後,John 表示,當無線的 maintainer 他有點累了,但是他還沒有找到更好的 candidate。在無線協議棧,有很多有天賦的開發者,但是大多數都是爲硬件 vendor 工作的。而這些硬件 vendor 不太熱衷於讓這些開發者爲其他 vendor 的硬件開發驅動,因此,一個新的無線 maintainer 幾乎確切的應該是硬件中立的組織,例如:一個 distributor。如果這有任何合適的人,John 也願意聽聽。

會議的這部分也 cover 了很多其他的 topic, 例如:藍牙 maintainer Marcel Holtmann 給了一個高速藍牙的升級,3.17 kernel 會包含藍牙 4.1 的功能。

結論就是,網絡協議棧的工作還有很多,並正在繼續着。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章