307
技術社區[雲棲]
內核月報2014年2月
32位ABI又暴高危漏洞-專家建議盡快更新
現在使用Linux內核的32位ABI的人已經不多了,事實上隻有少數幾個發行版沒有在編譯時就把它們直接關掉,而Ubuntu不幸就是其中之一。本月內核安全郵件列表security@kernel.org報告了最新發現的32位ABI安全漏洞,安全專家提醒受影響用戶盡快升級。
這個漏洞的起源要從Linux內核的X32模式說起。如果在編譯內核時打開CONFIG_X86_X32開關,然後給gcc使用-mx32開關去編譯程序。Linux內核就允許這個程序使用X32兼容模式運行,在這種模式下用戶既可以繼續享受64位x86帶來的更多寄存器等便利,又可以使用32位指針來減少內存開銷---當然,這時您可以索引的地址空間也隻有4GB了。為了配合這種模式下運行的程序,內核提供的基礎設施之一就是內核64位ABI的兼容層。2012年2月份,Lu Hong Jiu 向這個兼容層提交了一個補丁,用來給係統調用recvmsg提供這種32位的兼容接口。這次他犯了個小錯誤--對於用戶傳進來的最後一個timespec指針,沒有檢查它否是合法的用戶態指針(比如檢查它是否指向內核地址空間?)就直接向下送進了內部的操作流程。而在內核跑完recvmsg返回時,要向這個指針指向的timespec寫入剩餘的時間,因此攻擊者就可以利用它去操縱內核地址空間內的數據。這裏有兩個例子:https://pastebin.com/DH3Lbg54 和 https://github.com/saelo/cve-2014-0038
更慘的是,這個CVE Bug是在Ubuntu 13.04停止官方支持兩天之後被發現的,因此這個版本的Ubuntu用戶不會收到任何更新,自力更生的辦法是自己裝上這個模塊,由於代碼中用戶傳來的指針都加上了__user做修飾,這種濫用本來是可以被代碼靜態檢查工具發現的,事實上它確實被內核自帶的靜態檢查工具Sparse發現了 -- 夾雜在針對這個文件的上百條警告之中。警告太多等於沒有警告,內核開發者們說不定又要對此有所行動了。
ARM, SBSA, UEFI, and ACPI
在數據中心裏使用基於ARM的服務器已經吵吵了很多年了。爭論的焦點集中在:基於ARM的係統過於豐富,不同機器間的差異很大,這無疑會使在數據中心中大規模應用ARM服務器變得很苦逼。ARM Ltd. 對此心知肚明,所以才搞出了一套“SBSA(Server Base System Architecture)”標準。SBSA的中文可以翻譯為:麵向的服務器的係統體係結構。目前對SBSA的評論普遍比較樂觀,但在開發者社區裏還有一些不同的看法。
事實上,拿到SBSA的授權很難。該標準的許可證非常嚴格,讀過它的人不多。Arnd Bergmann這樣描述此標準:
“SBSA描述了兼容服務器上的硬件部件的細節。如:CPU,PCIe,定時器,IOMMU,UART,watchdog,中斷等等。它對細節描述的很充分,根據這些描述,我們能夠在服務器上引導兼容操作係統,並且自己加載非標準硬件的驅動程序。”
Arnd說SBSA製定的內容非常合理。Arm-soc內核樹的維護者Olof Johansson也支持這種觀點:
“這是一份及其重要的文檔,它改變了ARM廠商各自為戰的現狀。它使得軟件開發更加容易,至少不必再擔心硬件太多而且容易更新換代。”
簡單地說,SBSA正在打造一個基礎平台,該平台可以作為所有兼容係統的一部分。在ARM的世界裏,一直缺少這樣一個平台。這正是過去難以支持ARM的主要原因。在SBSA的支持下,內核開發者,硬件廠商,服務器管理員都會好過很多。
社區中對SBSA的討論也很激烈。最集中的討論是:作為平台控製核心的Firmware沒有在SBSA中得到描述。而是由一份獨立的標準進行了描述。該標準的細節無從得知,但能夠確定兼容平台應會同時使用UEFI和ACPI。
UEFI在大多數PC係統中得到廣泛應用。UEFI工作的不錯,它有一個開源版本的參考實現。內核也可以很容易支持UEFI。因此,在ARM係統上使用UEFI沒有什麼反對聲音。
ACPI就是另一回事了。在X86係統上調穩ACPI本就是一個苦逼的過程。現在把它用到ARM係統上,會更加苦逼。大多數ARM片上係統的廠商在ACPI上的經驗接近於0,他們在使用ACPI的道路上會犯很多錯。而內核呢,必須逐個的去cover這些錯誤。
也有很多人在關心“基於ACPI的ARM平台會表現如何。在PC領域,對平台的測試通常就是“Windows能不能跑起來?”,一旦windows能工作,Firmware的開發者就會認為一切OK。這就要求Linux內核的開發者小心處理好那些windows能夠做到的事。
在ARM服務器市場上,Windows肯定不再具有統治地位。這些係統上可能根本就不考慮安裝windows。取而代之的是“Red Hat Enterprise Linux”。屆時,各廠商都會在內核中提交N多補丁以支持他們的ACPI實現。最終可能會導致內核的兼容性下降,Bug泛濫,之後在花費數年的時間取解決它們。
ACPI真的需要嗎?ACPI是操作係統探測與初始化硬件設備的標準方法。但ACPI的批評者指出,ARM體係結構已經具有這個機製了。該機製就是“設備樹”。設備樹已經相當成熟了。就像Olof在最近給Linux的合並請求裏說到:
“新板卡,新設備在係統中就是一個新的設備樹文件。新設備的加入不需相應的C代碼做出任何改變,這意味著設備樹係統成熟了”。
已經開始這項工作的開發者們也不禁問道:為什麼非要采用一項在ARM中從沒用過的PC技術?一些開發者還認為,即使工作完成了,也很難被內核接納。
Linux內核的開發總是傾向於那些被廣泛使用的係統。而ACPI在ARM上的應用有限。Grant可能會把它放在一個長期的Firmware標準上,因為ACPI可以使硬件廠商在保持兼容性上較為容易:
“他們已經有了針對ACPI標準的硬件。ACPI中整合了平台管理工具,他們希望在X86與ARM產品上使用相同的技術。他們也盡力確保操作係統能夠直接在這些硬件上啟動,而無需在內核裏打補丁。使用ACPI使得他們能有度地控製平台的底層細節,從而可以抽象出平台間的差異。”
正如Grant指出的,ACPI與ARM係統的傳統操作方式相違背。ACPI把那些ARM平台上需要做的工作(針腳配置,時鍾編程等)統統放入了Firmware中,而不再有內核開發者控製。
無論如何,基於ACPI的ARM服務器還是很快會出現,內核也必須要支持它們。內核應該也會同時支持設備樹係統。ACPI很快就是走入嵌入式領域。不久後,ARM上的ACPI會是內核支持的配置方法之一。那時,人們也許會奇怪:為什麼當初對這事爭論了那麼久?但是,這個“不久”也可能會超出很多人的期待。
Controlling device power management
內核社區一直在為高效的電源管理而努力,特別是隨著手持智能設備的普及,內核電源管理的相關開發成了一個熱點。
電源管理中一個特別難處理的問題就是,保守設置管理可能效果不好,但是過於主動的設置又會引起問題,比如讓CPU深度睡眠的時候設備正在進行DMA操作,CPU剛躺下去就要被喚醒,徒增了延遲。為了避免類似的情況,內核之前引入了電源管理質量控製機製(PM_QOS)。通過這個機製,內核的設備驅動可以對電源管理子係統描述自己在延遲方麵的需求,避免不必要的睡眠。
在3.2內核開發周期中,這方麵的工作又向前走了一步,精細化到可以滿足每一個設備的不同需求。驅動可以設置一個值(DEV_PM_QOS_LATENCY),告訴電源管理模塊它最大能容忍某個單元的延遲。這樣這個單元可以根據這個值選擇自己的睡眠方式而不影響到性能。
隨著外設的發展,越來越多的外設有了自己內部的電源管理機製。這些機製通常是通過監控讀寫模式來選擇設備的狀態。比如說內存控製器發現某個BANK沒有什麼讀寫的時候,會自動把這個BANK放到低電量工作狀態。這些機製簡化了內核方麵在電源管理的工作。比如磁盤可以把自己置於不轉的狀態,相機可以自己選擇處於關閉狀態。顯然,當外設自主選擇隨眠時,也有不恰當的時候。同前麵的設備需要通知內核電源管理模塊一樣,現在需要一種機製,反過來讓內核可以通知外設,告訴它自己在延遲方麵的需求,讓外設滿足滿足這個值而避免不恰當的睡眠。比如說,CPU在某個時候告訴一個外設,我有一個請求,你最多隻有10納秒的時間來處理。
目前內核還缺少這麼一個機製,通知內核對於外設在延遲方麵的需求。這個情況最早會在3.15開發中得到好轉。Rafael Wysock同學搞了一套補丁,通過利用現存的PM_OQS機製,實現了內核到外設的延遲通知機製。有支持這種機製的外設,可以通過實現一個回調函數
void (*set_latency_tolerance)(struct device *dev, s32 tolerance);
來滿足這個需求。同時會在sysfs下暴露一個可調的屬性pm_qos_latency_tolerance_us,代表能忍受這個設備的最大延遲。當然大多數時候寫驅動的人不必關係這個特性,因為在設備的電源管理很多在bus這一層就做了。對於這個值的調節,大多數用戶程序也是不會關心的。要是設備沒有正常工作,倒是可以試試通過調節這個值(如果支持的話)來讓它正常起來。
Flags as a system call API design pattern
最近Miklos Szeredi給內核社區實現了一個新的係統調用renameat2(),這種係統調用名字後麵加1加2的做法很惡心,同時也再次提醒我們在設計內核態和用戶態交互的API時一定要考慮清楚,同樣的杯具在Linux內核開發過程中多次出現,而Unix也同樣不能幸免。
rename()在2.6.16的時候擴展成了renameat()(同時添加的還有其他12個係統調用),而這次新的renameat2()又是renameat()的擴展,這三個家夥都隻做一件事情:在同一個文件係統上給一個文件改名。rename()隻有兩個參數,舊文件名和新文件名;renameat()給他們倆一人增加了一個輔助的參數,用來指明相對的目錄:如果這兩個輔助目錄有效,那麼新舊文件名可以是一個相對於這兩個目錄的,具體請參見man(2) renameat。
renameat2()給renameat增加了一種新功能:原子的交換兩個文件名。盡管這兩個係統調用有關聯,但是我們還是得定義一個新的,因為renameat()竟然沒有一個flag用來表示不同的功能,這個太杯具了,瞧瞧人家clone(),open()想得多周到。沒辦法,renameat2增加了一個新的flag,並使用了一個bit RENAME_EXCHANGE,希望renameat3()這樣的杯具以後就別發生了。事實證明這個太讚了,很快Andy Lutomirski真的發現我們又需要了一個bit,RENAME_NOREPLACE,嗯,還好,還好。
杯具再次重演
還記得前麵提到添加renameat()的2.6.16麼?我們痛苦得發現和它一同進入內核的係統調用中竟然還有7個也沒有任何的flag,讓我們記住它們的名字: faccessat(), fchmodat(), futimesat(), mkdirat(), mknodat(), readlinkat(), renameat(), symlinkat()。原文後麵分析了這些係統調用是多麼需要一個flag,我就解釋了,嗯,社區人太多,難免有不同的人犯同樣的錯誤。不過本段結尾的時候話鋒一轉,以wait()為例說明Unix也是這個熊樣,到底是不讓Unix占一點便宜呀。
結論
設計新的係統調用的時候一定要想一下我們是否需要一個flag以備不時之需呢? 3.2裏增加了兩個新的係統調用:process_vm_readv()和process_vm_writev(),並在參數裏麵添加了一個目前沒用的flag,從曆史來看應該是一個非常明智的選擇。
Proper handling of unknown flags in system calls
上文提到設計係統調用的時候盡量要增加一個flag以保證後麵的擴展,另外還有一個非常重要的事情:內核必須處理好那些不支持的flag,否則以後的兼容性會成為很大的問題,不過Linux/Unix係統調用的發展曆史證明這個事情似乎沒幾個人記得住。
一個正確的帶flag的係統調用應該始終在函數開始做如下檢查:
if (flags & ~(FL_XXX | FL_YYY))
return -EINVAL;
FL_XXX和FL_YYY是該係統調用能夠解釋的flag,如果調用者使用其他的flag,則直接返回-EINVAL。如果將來我們增加一個新的flag FL_ZZZ,那麼直接將上麵的檢查修改為
if (flags & ~(FL_XXX | FL_YYY | FL_ZZZ))
return -EINVAL;
這樣用戶層的程序就能夠通過係統調用的返回值來判斷當前內核到底支持哪些flags。聽起來挺不錯是麼?可惜,內核裏麵有很多係統調用並沒有做這樣的檢查,比如clock_nanosleep(), clone(), epoll_ctl(), fcntl(F_SETFL), mmap(), msgrcv(), msgsnd(), open(), recv(), send(), sigaction(), splice(), unshare(), 名單還有很長... 這些係統調用並沒有一個清晰的方式來判定flag是否合法,所以調用的用戶就非常痛苦,到這裏似乎和內核開發者沒啥關係,不過別著急,下麵的例子證明他們的日子也不好過。
既然用戶沒有辦法判斷方便的判斷flags裏麵哪些bit是非法的,他們就有可能隨意的傳遞一些"沒用的"flag,這樣當內核開發者想使用其中的一些bit的時候,為了不使用戶態程序受到影響,他就必須寫一些不那麼優雅的實現。一個最近的例子就是EPOLLWAKEUP,為了避免錯誤,用戶必須擁有CAP_BLOCK_SUSPEND權限,似乎有點多此一舉。同樣的問題出現在O_TMPFILE,用戶為了使用O_TMPFILE還得加上O_DIRECTORY的bit才能工作。
結論
有同學可能會說把這些係統調用的處理裏麵都加上對flags的檢測不就好了麼?實際上並沒有那麼簡單,由於已經有大量的用戶態程序存在,增加這些check很有可能會導致一部分用戶的程序突然無法使用,這個是內核社區的大忌,是無論如何不能接受的。到這裏似乎最好的辦法隻有一個了,在給一個係統調用增加flags的時候,必須加入對這些bit的處理。
Best practices for a big patch series
Linux內核如今是一個有幾千名開發者參與的龐大開源項目,但大多數情況下開發者可以互無影響地獨立完成工作,這主要得益於內核卓越的模塊化設計。但林子大了什麼鳥都有,有些修改還是不太容易適配到目前的開發模型中,例如那些修改涉及麵廣泛、跨越多個子係統的補丁。雖然近年來有所改善,但應付這種補丁無論是提交者還是維護者仍然是一種挑戰,提交者需要小心處理依賴關係、做好職責分解,補丁粒度也得合適,還需要為相關開發者提供足夠信息,否則雙方都會事倍功半。在本文中,作者Wolfram Sang以提交者的角度給出了應付這種補丁的一些建議。
補丁的組織
第一個問題就是修改應該以什麼形式呈現?下麵是一些常用策略:
- 一次修改整個代碼樹。例如在修改了某個API之後,直接將涉及這個API的所有文件的修改都放到一個補丁中。這麼做的優點是隻要一個高大全的維護者負責處理就行,可以很好地處理任何依賴關係問題。主要缺點是合並會涉及到大量子係統,很可能會發生衝突。
- 每個補丁對應一個文件的修改。這可以給每個子係統維護者自主處置的自由和方便,但如果修改之間有依賴關係,一場噩夢就來臨了,這種方法也會在git repo中塞入大量同質補丁。這種方法的另一個好處是可以在二分查找bug時提供更細的粒度。
- 每個補丁對應一個目錄的修改,介於以上兩種方法之間的一個招兒。優缺點不必說了,作者還舉了兩三個例子,看原文唄。
- 不做特意的補丁分組,派一群小強補丁包圍維護者。還是會有些補丁不能做粗粒度合並,例如,在不知道硬件細節的情況下驅動修改,並且提交者沒有測試環境。這種修改如果不測試的話恐怕是無望合並的,所以——向維護者致敬吧,當他/她無法及時回複郵件時,說不定就正在苦著臉反複按那個big red button呢。
當然,無論怎樣,以上的分解策略都應該隻出於技術因素考慮,提高補丁數量尤其不應該作為分拆的目標。如果不確定的話,基於目錄組織補丁一般是個不錯的起點,在不影響靈活性的前提下要盡量降低補丁數量,而且最好在補丁說明中提一下你在補丁組織上的想法。
開發過程
無論是什麼形式的補丁,"release early, release often"準則都仍然是適用的。準備一個公開的git repo是方便別人跟蹤你的修改的好方法。如果修改比較複雜,最好先發個RFC征求一下意見和建議。如果是RFC式的修改,最好從單個子係統上開始。還可以請求吳峰光利用他的牛掰測試環境運行你的修改,如果沒有缺陷並且沒有反對意見就盡快發出補丁吧,當然不要忘記必要的補丁介紹。有可能一套補丁會經曆多個開發周期才能陸續被接受,要準備好不斷跟進嗬。
如果討論後決定使用第一種all-in-one的方法,補丁發出後有些維護者反饋說願意接受你的部分修改,而其餘維護者隻回複了Acked-By,這時你就可以向Linus發pull request了,通常,rc1之後的短暫時間窗口是個發pull requests的合適時機。同時,也要向Stephen Rothwell發出對linux-next的pull request。當然,你得事先做好patch的rebase。
補丁發給誰
git send-mail是個發送補丁郵件的利器。除了手工增加郵件接收人和轉發人(CC),內核代碼樹中的get_maintainer.pl是個不錯的CC管理工具,它可以接受.get_maintainer.conf作為配置文件。推薦將--no-rolestats寫入配置中,這可以避免郵件列表常見的一些垃圾內容。如果采用的是按文件組織補丁,需要使用--git-fallback,因為默認的行為CC子係統的maintainer,而且文件級的修改者。CC過多,會導致退信,這時可以嚐試一下--no-m選項,它隻收集郵件列表。
File-private POSIX locks
文件鎖被廣泛的用於數據庫、文件服務器等應用程序中。當你有多個程序要同時訪問一個文件的時候,這些訪問沒有同步的情況下很容易文件數據的損壞或者其他程序異常。目前的文件鎖可以解決上麵提到的問題。但目前的文件所實現使用起來較為困難,特別是對多線程程序來講。本文提到的File-private POSIX locks正在嚐試通過融合BSD和POSIX文件所中的元素來提供一個對線程更加友好的文件鎖API。
多個寫者同時修改同一個文件很容易互相影響。此外,對一個文件的修改可能會發生在一個文件的不同位置,如果另外一個線程看到的僅僅是這個修改操作的一部分,則很有可能會觸發程序的異常。
文件鎖通常有兩種類型:讀鎖(即共享鎖)和寫鎖(即排它鎖)。讀鎖允許多個程序同時訪問一個文件的某個部分;但某一時刻寫鎖隻允許被一個程序獲取。文件鎖在某些操作係統上是強製的,而在UNIX類操作係統上,文件鎖是通常並不是強製的。這樣的文件所有點類似於信號燈,他們隻有在所有程序在訪問文件之前都嚐試獲取它的情況下才能工作正常。
POSIX規範中定義了文件鎖處理的相關細節。規範中定義一個文件的任意字節均可以被一個文件鎖保護。但是,很不幸,這個規範中存在很多嚴重的問題使得現在的應用程序使用起來十分困難。
POSIX文件鎖的問題
當一個程序嚐試獲取一個文件所的時候,係統會根據當前其他鎖的狀態來批準或者拒絕這個請求。如果沒有鎖衝突,則批準請求;否則拒絕該請求。
在經典的POSIX文件鎖規範中,來自相同進程的鎖請求不會產生衝突。當針對一個文件的鎖請求與之前進程中一個已經獲取的鎖有衝突時,內核認為新的鎖請求是對原來鎖的修改。因此POSIX規範中的文件鎖無法用來同步同一進程內不同線程對同一個文件的訪問。隨著現在多線程應用的不斷普及,這一文件鎖變得越來越沒有用處。
此外,規範中還指出所有歸屬於一個進程的文件鎖在文件描述符被關閉時均會被釋放,即使該文件仍然被另一個文件描述符打開著。這一特性使得程序員不得不格外注意在確保文件鎖被釋放前不能關閉相應的文件描述符。
這就是問題更加複雜了。如果一個程序分別打開一個文件的兩個硬鏈接,在其中一個上獲取了文件鎖,然後關閉另外一個文件描述符。則文件鎖就會被悄悄的釋放掉,即使獲得文件鎖的文件描述符仍然處於打開的狀態。
這就造成應用程序在使用庫訪問文件時候的怪異問題。一個庫函數中打開、讀/寫數據並關閉文件,而函數的調用者根本不知道這一切的發生。如果在調用這個庫函數時,應用程序恰好獲得了這個文件的鎖,則程序根本意識不到鎖已經被釋放了。這就很可能造成數據的損壞。Jeremy Allision在這篇博客中詳細描述了這個問題。
此外,有一個誕生於BSD Unix上的文件鎖標準。這個文件鎖標準(使用flock(2)係統調用)有一個更加合理的語義。相比於POSIX文件鎖總是屬於某一進程,BSD文件鎖總是屬於開打的文件本身。如果一個京城打開一個文件兩次,並且嚐試設置兩個排它鎖,則第二次鎖請求會被拒絕。所以,BSD文件鎖更適合在線程間保證同步語義。注意使用dup(2)克隆一個文件描述符不能保證這一語義,因為這僅僅是增加了打開文件的引用計數。
BSD文件鎖會在打開文件的最後一個引用被關閉的時候釋放。因此如果一個程序打開一個文件並獲得一把文件鎖,然後使用dup(2)來複製一個文件描述符,則文件鎖會在所有文件描述符被關閉時自動釋放。
現在的問題是BSD文件鎖隻能針對整個文件進行加鎖。而POSIX文件鎖可以針對一個文件的任意部分進行加鎖。當然對整個文件加鎖也是有用的。但是在很多應用中,對整個文件加鎖是不夠的。應用程序,比如數據庫,需要細粒度鎖來提高並發性。
File-private PSOIX locks
根據上麵的描述,我們現在需要的是一個混合兩個文件鎖有點的新文件鎖——一個帶有BSD文件鎖語義可以跨fork(2)和close(2)係統調用的字節範圍鎖。此外,因為有很多程序依然在使用傳統的POSIX文件鎖,所以新的文件鎖要能夠識別傳統POSIX文件鎖並正確處理他。
經典POSIX文件鎖使用fcntl(2)係統調用來控製:
* F_GETLK - 測試是否可以獲取一個文件鎖
* F_SETLK - 嚐試獲取一個文件鎖。如果失敗返回錯誤
* F_SETLKW - 嚐試獲取一個文件鎖,如果不能獲取則阻塞
這些命令的使用需要一個struct flock參數:
<source lang=c> struct flock {
short int l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */
short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
off_t l_start; /* Offset where the lock begins. */
off_t l_len; /* Size of the locked area; zero means until EOF. */
pid_t l_pid; /* Process holding the lock. (F_GETLK only) */
}; </source>
於此類似,file-private POSIX locks使用相似的命令集,隻是添加了‘P'作為後綴:
* F_GETLKP - 測試是否可以獲取一個文件鎖
* F_SETLKP - 嚐試獲取一個文件鎖。如果失敗返回錯誤
* F_SETLKPW - 嚐試獲取一個文件鎖,如果不能獲取則阻塞
新的命令看上去和此前的命令非常類似。唯一的區別僅僅是文件鎖的屬主。經典POSIX文件鎖屬於進程而file-private POSIX locks的屬主是打開的文件。
使用file-private POSIX locks
目前為了使用新的文件鎖需要定義__GNU_SOURCE預定義宏,因為目前file-private POSIX locks還不是POSIX標準的一部分。新文件鎖的使用和舊的十分相似。在大多數情況下,應用程序可以直接用新文件鎖替換舊的文件鎖,盡管他們有些許不同。
經典POSIX文件鎖的最大問題是在文件被關閉時,所以很明顯新的文件鎖在文件被關閉時的行為在這裏是不同的。新的文件鎖僅在打開文件的引用計數為0時才會被自動釋放。
盡管我們很容易認為新的文件鎖事屬於文件描述符的,但這一描述嚴格說來並不準確。如果一個文件描述符是通過dup(2)克隆的,內核僅僅是對已經打開的文件的引用計數加一並且在文件描述符表中分配一個新的槽位。使用新文件鎖對一個克隆的文件描述符進行加鎖並不會與原始的文件描述符產生衝突。內核會將這樣一個鎖請求認為是對已有鎖的更新請求。同時,一旦兩個文件描述符都被關閉,file-private lcoks也將自動被釋放。盡管應用程序可以通過F_UNLCK請求手工釋放鎖。
fork(2)背後的操作也是類似的。當程序調用fork(2)後,內核會增加已經打開的文件的引用計數並且在新進程的文件描述符表中分配相同的槽位。對於相同已打開文件的鎖之間不會衝突並且在兩個進程均關閉該文件後,文件鎖會被自動釋放。
經典POSIX文件鎖和file-private文件鎖同時使用會有衝突,即使在相同進程或者相同文件描述符上。很明顯不應該同時混用新舊文件鎖。
F_GETLK向何處?
F_GETLK也許應該被命名為F_TESTLK更加合適。進去嚴格來講,他確實獲取了鎖的狀態,但是它真正的用途是在不嚐試加鎖的情況下測試一個給定範圍的文件鎖是否可以被獲得。如果在此範圍內已經有鎖被其他進程獲得,那麼內核會覆蓋掉struct flock的相關信息,並且設置l_pid來指明當前獲得鎖的進程。
l_pid對於file-private locks來講是一個問題,因為file-private locks並不屬於任何進程。一個文件描述符可能是通過fork(2)繼承而來的,所以l_pid的數值對於file-private locks來講已經沒有任何意義。
在Linux中,POSIX和BSD文件鎖事完全不同的名字空間。而在BSD上,他們在相同的名字空間。因此在BSD上,POSIX和BSD文件鎖是相互衝突的。如果一個程序獲得了一個BSD文件鎖,而另一個程序嚐試通過F_GETLK獲得所得狀態,BSD內核會將l_pid設置為-1。由於可移植的程序已經針對此行為進行了處理,所以對於file-private locks來講,將l_pid設置為-1師一個合理的選擇。
在線程中使用file-private locks
下麵我們通過一個例子來看一下file-private locks在線程中的使用:
<source lang=c>
1. define _GNU_SOURCE
2. include <stdio.h>
3. include <sys/types.h>
4. include <sys/stat.h>
5. include <unistd.h>
6. include <fcntl.h>
7. include <pthread.h>
1. define FILENAME "/tmp/foo"
2. define NUM_THREADS 3
3. define ITERATIONS 5
void * thread_start(void *arg) {
int i, fd, len;
long tid = (long)arg;
char buf[256];
struct flock lck = {
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 1,
};
fd = open(FILENAME, O_RDWR|O_CREAT, 0666);
for (i = 0; i < ITERATIONS; i++) {
lck.l_type = F_WRLCK;
fcntl(fd, F_SETLKPW, &lck);
len = sprintf(buf, "%d: tid=%ld fd=%d\n", i, tid, fd);
lseek(fd, 0, SEEK_END);
write(fd, buf, len);
fsync(fd);
lck.l_type = F_UNLCK;
fcntl(fd, F_SETLKP, &lck);
usleep(1);
}
pthread_exit(NULL);
}
int main(int argc, char **argv) {
long i;
pthread_t threads[NUM_THREADS];
truncate(FILENAME, 0);
for (i = 0; i < NUM_THREADS; i++)
pthread_create(&threads[i], NULL, thread_start, (void *)i);
pthread_exit(NULL);
return 0;
} </source>
上麵的程序首先創建了3個線程,每個循環5次向文件中追加寫入數據。文件的訪問通過file-private locks來進行同步。上麵的程序在運行後,我們會在文件中看到15行數據。
如果我們將上述程序中的F_SETLKP和F_SETLKPW替換為舊的POSIX文件鎖,則我們得到的會是損壞的數據。
結論
File-private locks能夠解決目前我們在使用經典POSIX文件鎖時遇到的問題。但是在使用中程序員需要注意新舊文件鎖的區別。
來自很多項目(Samba, NFS Ganesha, SQLite和OpenJDK)的開發人員已經對新文件鎖表示了極大的興趣。因為新文件鎖可以簡化程序中關於文件鎖的代碼,同時避免數據的損壞。
相關的內核補丁已經可以在郵件列表中獲取,或者通過linux-next代碼樹獲取。在linux-next中的相關代碼將會被及時的更新。所以你現在就可以通過linux-next來使用新的文件所。同時這裏有一個glibc的補丁,其中定義了新文件所的相關訪問方法。
目前內核代碼預計將於3.15版本合並進linux內核,隨後glibc的相關代碼也會被合並。與此同時,一份關於更新POSIX規範的請求也已經被提交,以便後續工作的開展。
C11 atomic variables and the kernel
C11標準為C和C++增加了一係列新特性,其中之一便是內置的原子類型,內核開發社區應該會比較感興趣。但是經過一些討論之後發現,內核社區似乎對切換原子類型的需求並不迫切。目前內核已經提供了一些原子類型及對應的操作方法,可以允許變量被原子的訪問而不需要顯式的加鎖。C11原子類型有助於內核原子類型的實現,但它的作用顯然不止如此。對C11原子變量的訪問附帶有一個顯式的"內存模式(memory model)",它用來描述什麼類型的內存訪問可以被處理器或編譯器優化,最寬鬆的模式允許操作被重新排序或者合並來提高性能。默認的模式("sequentially consistent")是最嚴格的,不允許對操作進行任何合並或重排,因此代價很大,而這些代價在不影響正確性的前提下會顯得有些浪費。最寬鬆的模式允許在可控的方式下做一些優化並確保排序的正確性。
C11原子變量包含內核裏稱為內存屏障(memory barrier)的特性,例如在內核裏可能有如下代碼:
smp_store_release(&x, new_value);
smp_store_release() 語句告訴處理器確保它之前的所有讀寫操作在x賦值之前都已執行完成,而該語句之前的操作可以被重新排序,該語句之後的操作也可以被重排。在大多數情況下,操作都可以被重新排序而不影響正確性,因此隻在必需的地方使用barrier可以使得其他大部分不需要barrier的訪問被優化並顯著提升性能。
如果x是C11原子類型,可以寫成:
atomic_store(&x, new_value, memory_order_release);
memory_order_release指定了和smp_store_release()相同的順序要求。
smp_store_release()的實現方式采用了特定於體係結構的代碼,而C11方式則是在編譯器裏實現。當內核開始支持多核係統時,C語言並沒有原子類型或內存屏障的概念,因此內核開發者必須自己想辦法。然而既然現在語言標準也提出了解決辦法,內核似乎應該使用標準的原子類型,雖然這個轉變過程可能會很慢。
對優化的擔心
編譯器的一個判斷標準經常是代碼的生成速度,編譯器開發人員在優化代碼時就會盡可能地追求這個目標。寫程序時如果不仔細研究標準的話,一些情況下的優化可能會違反代碼邏輯。在內核開發者看來,編譯器開發者經常對標準咬文嚼字來判斷是否進行優化,而這些優化通常沒有意義或者違反代碼。內核裏並行度很高的代碼在進行優化的情況下會更易於出錯,因此內核開發者需要更加小心。其中一個問題是"寫預測(speculative stores)",造成一個不正確的值對別人可見。一個經典的例子代碼如下:
if (x)
y = 1;
else
y = 2;
編譯器很可能進行如下優化:
y = 2;
if (x)
y = 1;
對於在自身地址空間裏的順序代碼而言,優化結果是相同的且後者會省去一次跳轉,但如果y在別處可見,在判斷x之前就提前存入值就會引起錯誤的結果,因此這種會被其他運行進程可見的錯誤優化就需要被避免。標準裏對原子變量行為的描述比較複雜,內核社區對寫預測也比較擔心,代表內核方麵參與標準委員會的Paul McKenney對這種可能性也不是完全確定,他認為雖然標準基本上可以避免寫預測行為,但是可能會有一些corner case。
另一個關心的問題是控製流依賴:原子變量和控製流交互的地方。考慮一下代碼:
x = atomic_load(&a, memory_order_relaxed);
if (x)
atomic_store(&y, 42, memory_order_relaxed);
y的賦值依賴於x的值,但是C11標準目前沒有進行描述,這意味著編譯器或者處理器可以任意處理這兩個原子操作的順序,更甚至把判斷分支優化掉,而這種優化結果可能會給內核帶來災難。針對這種情況Paul建議增加一些標誌和一個新的memory_order_control內存模式來使控製依賴關係更加明顯:
x = atomic_load(&a, memory_order_control);
if (control_dependency(x))
atomic_store(&b, 42, memory_order_relaxed);
但是因為Linus對這個主意很不爽,不大可能被采用。Linus認為控製依賴關係很明顯,代碼裏已經寫了要先判斷x的值,任何錯誤地改變atomic_store()的編譯器行為都違反了代碼。
還有一種"值預測(value speculation)",編譯器提前猜測變量的值,如果預測錯誤就再加入一些分支去修正,預測正確的話處理器的硬件預測分支會再次加速執行。Paul的一些工作證明值預測有時是錯誤的,慶幸的是那些情況將不被允許,但也不能100%確定當前標準會禁止所有可能的情況。
非局部優化的問題
另一個憂慮是關於全局優化,編譯器開發者會在整個源文件或源文件組的層次上優化程序,如果編譯器理解變量的使用,那麼優化效果經常會不錯。但是編譯器並不了解程序執行的硬件信息,因此標準規定它應該假設是在一個虛擬的機器上工作,因此如果真實的機器和虛擬機器行為不一致,結果就可能出現問題。Linus舉過一個例子:編譯器可能搜集內核訪問頁表的信息並注意到代碼裏並不會設置"page dirty"位,它如果得出任何測試dirty位的判斷都可以被優化掉的結論就顯然錯誤了,因為該位是由硬件進行設置的而不是內核代碼。Linus認為這種優化本身就違反了定義,因為它假設所有信息都會在程序中給出。Paul也給出了一個列表證明編譯器的虛擬機器模型和真實情況並不匹配,他的例子包括匯編代碼,內核模塊,內核空間內存映射到用戶空間,JIT-compiled BPF代碼等,這些都說明內核中的很多信息編譯器都不太可能知道。
對這些非局部問題的一個解決方法是對易於出錯的變量使用volatile,但這會關閉對該變量的所有優化,也不能使用原子變量。如果必須使用volatile,內核還不如繼續使用當前的內存屏障,它起碼能允許編譯器和處理器上盡可能的優化。雖然有這些擔心,但Linus也認為現實情況下編譯器不大會這麼幹,它們不至於破壞依賴關係鏈,雖然說沒有明確的保證。
內核會使用C11原子類型麼
Linus認為如果C11原子變量的標準和實現不能滿足內核的需要,內核就不會使用這個特性。內核非常複雜非常龐大,是C11方案最想應用的客戶,但是內核現有的方案工作的很好,如果沒有很好的理由他就不會切換過去。Torvald也同意這個觀點,但他也認為使用這個標準機製也會有一些好處,比如廣泛的測試,以及把內核不同體係結構相關的代碼都換成更通用更易維護的代碼等。另外使用C11原子類型也會從一些學術研究工作中獲益,劍橋大學的一些研究人員也提出了C11下的並發應該如何工作的論文等。如果最後大量程序都開始使用C11原子類型,編譯器的質量也將會有很大提升。
最後如果C11原子類型被廣泛使用,就有可能為C/C++在高並發環境下建立可靠的模式規範,而當前開發者並不知道如何做才是安全的。這樣C11標準就有點類似於"封裝"且原子類型可以成為更通用的語言,這樣開發者可以更好的理解並發以及允許的優化。
因此如果使用這些新的語言特性內核也會有所受益,但是轉換的過程也會很漫長,畢竟底層代碼的並發性bug很難定位。路漫漫啊。
C11 atomics part 2: "consume" semantics
接著上篇文章,最近的討論圍繞著稱為"consume"的內存訪問順序。
consume介紹
內存訪問順序方麵有兩個操作稱為"acquire"和"release",這個鏈接介紹了標準裏的模型含義。簡而言之,帶有"acquire"語義的讀操作必須保證發生在同進程裏所有後續讀寫操作之前,帶有"release"語義的寫操作必須發生在該句之前所有讀寫操作的之後。這兩個語義經常一起使用,最終修改一個數據結構的寫操作帶有"release"類型,而讀該數據指針的操作帶有"acquire"類型。"acquire"和"release"是無鎖共享數據環境下一個常見的概念,但是在很多情況下一個acquire操作提供了過於嚴格的順序要求,而有些沒有必要。帶有acquire語義的讀操作限製了所有後續的讀寫順序,即使這些請求跟本次的讀數據沒有任何關係,而它們其實可以被編譯器或處理器進行重排序的優化,這種情況就是內核希望acquire所能提供的較弱的順序保證。與之相關的是read-copy-update(RCU)操作,簡單說RCU的工作原理是保護通過指針訪問的數據結構,修改數據時需要先分配一個新的數據結構,將更新數據拷貝過去,最後更新指針來指向新的結構。訪問該數據的代碼要麼看見舊的數據指針要麼看見新的數據指針,但在訪問的當時都是有效的。這其中有一點很重要,更新數據結構指針之前,新的結構裏的數據必須已經有效,否則可能會有進程訪問到無效數據。我們可以使用帶release語義的指針賦值來滿足這個要求,而讀指針的操作(經常通過rcu_dereference())帶上acquire語義,但是其實真正要關心的是 獲得指針 和 訪問該指針的數據 兩者之間的順序,而這在很多處理器上不需要內存屏障就可以達到目的。
提供較弱版本的順序保證就是"consume",它隻保證和讀操作有依賴的寫操作的可見性。代碼類似於:
p = rcu_dereference(pointer);
q = p->something;
*a = something_else;
在acquire語義下,*a的賦值語句不能重排序到rcu_dereference()調用之前,而consume就可以,而且在一些體係架構下運行時排序的代價也非常低(或是0)。目前RCU類的技術經常使用在性能敏感的環境下,因此如果這時有consume語義就值得一試了。
Fixing consume
目前consume順序的問題在於標準要求記錄數據訪問的所有依賴關係,而這些記錄通常很難做,且記錄的結果對於開發者也不易讀,經常有GCC報告的bug反應consume順序的處理不正確。而一些編譯器的consume實現其實就是acquire,有著正確的結果但是性能較低。這些問題都使得consume很難在內核裏使用,內核也會繼續使用體係機構相關的宏或內存屏障。但是標準本身能否對consume順序做出一些修改呢?Linus有一個建議,去掉繁雜的描述依賴關係的語言以及跟蹤記錄機製,而換成一個更簡單的方案:consume隻保證 原子讀操作 和 通過直接或間接指針鏈來訪問數據的操作 之間的順序(原話是:The consume ordering guarantees the ordering between that atomic read and the accesses to the object that the pointer points to directly or indirectly through a chain of pointers)。想法很簡單,其實就是提供RCU所需要的順序保證即可,但是這塊有一些小問題,"指針鏈"的概念是指賦值或者簡單的修改,例如賦值是:
p = rcu_dereference(something);
下麵這些賦值也會產生指針鏈:
q = p;
r = p + 1;
但是"指針鏈"並不包含別名,如果別的指針也恰巧指向p指向的結構,通過該指針發生的訪問就不會有順序保證,這使得Linus建議的consume語義不同於標準的定義,因為後者要求編譯器也獲得所有的別名指針信息。
Paul McKenney向標準委員會寫了一個十二條準則試圖描述"指針鏈"的構成,有一個例子是:
q = p & ~0x1;
r = p & 0x1;
邏輯AND操作在內核很常見,指針的低位bit常被用來保存額外的flag信息,q的賦值語句可以當作是有依賴關係的指針鏈,但r的賦值語句不是。編譯器可以發現第二條賦值其實是生成一個整數而不是指針,因此便可以做相應的優化。但是Linus說你可以想出各種各樣的方式來繞過依賴鏈,比如:
p = atomic_read(pp, consume);
if (p == &variable)
return p->val;
這種情況下編譯器可能將p->val替換成variable.val,這樣就不是指針鏈也沒有順序保證,variable.val的讀操作也可能發生在原子讀之前。而如果把 == 換成了!=,那麼指針鏈和操作順序就能保證,因為編譯器提前不知道p指向何處。
Deferrable timers for user space
deferrable timers的概念最早可追溯到2007年。可延遲定時器(deferrable timer)可用於定時器到期和超時代碼執行之間可以存在一個任意延遲的情況。在這種情 況下,定時器到期時間可以延遲直到CPU被其他事件喚醒。可延遲到期時間可以通過這種方式減少CPU喚醒次數,也因此可以減少耗電量。
deferrable timers在內核依舊支持,但是並沒有提供給用戶態。導致定時器相關的係統調用(包括timerfd_settime(),clock_nanosleep(),和nanosleep())將在定時器到期時盡量通知用戶態,即使用戶態能夠容忍一定的延遲。這對致力於改善耗電量的開發者來說很不能接受的。對於這些開發者來說有個好消息,經過數次錯誤的開始,現在看來deferrable timers終於有望對用戶態提供。
一些讀者肯定會想到提供給用戶態有的timer slack機製。但是,這個機製會影響所有的定時器,對於一些應用部分定時器可能比其他的可延遲更久。帶slack的定時器隻能被延遲一樣的時間,意味著它們到期時還是需要喚醒一顆正在休眠的CPU。deferrable timers可能會很適合這樣一些timer slack不能很好處理的用例。
早在2012年,Anton Vorontsov曾發過一組patch用於為timerfd_settime()係統調用添加deferrable timer支持。在合並這個patch的過程中,Anton碰到了一個問題 :和所有其他定時相關係統調用,timerfd機製使用了內核的高精度定時器。而高精度定時器不支持deferrable操作,這種功能僅僅限用於老的"timer wheel"機製。timer wheel是存在很多問題的老代碼,被提出需要移除已經有幾年了,但一直沒有人完成這部分工作,使得timer wheel仍然在那,同時也是唯一一個支持deferrable選項的地方。
Anton做的是通過這兩個機製在timerfd子係統中將定時器分開。普通的定時器請求將照常使用高精度定時器子係統,而任何請求中如果帶有TFD_TIMER_DEFERABLE標 識,則會由timer wheel處理。除其他事項外,唯一限製的是deferrable timers精度隻能達到一個jiffy(0.001到0.001秒,由內核配置決定),但是,如果這個定 時器是deferrable,低精度並不一定是個問題。不過,這個補丁並沒有走很遠,Anton似乎很快就放棄它了。
最近,Alexey Perevalow重新拾起這一概念,嚐試著去推進。他首先在一月份發了一個patch,這個一發帶來的討論甚至超過了之前那次。John Stultz更關心的是僅僅timerfd定時器能夠獲得這一新功能,他覺得更好的一個方式是將這一特性實現到更底層,使得所有的定時器相關的係統調用都將受益。這樣的話,意味著需要為高精度定時器子係統添加deferrable功能。
Thomas Gleixner語氣強硬的指出timer wheel的使用在將來將不會再存在。他建議這一功能應該通過提供一組新clock ID的方式添加到高精度定時器。clock ID由用戶態提供,用於指明使用哪種時鍾。例如,CLOCK_REALTIME對應於係統時鍾,CLOCK_MONOTONIC時鍾被保證單調遞增。也還有其他一些時鍾,包括在3.10添加的對應國際原子時間的CLOCK_TAI。Thomas拋出一組概念驗證的補丁,添加了所有這些新版本時鍾(如CLOCK_MONOTONIC_DEFERRABLE),用於提供deferrable操作。
然而John爭論到clock ID並不是正確暴露給用戶態的接口:
我的理由是,可延遲性並不是一種時鍾域,而更多隻是一個修改。為此為了保持合理的接口(避免必需要為每個新修改添加N個新clockid),我們應該使用定時器的flag參數。所以,相對於當前僅有的TIMER_ABSTIME,我們可以添加比如TIMER_DEFER,以供利用。
內核使用clock ID作為內核實現是沒問題,但是提供給用戶態用於請求該特性正確的方式是一個可以修改的flag,他說到。幸運的是幾乎所有相關的係統調用已經有一個flag參數,唯一大例外的是一些開發者希望看到簡單消失掉的nanosleep()。這樣的話,John的爭論獲勝並沒有真正的異議。
Alexey發了這組patch的很多版本,但是並沒有得到Thomas的讚同。Thomas最終發了一組他自己的deferrable定時器補丁,以展示他覺得這個問題該如何解決。在這組patch中,clock_nanosleep()添加了一個新的TIMER_IS_DEFERRABLE標識,而timerfd_settime()添加了TFD_TIMER_IS_DEFERRABLE。任何一種方式設置標識都將導致內核使用新deferrable內部時鍾ID的一個。基於這些ID的定時器並不對硬件編程,因此他們並不產生中斷,也無法喚醒係統。到期函數將在係統被其他進程喚醒>時執行。沒有使用新flag的係統調用和之前一樣。
Thomas的補丁在除了底層實現細節外,並沒有收到太多的評論。如果這種情況持續,靜默意味著同意。因此,若無意外內核可能會在3.15左右版本為用戶態提供deferrable timer。
What Linux and Solaris can learn from each other
Brendan Gregg目前作為性能優化工程師供職於Joyent。Joyent是一家雲計算提供商,它維護了一個基於Solaris的操作係統SmartOS。SmartOS是一個誕生不久的操作係統,但是Gregg已經擁有在其他Solaris係統上進行性能分析和診斷的大量經驗。他在SCALE 12x中的演講介紹了Linux和Solaris陣營應該從對方那裏學習的東西。
Gregg從一個基本的問題開始試著了解不同操作係統的不同。為什麼一個應用程序在Linux和Illumos的表現會有不同呢?(Illumo是一個基於OpenSolaris的開源操作係統。)SmartOS使用Illumos內核,Joyent則同時提供SmartOS和Linux鏡像給客戶。所以從某些角度來說,這是不同操作係統內核之間的比較,但是在很多方麵,係統架構的其他方麵也對性能有著巨大的影響。Gregg將這兩者作為不同的問題區別開來。
性能優化,並不簡單
Gregg通過一個循環1億設置字符串變量的perl腳本來比較兩個係統性能上的差異。盡管隻有一行,但是這一腳本在兩個係統上運行的結果性能相差了14%(他沒有說哪個係統差)。他指出,盡管隻有一行,但要想優化這一程序則是十分複雜的。因為在Linux和SmartOS係統上,不同的因素太多了。perl解釋器的版本,被不同編譯器編譯的perl解釋器,不同的編譯器優化參數,不同的係統庫,不同的後台任務等等。任何這些組合都會直接影響程序的性能。當然這其中也包含內核的因素:設置字符串涉及到內存IO,內核自己來決定代碼在內存中的位置,內核控製CPU的時鍾頻率,內核可能受到中斷的影響,內核可能會將進程遷移到不同CPU上。
而問題的核心是客戶希望了解這一14%的性能差距根源出在哪裏?作為一名性能優化工程師,Gregg希望自己能夠跟蹤到引起這一差別的根源(盡管這一根源並不總是很容易觀察到),判斷這一差別是否是不同內核之間的差別,並且這一差別是否可以被修複。這些問題並不容易回答。很多時候,比較Linux和SmartOS之間的差別就想比較美國和澳大利亞的差別一樣。他們有很多不同,也有很多相似之處。
這裏他列舉了兩個內核中一些比較重要的影響性能因素。Linux上總是有更新及時的軟件包,這些軟件可以被更廣泛的測試,設備驅動也更豐富,配置選項頁更豐富。嚴格來說,RCU,futex和動態時鍾對性能有著比較大的影響。而SmartOS上則有十分成熟的Zone虛擬化係統,ZFS文件係統,DTrace框架,可用於性能排查的豐富的符號表,優秀的CPU擴展性(源於Solaris內核通常會在眾核機器上被反複測試和分析)。此外,這兩個內核有很多小的區別也會影響到性能。
Gregg提醒我們,他演講的主題是兩個內核互相能從中學習到什麼。
Solaris能從Linux中學到什麼
Solaris能從Linux中學到的是:
從非技術角度講是豐富、被及時更新的軟件包。Solaris經常遇到的性能問題是由於過舊的MySQL或者OpenSSL造成的。
從技術角度來講,Linux中的likely()/unlikely()為編譯器的分支預測提供了一種很有效的機製。Solaris中則缺少此類機製。Linux中的tickless特性也是一個改進性能的優秀特性。Solaris中目前仍然使用clock()函數來獲取時鍾周期。Gregg經常遇到的問題是10ms的延遲(clock()默認的時鍾頻率)在將close()頻率修改為1000Hz後變為了1ms。
目前Solaris會將進程完全交換出去,這一特性從PDP-11/20時代就被引入了。在那個時期,把整個進程患處是合理的,因為當時進程最大不過64KB。而分頁的支持時候來才加入的。Gregg認為應該將整個進程換入換出特性拋棄。另一方麵,Solaris對於虛擬內存是有限製的,而Linux則允許盡可能多的使用使用內存,並通過OOM來進行回收。Solaris的工程師不能想象這一特性。Gregg說,使用這一特性的Linux運行在一個手機上也許是好的,但是對於服務器則說不同。同時他也警告目前Linux中很多地方並沒有檢測ENOMEM錯誤。
一個有趣的例子是Linux中的SLUB分配器。SLUB是Solaris中SLAB分配器的一個簡化版本,但是它可以帶來一定的性能提升。因此Solaris應該考慮將其移植回來。此外,Solaris也缺少Linux中懶惰TLB模式。這一模式在Linux中帶來了顯著的性能提升。Linux中的Sar工具能夠提供比Solaris更豐富的信息。這也是值得Solaris學習的地方。
最後,Gregg指出盡管Solaris有成熟、可靠的虛擬化機製Zone。但是Zone目前隻能運行一個內核。而KVM則可以運行多個OS。Joyent開發團隊已經將KVM移植到了Illumos內核上,但是Oracle還沒有合並。
Linux能從Solaris中學到什麼
當然,Linux也有很多需要學習的地方。ZFS文件係統是一個非常棒的文件係統,它有很豐富的特性。盡管因為許可證的問題,Linux不能直接合並ZFS代碼。但是Linux中有Btrfs和ZFS on Linux。Solaris中的Zones虛擬化技術有很好的性能,而最近Linux也開始學習其中的概念,比如LXC,cgroup等等。
Gregg在這裏提醒大家注意Solaris中的STREAMS。它曾被作為內核消息模塊在Unix第八版時引入。Solaris使用它來實現TCP/IP協議棧,結果是在過去的數年中不斷出現的性能問題。
另一方麵,Gregg指出Solaris上很容易進行性能分析。而在這方麵Linux則做得不盡如人意。Linux上默認情況下編譯器會丟棄符號表,因此性能分析工具隻能輸出難於辨認的十六進製代碼。同時由於編譯器會丟棄棧幀,因此Linux上對於棧的分析也很困。Gregg建議應該使用-fno-omit-frame-pointer選項。Solaris上的prstat -mLc命令可以提供豐富的線程狀態分析,而Linux上則機會沒有任何微觀統計信息。Linux可以學習的還有在htop工具中提供更加豐富的功能。SmartOS中還有一個叫做vfsstat的工具可以同來輸出vfs中鎖衝突,資源限流等等信息。
關於性能分析工具最大的爭論是DTrace。DTrace是一個可編程、實時的性能跟蹤工具。它幾乎可以解決任何性能問題。最為關鍵的是DTrace可以直接用於生產係統上。目前DTrace在Linux上有兩個實現。Gregg這裏給出的Linux應該學習的是生產安全永遠是最重要的。DTrace絕對不會將內核搞奔潰。你可以向使用top一樣使用它。
Linux上有很多項目可以提供相似的功能,比如perf和ktap。盡管這兩個並沒有完全為生產係統做好準備。SystemTap看起來雖然不錯,但是仍然有問題存在。Gregg說他自己並沒有時間來仔細使用LTTng,所以不好給出評價。
Gregg指出,目前Solaris中DTrace的一大不足是Oracle提供的DTrace代碼不全。如果DTrace4Linux可以不全這些欠缺的特性,那就真是太完美了。
Linux需要學習Solaris中追求性能的文化。Solaris長久以來被高端支付客戶使用,因此具備很多性能分析工具。Linux在不久的將來也會由此需求,而在這些性能分析工具上,Linux仍然有很大的欠缺。
最後所有人都想知道到底Linux和Solaris那個更快。Gregg則表示他自己在不同場景中遇到過5%到500%的性能差別。但他指出,最重要的不是兩個係統的性能差別,認識你自己的係統上的性能是多少,你是否可以自己將性能優化到滿意。
Seven problems with Linux containers
OpenVZ的Kirill Kolyshkin在今年的SCALE 12x上有一個talk,題目是“Seven problems with Linux containers”。從名字上看似乎是在批評container,不過實際的內容是關於在容器化(containerization)過程中的挑戰以及對應的策略。在開篇的時候,Kolyshkin提到在LinuxFest Northwest上他提到了6個問題,到了SCALE 12x變成了7個問題,所以實際上問題是在不斷增加的,而每個問題都可以充分展開來說。
一 高效的虛擬化
首要的問題是如何在用戶不增加硬件成本的條件下最高效得搞定虛擬化。目前OpenVZ使用的是Linux container,這個是在目前各種虛擬化方式下最好的辦法。但是我們應該看到,從曆史上看大機上的虛擬化曾經是最好的,並衍生了許多新的虛擬機管理方式,但是他們卻一直在持續的優化中。
如果把計算機看成一個三層三明治(這個比喻有點意思),這三層分別是硬件,操作係統和用戶態,每層都可以有自己的虛擬化方式。Intel從硬件來支持虛擬化,VM hypervisor通過hypervisor來提供多種操作係統的虛擬化,container則是通過用戶態來做。在container的方案中,我們僅僅是讓各個進程不可見,所以效率最高。另外Linux還有各種各樣的namespace,比如PID, network, users, inter-process communication (IPC)這些,所以簡單的實現虛擬化。
二 在container之間共享資源
第一步把進程限製在container裏麵以後,隨著而來的是另一個問題:如何在各個container之間分配資源,比如CPU,內存,磁盤等等,另外還有有一些需求比如需要設置不同container的優先級以及防止對某個containter的ddos攻擊會影響到其他container。
OpenVZ通過一整套資源控製方案來對每個container進行控製,其中一個是從HP/UX借鑒而來的beancounters,它大約可以以進程為力度控製20個左右的進程資源,但是事實證明這是遠遠不夠的(比如像apache,PostgreSQL,它們會啟動很多進程)。Linux upstream實現了cgroup,可以控製進程組級別的資源隔離,但是還是不夠成熟。
三 讓資源限製的概念更加清晰
前麵提到了資源限製,就引入了另一個問題:如何讓用戶理解這些概念。前麵提到OpenVZ有20個左右的資源控製維度,這麼多的維度又相互獨立,無疑極大增加了理解的難度,OpenVZ因此寫了很多wiki以及知識庫的文章,還做了一些工具想讓用戶能夠快速理解。不過杯具的是,用戶還是不太理解並不停的抱怨,"我隻想運行數據庫,為啥讓我學這麼多東西"。最後沒有辦法,OpenVZ提供了一個VSwap的概念,它隻包括RAM+SWAP,這樣就通俗易懂多了。
四 實例熱遷移
OpenVZ可以在兩台不同的機器之間做熱遷移,這一點連Solaris的zone都無法做到,但是如果做熱遷移就必須做到足夠快。一些大的container在運行Oracle數據庫,這意味著內存中有大量的數據需要遷移,這樣對於終端用戶的查詢相應會有很大的影響。OpenVZ的熱遷移一般有如下幾步:凍結container的一切操作,將container的狀態導入到一個文件,將文件傳輸到目標服務器,然後再將狀態導出,最後恢複container的運行。用戶能感覺到延遲主要是因為大量內存導致文件導入和傳輸的速度都非常的慢。於是OpenVZ開發出一種新的辦法——network swap,它隻將很小一部分關係到container啟停的內存傳輸到目標服務器,而後麵的任何內存需求則通過fault的方式來讀取。這種方案很快,但是卻很容易受到網絡的影響。所以OpenVZ新的方案是修改Linux內核,讓它記錄被修改的頁,然後周期性的拷貝它們從而達到一種類似rsync的效果。
五 合並進入Linux內核主線
將對內核的修改合並進入主線似乎是OpenVZ目前最大的問題。在剛開始的時候,OpenVZ並沒有想過將自己的修改合並進入Linux內核主線,於是它自己維護了一個很大的patch,但是漸漸的,它意識到將修改合並進入內核主線可以降低自己維護patch的時間並減少自己移植patch的次數。後來,OpenVZ開始嚐試合並,不出意料的有很多的阻力。於是OpenVZ開始按照內核社區的要求改寫代碼,比如用cgroup改寫beancounters,重新修改PID名字空間等等。對於有些社區不歡迎的patch,它們改寫成了用戶態,比如用戶態的checkpoint/Restore,令人高興的是,google和samsung對這個也很感興趣,也算是意外收獲吧!
六 文件係統
第六個問題是在container之間共享文件係統。container的根目錄其實就是普通文件係統裏麵的一個目錄,我們通過chroot讓container內的用戶以為是根,這樣帶來的問題就是如果在一個文件係統中安裝了大量的container,那麼元數據更新的性能將會是一個很嚴重的問題。大量的小IO會影響上麵提到的熱遷移,並導致DOS的發生。這裏Kirill舉了一個例子:你可以試著對一個1MB的文件執行一百萬次truncate,每次隻truncate一個byte,那麼整個文件係統的性能都會受到很嚴重的影響。另外目前比較成熟的文件係統還不支持對某個目錄做snapshot和disk quotas(注:btrfs沒人敢用),這裏譯者做個廣告:阿裏內核組已經加入了對目錄quota的支持,另外可以通過阿裏內核的overlayfs達到某種snapshot的效果。
LVM隻支持塊設備而loopback設備卻不支持動態空間分配,所以OpenVZ使用ploop,它支持動態調整空間,實時snapshot,並支持多種文件格式。
七 存儲的低效
Kolyshkin提到的最後一個問題和存儲相關。OpenVZ的很多客戶都是做虛擬主機的,所以每個主機都會有很多盤。最新的統計顯示這些用戶的平均磁盤利用率是37%,最高的達到51%,最低的也有14%,所以這裏磁盤利用率成為很大的問題,但更重要的是浪費的磁盤帶寬。
解決這個問題最簡單就是用SAN了,SAN提供搞可用性,高性能以及高利用率,另外由於SAN是共享的,所以container熱遷移的時候也不需要拷貝數據,不過SAN最大的毛病就是它的高價格啦。於是OpenVZ自己做了一個存儲係統pstorage(Parralles Cloud Storage),把所有的磁盤做成一個虛擬的SAN。看描述就是一個分布式文件係統了,這個大家都有的啦,嗬嗬。
最後更新:2017-06-05 17:31:28