內容簡介:
Linux 擁有現代操作系統所有的功能,如真正的搶先式多任務處理、支持多用戶,內存保護,虛擬內存,支持SMP、UP,符合POSIX標准,聯網、圖形用戶接口和桌面環境。具有快速性、穩定性等特點。本書通過分析Linux的內核源代碼,充分揭示了Linux作為操作系統的內核是如何完成保證系統正常運行、協調多個並發進程、管理內存等工作的。現實中,能讓人自由獲取的系統源代碼並不多,通過本書的學習,將大大有助於讀者編寫自己的新程序。
第一部分 Linux 內核源代碼
arch/i386/kernel/entry.S 2 arch/i386/kernel/init_task.c 8 arch/i386/kernel/irq.c 8 arch/i386/kernel/irq.h 19 arch/i386/kernel/process.c 22 arch/i386/kernel/signal.c 30 arch/i386/kernel/smp.c 38 arch/i386/kernel/time.c 58 arch/i386/kernel/traps.c 65 arch/i386/lib/delay.c 73 arch/i386/mm/fault.c 74 arch/i386/mm/init.c 76 fs/binfmt-elf.c 82 fs/binfmt_Java.c 96 fs/exec.c 98 include/asm-generic/smplock.h 107 include/asm-i386/atomic.h 108 include/asm-i386/current.h 109 include/asm-i386/dma.h 109 include/asm-i386/elf.h 113 include/asm-i386/hardirq.h 114 include/asm-i386/page.h 114 include/asm-i386/pgtable.h 115 include/asm-i386/ptrace.h 122 include/asm-i386/semaphore.h 123 include/asm-i386/shmparam.h 124 include/asm-i386/sigcontext.h 125 include/asm-i386/siginfo.h 125 include/asm-i386/signal.h 127 include/asm-i386/smp.h 130 include/asm-i386/softirq.h 132 include/asm-i386/spinlock.h 133 include/asm-i386/system.h 137 include/asm-i386/uAccess.h 139 include/linux/binfmts.h 146 include/linux/capability.h 147 include/linux/elf.h 150 include/linux/elfcore.h 156 include/linux/interrupt.h 157 include/linux/kernel.h 158 include/linux/kernel_stat.h 159 include/linux/limits.h 160 include/linux/mm.h 160 include/linux/module.h 164 include/linux/msg.h 168 include/linux/personality.h 169 include/linux/reboot.h 169 include/linux/resource.h 170 include/linux/sched.h 171 include/linux/sem.h 179 include/linux/shm.h 180 include/linux/signal.h 181 include/linux/slab.h 184 include/linux/smp.h 184 include/linux/smp_lock.h 185 include/linux/swap.h 185 include/linux/swapctl.h 187 include/linux/sysctl.h 188 include/linux/tasks.h 194 include/linux/time.h 194 include/linux/timer.h 195 include/linux/times.h 196 include/linux/tqueue.h 196 include/linux/wait.h 198 init/main.c 198 init/version.c 212 ipc/msg.c 213 ipc/sem.c 218 ipc/shm.c 227 ipc/util.c 236 kernel/capability.c 237 kernel/dma.c 240 kernel/exec_domain.c 241 kernel/exit.c 242 kernel/fork.c 248 kernel/info.c 255 kernel/itimer.c 255 kernel/kmod.c 257 kernel/module.c 259 kernel/panic.c 270 kernel/printk.c 271 kernel/sched.c 275 kernel/signal.c 295 kernel/softirq.c 307 kernel/sys.c 307 kernel/sysctl.c 318 kernel/time.c 330 mm/memory.c 335 mm/mlock.c 345 mm/mmap.c 348 mm/mprotect.c 358 mm/mremap.c 361 mm/page_alloc.c 363 mm/page_io.c 368 mm/slab.c 372 mm/swap.c 394 mm/swap_state.c 395 mm/swapfile.c 398 mm/vmalloc.c 406 mm/vmscan.c 409
第二部分 Linux 內核源代碼分析
第1章 Linux 簡介
讓用戶很詳細地了解大多數現有操作系統的實際工作方式是不可能的,因為大多數操作系統的源代碼都是嚴格保密的。除了一些研究用的及為操作系統教學而設計的系統外。盡管研究和教學目的都很好,但是這類系統很少能夠通過對正式操作系統的小部分實現來體現操作系統的實際功能。對於操作系統的一些特殊問題,這種折衷系統所能夠表現的就更是少得可憐了。
在以實際使用為目標的操作系統中,讓任何人都可以自由獲取系統源代碼,無論目的是要了解、學習還是改進,這樣的現實系統並不多。本書的主題就是這些少數操作系統中的一個:Linux。
Linux的工作方式類似於Uinx,它是免費的,源代碼也是開放的,符合標准規范的32位(在64位CPU上是64位)操作系統。Linux擁有現代操作系統的所具有的內容,例如:
* 真正的搶先式多任務處理,支持多用戶。
* 內存保護。
* 虛擬內存。
* 支持對稱多處理機SMP(symmetric multiprocessing),即多個CPU機器以及通常的單CPU(UP)機器。
* 符合POSIX標准。
* 聯網。
* 圖形用戶接口和桌面環境(實際上桌面環境並不只一個)。
* 速度和穩定性。
嚴格說來,Linux並不是一個完整的操作系統。當我們在安裝通常所說的Linux時,我們實際安裝的是很多工具的集合。這些工具協同工作以組成一個功能強大的實用系統。Linux本身只是這個操作系統的內核,是操作系統的心髒、靈魂、指揮中心(整個系統應該稱為GNU/Linux,其原因在本章的後續內容中將會給以介紹)。內核以獨占的方式執行最底層任務,保證系統正常運行—協調多個並發進程,管理進程使用的內存,使它們相互之間不產生沖突,滿足進程訪問磁盤的請求等等。
在本書中,我們給大家揭示的就是Linux是如何完成這一具有挑戰性的工作的。
1.1 Linux和Unix的簡明歷史
為了讓大家對本書所討論的內容有更清楚的了解,讓我們先來簡要回顧一下Linux的歷史。由於Linux是在Unix的基礎上發展而來的,我們的話題就從Unix開始。
Unix是由AT&T貝爾實驗室的Ken Thompson和Dennis Ritchie於1969年在一台已經廢棄了的PDP-7上開發的;它最初是一個用匯編語言寫成的單用戶操作系統。不久,Thompson和Ritchie成功地說服管理部門為他們購買更新的機器,以便該開發小組可以實現一個文本處理系統,Unix就在PDP-11上用C語言重新編寫(發明C語言的部分目的就在於此)。它果真變成了一個文本處理系統—不久之後。只不過問題是他們先實現了一個操作系統而已……
最終,他們實現了該文本處理工具,而且Unix(以及Unix上運行的工具)也在AT&T得到廣泛應用。在1973年,Thompson和Ritchie在一個操作系統會議上就這個系統發表了一篇論文,該論文引起了學術界對Unix系統的極大興趣。
由於1956年反托拉斯法案的限制,AT&T不能涉足計算機業務,但允許它象征性地收取費用發售該系統。就這樣,Unix被廣泛發布,首先是學術科研用戶,後來又擴展到政府和商業用戶。
伯克利加州大學是學術用戶中的一個。在這裡,Unix得到了計算機系統研究小組(CSRG)的廣泛應用。並且在這裡所進行的修改引發了Unix的一大系列,這就是廣為人知的伯克利軟件開發(BSD)Unix。除了AT&T所提供的Unix系列之外,BSD是最有影響力的Unix系列。BSD在Unix中增加了很多顯著特性,例如TCP/IP網絡,更好的用戶文件系統(UFS),工作控制,並且改進了AT&T的內存管理代碼。
多年以來,BSD版本的Unix一直在學術環境中占據主導地位,但最終發展成為System V版本的AT&T的Unix則成為商業領域的領頭羊。從某種程度上來說,這是有社會原因的:學校傾向於使用非正式但通常更好用的BSD風格的Unix,而商業界則傾向於從AT&T獲取Unix。
在用戶需求和用戶編程改進特性的促進下,BSD風格的Unix一般要比AT&T的Unix更具有創新性,而且改進也更為迅速。但是,在AT&T發布最後一個正式版本System V Release 4(SVR4)時,System V Unix已經吸收了BSD的大多數重要的優點,並且還增加了一些自己的優勢。這部分由於從1984年開始,AT&T逐漸可以將Unix商業化,而伯克利Unix的開發工作在1993年BSD4.4版本完成以後就逐漸收縮,以至終止了。然而,BSD的進一步改進由外界開發者延續下來,到今天還在繼續進行。正在進行的Unix系列開發中至少有四個獨立的版本是直接起源於BSD4.4,這還不包括幾個廠商的Unix版本,例如惠普的HP-UX,都是部分地或者全部基於BSD而發展起來的。
實際上Unix的變種並不止BSD和System V。由於Unix主要使用C語言來編寫,這就使得它移植到新的機器上相對比較容易,它的簡單性也使其重新設計與開發相對比較容易。Unix的這些特點大受商業界硬件供應商的歡迎,比如Sun、SGI、HP、IBM、DEC、Amdahl等等;IBM還不止一次對Unix進行了再開發。廠商們設計開發出新的硬件,並簡單地將Unix移植到新的硬件上,這樣新的硬件一經發布便具備一定的功能。經過一段時間之後,這些廠商都擁有了自己的專有Unix版本。而且為了占有市場,這些版本故意以不同的側重點發布出來,以更好地占有用戶。
版本混亂的狀態促進了標准化工作的進行。其中最主要的就是POSIX系列標准,它定義了一套標准的操作系統接口和工具。從理論上說,POSIX標准代碼很容易移植到任何遵守POSIX標准的操作系統中,而且嚴格的POSIX測試已經把這種理論上的可移植性轉化為現實。直到今天,幾乎所有的正式操作系統都以支持POSIX標准為目標。
現在讓我們回顧一下,在1984年,傑出的電腦黑客Richard Stallman獨立開發出一個類Unix的操作系統,該操作系統具有完全的內核、開發工具和終端用戶應用程序。在GNU(“GNU誷 Not Unix”首字母的縮寫)計劃的配合下,Stallman開發這個產品有自己的技術理想:他想開發出一個質量高而且自由的操作系統。Stallman使用了“自由”(free)這個詞,不僅意味著用戶可以免費獲取軟件;而且更重要的是,它將意味著某種程度的“解放”:用戶可以自由使用、拷貝、查詢、重用、修改甚至是分發這份軟件,完全沒有軟件使用協議的限制。這也正是Stallman創建自由軟件基金會(FSF)資助GNU軟件開發的本意(FSF也在資助其他科研方面的開發工作)。
15年來,GNU工程已經吸收、產生了大量的程序,這不僅包括Emacs、gcc(GNU的C編譯器)、bash(shell命令),還有大部分Linux用戶所熟知的許多應用程序。現在正在進行開發的項目是GNU Hurd內核,這是GNU操作系統的最後一個主要部件(實際上Hurd內核早已能夠使用了,不過當前的版本號為0.3的系統在什麼時候能夠完成,還是未知數)。
盡管Linux大受歡迎,但是Hurd內核還在繼續開發。原因有幾個方面,其一是Hurd的體系結構十分清晰地體現了Stallman關於操作系統工作方式的思想,例如,在運行期間,任何用戶都可以部分地改變或替換Hurd(這種替換不是對每個用戶都是可見的,而是只對申請修改的用戶可見,而且還必須符合安全規范)。另一個原因是據介紹Hurd對於多處理器的支持比Linux本身的內核要好。還有一個簡單的原因是興趣的驅動,因為程序員們希望能夠自由地進行自己所喜歡的工作。只要有人希望為Hurd工作,Hurd的開發就不會停止。如果他們能夠如願以償,Hurd有朝一日將成為Linux的強勁對手。不過在今天,Linux還是自由內核王國裡無可爭議的統治者。
在GNU發展的中期,也就是1991年,一個名叫Linus Torvalds的芬蘭大學生想要了解Intel的新CPU—80386。他認為比較好的學習方法是自己編寫一個操作系統的內核。出於這種目的,加上他對當時Unix變種版本對於80386類機器的脆弱支持十分不滿,他決定要開發出一個全功能的、支持POSIX標准的、類Unix的操作系統內核,該系統吸收了BSD和System V的優點,同時摒棄了它們的缺點。Linus(雖然我知道我應該稱他為Torvalds,但是所有人都稱他為Linus)獨立把這個內核開發到0.02版,這個版本已經可以運行gcc、bash和很少的一些應用程序。這些就是他開始的全部工作了。後來,他又開始在因特網上尋求廣泛的幫助。
不到三年,Linus的Unix—Linux,已經升級到1.0版本。它的源代碼量也呈指數形式增長,實現了基本的TCP/IP功能(網絡部分的代碼後來重寫過,而且還可能會再次重寫)。此時Linux就已經擁有大約10萬用戶了。
現在的Linux內核由150多萬行代碼組成,Linux也已經擁有了大約1000萬用戶(由於Linux可以自由獲取和拷貝,獲取具體的統計數字是不可能的)。Linux內核GNU/Linux附同GNU工具已經占據了Unix 50%的市場。一些公司正在把內核和一些應用程序同安裝軟件打包在一起,生產出Linux的發行版本,這些公司包括Red Hat和Caldera 公司。現在的GNU/Linux已經備受矚目,得到了諸如Sun、IBM、SGI等公司的廣泛支持。SGI最近決定在其基於Intel的Merced的系列機器上不再搭載自己的Unix變種版本IRIX,而是直接采用GNU/Linux;Linux甚至被指定為Amiga將要發布的新操作系統的基礎。
1.2 GNU通用公共許可證
這樣一個如此流行的操作系統當然值得我們學習。按照通用公共許可證(GPL,General Public License)的規定,Linux的源代碼可以自由獲取,這滿足了我們學習該系統的強烈願望。GPL這份非同尋常的軟件許可證,充分體現了上面提到的Stallman的思想:只要用戶所做的修改是同等自由的,用戶可以自由地使用、拷貝、查詢、重用、修改甚至重新發布這個軟件。通過這種方式,GPL保證了Linux(以及同一許可證保證下的大量其他軟件)不僅現在自由可用,而且以後經過任何修改之後都仍然可以自由使用。
請注意這裡的自由並不是說沒有人靠這個軟件盈利,有一些日益興起的公司,比如發行最流行的Linux發行版本的Red Hat就是一個例子(Red Hat自從上市以來,市值已經突破數十億美元,每年盈利數十萬美元,而且這些數字還在不斷增長)。但是任何人都不能限制其他用戶涉足本軟件領域,而且所做的修改不能減少其自由程度。
本書的附錄B中收錄了GNU通用公共許可證協議的全文。
1.3 Linux開發過程
如上所述,由於Linux是一個自由軟件,它可以免費獲取以供學習研究。Linux之所以值得學習研究,是因為它是相當優秀的操作系統。如果Linux操作系統相當糟糕,那它就根本不值得我們使用,也就沒有必要去研究相關的書籍。Linux是一個十分優秀的操作系統還在於幾個相互關聯的原因。
原因之一在於它是基於天才的思想開發而成的。在學生時代就開始推動整個系統開發的Linus Torvalds是一個天才,他的才能不僅展現在編程能力方面,而且組織技巧也相當傑出。Linux的內核是由世界上一些最優秀的程序員開發並不斷完善的,他們通過Internet相互協作,開發理想的操作系統;他們享受著工作中的樂趣,而且也獲得了充分的自豪感。
Linux優秀的另外一個原因在於它是基於一組優秀的概念。Unix是一個簡單卻非常優秀的模型。在Linux創建之前,Unix已經有20年的發展歷史。Linux從Unix的各個流派中不斷吸取成功經驗,模仿Unix的優點,拋棄Unix的缺點。這樣做的結果是Linux 成為了Unix系列中的佼佼者:高速、健壯、完整,而且拋棄了歷史包袱。
然而,Linux最強大的生命力還在於其公開的開發過程。每個人都可以自由獲取內核源程序,每個人都可以對源程序加以修改,而後他人也可以自由獲取你修改後的源程序。如果你發現了缺陷,你可以對它進行修正,而不用去乞求不知名的公司來為你修正。如果你有什麼最優化或者新特點的創意,你也可以直接在系統中增加功能,而不用向操作系統供應商解釋你的想法,指望他們將來會增加相應的功能。當發現一個安全漏洞後,你可以通過編程來彌補這個漏洞,而不用關閉系統直到你的供應商為你提供修補程序。由於你擁有直接訪問源代碼的能力,你也可以直接閱讀代碼來尋找缺陷,或是效率不高的代碼,或是安全漏洞,以防患於未然。
除非你是一個程序員,否則這一點聽起來仿佛沒有多少吸引力。實際上,即使你不是程序員,這種開發模型也將使你受益匪淺,這主要體現在以下兩個方面:
* 可以間接受益於世界各地成千上萬的程序員隨時進行的改進工作。
* 如果你需要對系統進行修改,你可以雇用程序員為你完成工作。這部分人將根據你的需求定義單獨為你服務。可以設想,這在源程序不公開的操作系統中將是什麼樣子。
Linux這種獨特的自由流暢的開發模型已被命名為bazaar(集市模型),它是相對於cathedral(教堂)模型而言的。在cathedral模型中,源程序代碼被鎖定在一個保密的小范圍內。只有開發者(很多情況下是市場)認為能夠發行一個新版本,這個新版本才會被推向市場。這些術語在Eric S. Raymond的《教堂與集市》(The Cathedral and the Bazaar)一文中有所介紹,大家可以在http://www.tuxedo.org/~esr/writings/找到這篇文章。bazaar開發模型通過重視實驗,征集並充分利用早期的反饋,對巨大數量的腦力資源進行平衡配置,可以開發出更優秀的軟件。(順便說一下,雖然Linux是最為明顯的使用bazaar開發模型的例子,但是它卻遠不是第一個使用這個模型的系統。)
為了確保這些無序的開發過程能夠有序地進行,Linux采用了雙樹系統。一個樹是穩定樹(stable tree),另一個樹是非穩定樹(unstable tree)或者開發樹(development tree)。一些新特性、實驗性改進等都將首先在開發樹中進行。如果在開發樹中所做的改進也可以應用於穩定樹,那麼在開發樹中經過測試以後,在穩定樹中將進行相同的改進。按照Linus的觀點,一旦開發樹經過了足夠的發展,開發樹就會成為新的穩定樹,如此周而復始的進行下去。
源程序版本號的形式為x.y.z。對於穩定樹來說,y是偶數;對於開發樹來說,y比相應的穩定樹大一(因此,是奇數)。截至到本書截稿時,最新的穩定內核版本號是2.2.10,最新的開發內核的版本號是2.3.12。對2.3樹的缺陷修正會回溯影響(back-propagated)2.2樹,而當2.3樹足夠成熟的時候會發展成為2.4.0。(順便說一下,這種開發會比常規慣例要快,因為每一版本所包含的改變比以前更少了,內核開發人員只需花很短的時間就能夠完成一個實驗開發周期。) http://www.kernel.org及其鏡像站點提供了最新的可供下載的內核版本,而且同時包括穩定和開發版本。如果你願意的話,不需要很長時間,這些站點所提供的最新版本中就可能包含了你的一部分源程序代碼。
第2章 代 碼 初 識
本章首先從較高層次介紹Linux內核源程序的概況,這些都是大家關心的一些基本特點。隨後將簡要介紹一些實際代碼。最後介紹如何編譯內核。
2.1 Linux內核源程序的部分特點
在過去的一段時期,Linux內核同時使用C語言和匯編語言來實現。這兩種語言需要一定的平衡:C語言編寫的代碼移植性較好、易於維護,而匯編語言編寫的程序則速度較快。一般只有在速度是關鍵因素或者一些因平台相關特性而產生的特殊要求(例如直接和內存管理硬件進行通訊)時才使用匯編語言。
正如實際中所做的,即使內核並未使用C++的對象特性,部分內核也可以在g++(GNU的C++編譯器)下進行編譯。同其他面向對象的編程語言相比較,相對而言C++的開銷是較低的,但是對於內核開發人員來說,這已經是太多了。
內核開發人員不斷發展編程風格,形成了Linux代碼獨有的特色。本節將討論其中的一些問題。
2.1.1 gcc特性的使用
Linux內核被設計為必須使用GNU的C編譯器gcc來編譯,而不是任何一種C編譯器都可以使用。內核代碼有時要使用gcc特性,本書將陸續介紹其中的一部分。
一些gcc特有代碼只是簡單地使用gcc語言擴展,例如允許在C(不只是C++)中使用inline關鍵字指示內聯函數。也就是說,代碼中被調用的函數在每次函數調用時都會被擴充,因而就可以節約實際函數調用的開銷。
一般情況下,代碼的編寫方式比較復雜。因為對於某些類型的輸入,gcc能夠產生比其他輸入效率更高的執行代碼。從理論上講,編譯器可以優化具有相同功能的兩種對等的方法,並且得到相同的結果。因此,代碼的編寫方式是無關緊要的。但在實際上,用某種方法編寫所產生的代碼要比用另外一些方法編寫所產生的代碼執行速度快許多。內核開發人員知道怎樣才能產生更高效的執行代碼,這不斷地在他們編寫的代碼中反映出來。
例如,考慮內核中經常使用的goto語句—為了提高速度,內核中經常大量使用這種一般要避免使用的語句。在本書中所包含的不到40 000行代碼中,一共有500多條goto語句,大約是每80行一個。除匯編文件外,精確的統計數字是接近每72行一個goto語句。公平地說,這是選擇偏向的結果:比例如此高的原因之一是本書中涉及的是內核源程序的核心,在這裡速度比其他因素都需要優先考慮。整個內核的比例大概是每260行一個goto語句。然而,這仍然是我不再使用Basic進行編程以來見過的使用goto頻率最高的地方。
代碼必需受特定編譯器限制的特性不僅與普通應用程序的開發有很大不同,而且也不同於大多數內核的開發。大多數的開發人員使用C語言編寫代碼來保持較高的可移植性,即使在編寫操作系統時也是如此。這樣做的優點是顯而易見的,最為重要的一點是一旦出現更好的編譯器,程序員們可以隨時進行更換。
內核對於gcc特性的完全依賴使得內核向新的編譯器上移植更加困難。最近Linus對這一問題在有關內核的郵件列表上表明了自己的觀點:“記住,編譯器只是一個工具。”這是對依賴於gcc特性的一個很好的基本思想的表述:編譯器只是為了完成工作。如果通過遵守標准還不能達到工作要求,那麼就不是工作要求有問題,而是對於標准的依賴有問題。
在大多數情況下,這種觀點是不能被人所接受的。通常情況下,為了保證和程序語言標准的一致,開發人員可能需要犧牲某些特性、速度或者其他相關因素。其他的選擇可能會為後期開發造成很大的麻煩。
但是,在這種特定的情況下,Linus是正確的。Linux內核是一個特例,因為其執行速度要比向其他編譯器的可移植性遠為重要。如果設計目標是編寫一個可移植性好而不要求快速運行的內核,或者是編寫一個任何人都可以使用自己喜歡的編譯器進行編譯的內核,那麼結論就可能會有所不同了;而這些恰好不是Linux的設計目標。實際上,gcc幾乎可以為所有能夠運行Linux的CPU生成代碼,因此,對於gcc的依賴並不是可移植性的嚴重障礙。
在第3章中我們將對內核設計目標進行詳細說明。
2.1.2 內核代碼習慣用語
內核代碼中使用了一些顯著的習慣用語,本節將介紹常用的幾個。當通讀源代碼時,真正重要的問題並不在這些習慣用語本身,而是這種類型的習慣用語的確存在,而且是不斷被使用和發展的。如果你需要編寫內核代碼,你應該注意到內核中所使用的習慣用語,並把這些習慣用語應用到你的代碼中。當通讀本書(或者代碼)時,看看你還能找到多少習慣用語。
為了討論這些習慣用語,我們首先需要對它們進行命名。為了便於討論,筆者創造了這些名字。而在實際中,大家不一定非要參考這些用語,它們只是對內核工作方式的描述而已。
一個普通的習慣用語,筆者稱之為“資源獲取”(resource acquisition idiom)。在這個用語中,一個函數必須實現一系列資源的獲取,包括內存、鎖等等(這些資源的類型未必相同)。只有成功地獲取當前所需要的資源之後,才能處理後面的資源請求。最後,該函數還必須釋放所有已經獲取的資源,而不必考慮沒有獲取的資源。
我采用“錯誤變量”這一用語(error variable idiom)來輔助說明資源獲取用語,它使用一個臨時變量來記錄函數的期望返回值。當然,相當多的函數都能實現這個功能。但是錯誤變量的不同點在於它通常是用來處理由於速度的因素而變得非常復雜的流程控制中的問題。錯誤變量有兩個典型的值,0(表示成功)和負數(表示有錯)。
如果執行到標號out2,則都已經獲取了r1和r2資源,而且也都需要進行釋放。如果執行到標號out1(不管是順序執行還是使用goto語句進行跳轉到),則r2資源是無效的(也可能剛被釋放),但是r1資源卻是有效的,而且必需在此將其釋放。同理,如果標號out能被執行,則r1和r2資源都無效,err所返回的是錯誤或成功標志。
在這個簡單的例子中,對err的一些賦值是沒有必要的。在實踐中,實際代碼必須遵守這種模式。這樣做的原因主要在於同一行中可能包含有多種測試,而這些測試應該返回相同的錯誤代碼,因此對錯誤變量統一賦值要比多次賦值更為簡單。雖然在這個例子中對於這種屬性的必要性並不非常迫切,但是我還是傾向於保留這種特點。有關的實際應用可以參考sys_shmctl(第21654行),在第9章中還將詳細介紹這個例子。
2.1.3 減少#if和#ifdef的使用
現在的Linux內核已經移植到不同的平台上,但是我們還必須解決移植過程中所出現的問題。大部分支持各種不同平台的代碼由於包含許多預處理代碼而已經變得非常不規范,例如:
這個例子試圖實現操作系統的可移植性,雖然Linux關注的焦點很明顯是實現代碼在各種CPU上的可移植性,但是二者的基本原理是一致的。對於這類問題來說,預處理器是一種錯誤的解決方式。這些雜亂的問題使得代碼晦澀難懂。更為糟糕的是,增加對新平台的支持有可能要求重新遍歷這些雜亂分布的低質量代碼段(實際上你很難能找到這類代碼段的全部)。
與現有方式不同的是,Linux一般通過簡單函數(或者是宏)調用來抽象出不同平台間的差異。內核的移植可以通過實現適合於相應平台的函數(或宏)來實現。這樣不僅使代碼的主體簡單易懂,而且在移植的過程中還可以比較容易地自動檢測出你沒有注意到的內容:如引用未聲明函數時會出現鏈接錯誤。有時用預處理器來支持不同的體系結構,但這種方式並不常用,而相對於代碼風格的變化就更是微不足道了。
順便說一下,我們可以注意到這種解決方法和使用用戶對象(或者C語言中充滿函數指針的strUCt結構)來代替離散的switch語句處理不同類型的方法十分相似。在某些層次上,這些問題和解決方法是統一的。
可移植性的問題並不僅限於平台和CPU的移植,編譯器也是一個重要的問題。此處為了簡化,假設Linux只使用gcc來編譯。由於Linux只使用同一個編譯器,所以就沒有必要使用#if塊(或者#ifdef塊)來選擇不同的編譯器。
內核代碼主要使用#ifdef來區分需要編譯或不需要編譯的部分,從而對不同的結構提供支持。例如,代碼經常測試SMP宏是否定義過,從而決定是否支持SMP機。
2.2 代碼樣例
了解Linux代碼風格最好的方法就是實際研究一下它的部分代碼。即使你不完全理解本節所討論代碼的細節也無關緊要,畢竟本節的主要目的不是理解代碼,一些讀者可以只對本節進行浏覽。本節的主要目的是讓讀者對Linux代碼進行初步了解,為今後的工作提供必要基礎。該討論將涉及部分廣泛使用的內核代碼。
2.2.1 printk
printk(25836行)是內核內部消息日志記錄函數。在出現諸如內核檢測到其數據結構出現不一致的事件時,內核會使用printk把相關信息打印到系統控制台上。對於printk的調用一般分為如下幾類:
* 緊急事件(emergency)—例如,panic函數(25563行)多次使用了printk。當內核檢測到發生不可恢復的內部錯誤時就會調用panic函數,然後盡其所能地安全關閉計算機。這個函數中調用printk以提示用戶系統將要關閉。
* 調試—從3816行開始的#ifdef塊使用printk來打印SMP邏輯單元(box)中每一個處理器的相關配置信息,但是此過程只有在使用SMP_DEBUG標志編譯代碼的情況下才能夠被執行。
* 普通信息—例如,當機器啟動時,內核必須估計系統速度以確保設備驅動程序能夠忙等待(busy-wait)一個精確的極短周期。計算這種估計值的函數名為calibrate_delay(19654行),它既在19661行使用printk聲明馬上開始計算,又在19693行報告計算結果。另外,在第4章將詳細的介紹calibrate_delay函數。
如果你已經浏覽過這些參照行,你可能已經注意到printk和printf的參數十分類似:一個格式化字符串,後跟零個或者多個參數加入字符串中。格式化字符串可能是以一組“”開始,這裡的N是從0到7的數字,包括0和7在內。數字區分了消息的日志等級(log level),只有當日志等級高於當前控制台定義的日志等級(console_loglevel,25650行)時,才會打印消息。root可以通過適當減小控制台的日志等級來過濾不是很緊急的消息。如果內核在格式化字符串中檢測不到日志等級序列,那麼就會一直打印消息(實際上,日志等級序列並不一定要在格式化字符串中出現,可以在格式化文本中查找到它的代碼)。
從14946行開始的#define塊說明了這些特殊序列,這些定義可以幫助調用者正確區分對printk的調用。簡單地說,我稱日志等級0到4為“緊急事件”,等級5到等級6為“普通信息”,等級7自然就是我所說的“調試”(這種分類方法並不意味著其他更好的分類方法沒有用處,而只是目前我們還不關心它而已)。
在上面討論的基礎上,我們研究一下代碼本身。
printk
25836:參數fmt是printf類型的格式化字符串。如果你對“...”部分的內容不熟悉,那就 需要參閱一本好的C語言參考書(在其索引中查找“變參函數,variadic function”)。另外,在安裝的GNU/Linux中的stdarg幫助裡也包含了一個有關變參函數的簡明描述,在這兒只需要敲入“man stdarg”就可以看到。
簡單地說,“...”部分提示編譯器fmt後面可能緊跟著數量不定的任何類型的參數。由於這些參數在編譯的時候還沒有類型和名字,內核使用由三個宏va_start、va_arg和va_end組成的特殊組及一個特殊類型—va_list對它們進行處理。
25842:msg_level記錄了當前消息的日志等級。它是靜態的,這看起來可能會有些奇怪—為什麼下一次對printk的調用需要記錄日志等級呢?問題的答案是只有打印出新行(\n)或者賦給一個新的日志等級序列以後,當前消息才會結束。這樣,通過在包含消息結束的新行裡調用printk,就保證了在多個短期沖突的情況下,調用者只打印唯一一個長消息。
25845:在SMP邏輯單元中,內核可能試圖從不同的CPU向控制台同時打印信息(有時在單處理機(UP)邏輯單元中也會發生同樣問題,但由於中斷還未被覆蓋掉,所以問題也並不十分明顯)。如果不進行任何協同的話,結果就將處於完全無法讓人了解的雜亂無章的狀態,每個消息的各個部分都和其他消息的各個部分混雜交織在一起。
相反,內核使用旋轉鎖(spin-lock)來控制對控制台的訪問。旋轉鎖將在第10章進行深入介紹。
如果你對flags 在傳送給spin_lock_irqsave之前為什麼不對它初始化感到疑惑,請不要擔心:spin_lock_irqsave(對於不同的版本請分別參看12614行,12637行,12716行和12837行)是一個宏,而不是一個函數。該宏實際上是將值寫入flags中,而不是從flags中讀出值(在25895行中,存儲在flags中的信息被spin_unlock_irqrestore回讀,請參看12616行,12639行,12728行和12841行)。
25846:初始化變量args,該變量代表printk參數中的“...”部分。
25848:調用內核自身的vsprintf(為節省空間而省略)實現。該函數的功能與標准vsprintf函數非常相似,向buf中寫入格式化文本(25634行)並返回寫入字符串的長度(長度不包括最後一位終止字符0字節)。很快,你將可以看到為什麼這種機制會忽略buf的前三個字符。
(正如25847行的注釋中所述)我們應該注意到在這裡並沒有采取嚴格的措施來保證緩沖器不會過載。這裡系統假定1024個字符長度的buf已經足夠使用(參閱25634行)。如果內核在這裡能夠使用vsnprintf函數的話,情況就會好許多。然而,vsnprintf還有另外一個參數限制了它能夠寫入緩沖器的字符長度。
25849:計算buf中最近使用的元素,調用va_end終止對“...”參數的處理。
25851:開始格式化消息的循環。其中存在一個內部循環能夠處理更多內容(這一點隨後就能看到),因此,每次內循環開始,都開始一個新的打印行。由於通常情況下printk只用於打印單行,所以在每次調用中,這種循環通常只執行一次。
25853:如果預先不知道消息的日志等級,printk會檢查當前行是否以日志等級序列開頭。
25860:如果不是,buf中開始未使用的三個字符就能夠起作用了(第一次以後的每次循環,都會覆蓋部分消息文本,但是這樣並不會引起問題,因為這裡的文本只是前面行中的一部分,它們已經被打印過,而且以後也不再需要了)。這樣,就可以將日志等級插入buf中。
25866:此處有如下屬性:p指向日志等級序列(消息文本緊隨其後),msg指向消息文本—請注意25852行和25865行中對msg的賦值。
由於已知p用來指示日志等級序列的開頭—該日志等級序列可能是由函數自身所創建的,日志等級可以從p中抽出並存到msg_level中。
25868:沒有檢測到新行,清空line_feed標志。
25869:這是前面談到過的內循環,循環將運行到本行結束(也就是檢測到新行標志)或者緩沖器的末尾為止。
25870:除了將消息打印到控制台之外,printk還能夠記錄最近打印的長度為LOG_ BUF_LEN的字符組(LOG_BUF_LEN為16K,請參看25632行)。如果在控制台打開之前,內核就已經調用printk,則顯然不能在控制台上正確打印消息,但是這些消息將被盡可能地存儲到log_buf中(25656行)。當控制台打開以後,緩存在log_buf中的數據就可以轉儲並在控制台上打印出來,請參看25988行。
log_buf是一個循環緩沖器,log_start和log_size變量(25657行和25646行)分別記錄當前緩沖器的開始位置和長度。本行中的按位與(AND)操作實際上是快速求模(%)運算,它的正確性依賴於LOG_BUF_LEN的值是2的冪。
25872:保存變量跟蹤記錄循環日志的值。顯然,日志大小會不斷增長,直至達到LOG_BUF_LEN的值為止。此後,log_size將保持不變,而插入新字符將導致log_start的增長。
25878:請注意logged_chars(25658行)記錄從機器啟動之後由printk寫入的所有字符的長度,它在每次循環中都會被更新,而不是在循環結束後才改變一次。基於同樣的道理,log_start和log_size的處理方式也是一樣。這實際上是一種優化的時機,本書將在結束對函數的介紹之後再對它進行詳細討論。
25879:消息被分為若干行,這當然要使用新行標志符來進行分割。一旦內核檢測到新行標志符,就寫入一個完整行,從而內循環的執行也可以提前終止。
25884:在這裡我們先不考慮內部循環是否會提前退出,從msg到p的字符序列是專門提供給控制台使用的(這種字符序列我稱之為行,但是不要忘了,這裡的行可能並不意味著新行終止,因為buf也許還沒有終止)。如果該行的日志等級高於系統控制台定義的日志等級,而且當前又有控制台可供打印,那麼就能夠正確打印該行。(記住,printk可能在所有控制台打開之前就已經被調用過了。)
如果在該消息塊中沒有發現日志等級序列,並且在前面的printk調用中也沒有對msg_level賦值,那麼本行中的msg_level就是-1。由於console_loglevel總不小於1(除非root通過sysctl接口鎖定),於是總是可以打印這些行。
25886:本行應該能夠被打印。printk通過遍歷打開的控制台驅動鏈表告知每一個控制台驅動去打印當前行設備驅動在本書的討論范圍之外,因此,控制台驅動代碼則並不包含在內)。
25888:請注意這裡消息文本的開頭使用的是msg而不是p,這樣就在沒有日志等級序列的情況下寫入消息了。然而,日志等級序列已經被存儲到log_buf緩沖器中了。這樣就使後來能夠訪問log_buf以獲取消息日志等級的代碼(請參看25998行),不會再產生顯示混亂信息序列的現象。
25892:如果內層for循環發現一新行,那麼buf中的剩余字符(如果有的話)將被認為是新的消息,因此msg_level會被重置。但是無論怎樣,外層循環都會持續到buf清空為止。
25895:釋放在25845行獲取的控制台鎖(console lock)。
25896:喚醒等待被寫入控制台日志的所有進程。注意即使沒有文本被實際寫入任何控制台,這個過程也仍然會發生。這樣處理是正確的,因為無論是否要往控制台中寫入文本,等待進程實際上都是在等待從log_buf中讀出信息。在25748行,進程被轉入休眠狀態以等待log_buf的活動。在休眠、喚醒和等待隊列中所使用的機制將在下一節中進行討論。
25897:返回日志中寫入的字符長度。
如果對於每個字符的處理工作都能減少一點,那麼從25869行開始的for循環就執行得更快一點。當循環存在時,我們可以通過只在循環退出時將logged_chars更新一次來稍微提高運行速度。然而我們還可以通過其他努力來提高速度。由於我們可以預知消息的長度,因此log_size和log_start可以到最後再增長。讓我們來實驗一下這樣能否提高速度,下面是一段經過理想優化的代碼:
請注意循環通常只需要執行一次,只有在log_buf末尾寫入信息需要折行時才會多次執行。因而log_size和log_buf只需要更新一次(或者當寫入需要換行時是兩次)。
這時速度的確提高了,但是有兩個原因使我們並不能這樣做。首先,內核可能有自己特有的memcpy函數,我們必須確保對memcpy的調用不會再次進入對printk的調用(有一部分內核移植版定義了自己特有的速度較快的memcpy函數版本,因此所有的移植都要在這一點上保持一致)。如果memecpy調用printk來報告失敗,那麼就有可能觸發無限循環。
然而在這一點上也並不是真的無藥可救。使用這種解決方案的最大問題在於該內核循環的形式中也要留意新行標志符,因此使用memcpy將整個消息拷貝到log_buf中是不正確的:如果此處存在新行,我們將無法對其進行處理。
我們可以試驗一個一箭雙雕的辦法。下面這種替代的嘗試雖然可能比前面那種初步解決方法速度要慢,但是它保持了內核版本的語意:
(請注意gcc的優化器十分靈敏,它足以能檢測到循環內部的表達式log_buf+LOG_BUF_LEN並沒有改變,因此在上面的循環中試圖手工加速計算是沒有任何效果的。)
不幸的是,這種方法並不能比現在的內核版本在速度上快許多,而且那樣會使得代碼晦澀難懂(如果你編寫過更新log_size和log_start的代碼,你就能清楚地了解這一點)。你可以自己決定這種折衷是否值得。然而無論怎樣,我們學到了一些東西,通常,不管成功與否,改進內核代碼都可以加深你對內核工作原理的理解。
2.2.2 等待隊列
前一節我們曾簡要的提到進程(也就是正在運行的程序)可以轉入休眠狀態以等待某個特定事件,當該事件發生時這些進程能夠被再次喚醒。內核實現這一功能的技術要點是把等待隊列(wait queue)和每一個事件聯系起來。需要等待事件的進程在轉入休眠狀態後插入到隊列中。當事件發生之後,內核遍歷相應隊列,喚醒休眠的任務讓它投入運行狀態。任務負責將自己從等待隊列中清除。
等待隊列的功能強大得令人吃驚,它們被廣泛應用於整個內核中。更重要的是,實現等待隊列的代碼量並不大。
1. wait_queue結構
18662:簡單的數據結構就是等待隊列節點,它包含兩個元素:
* task—指向struct task_struct結構的指針,它代表一個進程。從16325行開始的struct task_struct結構將在第7章中進行介紹。
* next—指向隊列中下一節點的指針。因而,等待隊列實際上是一個單鏈表。
通常,我們用指向等待隊列隊首的指針來表示等待隊列。例如,printk使用的等待隊列log_wait(25647行)。
2. wait_event
16840:通過使用這個宏,內核代碼能夠使當前執行的進程在等待隊列wq中等待直至給定condition(可能是任何的表達式)得到滿足。
16842:如果條件已經為真,當前進程顯然也就無需等待了。
16844:否則,進程必須等待給定條件轉變為真。這可以通過調用__wait_event來實現(16824行),我們將在下一節介紹它。由於__wait_event已經同wait_event分離,已知條件為假的部分內核代碼可以直接調用__wait_queue,而不用通過宏來進行冗余的(特別是在這些情況下)測試,實際上也沒有代碼會真正這樣處理。更為重要的是,如果條件已經為真,wait_event會跳過將進程插入等待隊列的代碼。
注意wait_event的主體是用一個比較特殊的結構封閉起來的:
奇怪的是,這個小技巧並沒有得到應有的重視。這裡的主要思路是使被封閉的代碼能夠像一個單句一樣使用。考慮下面這個宏,該宏的目的是如果p是一個非空指針,則調用free:
除非你在如下所述的情況下使用FREE1,否則所有調用都是正確有效的:
FREE1經擴展以後,else就和錯誤的if(FREE1的if)聯系在一起。
有些程序員通過如下途徑解決這種問題:
這兩種方法都不盡人意,程序員在調用宏以後自然而然使用的分號會把擴展信息弄亂。以FREE2為例,在宏展開之後,為了使編譯器能更准確地識別,我們還需要進行一定的縮進調節,最終代碼如下所示:
這樣就會引起語法錯誤—else和任何一個if都不匹配。FREE3從本質上講也存在同樣的問題。而且在研究問題產生原因的同時,就能夠明白為什麼宏體裡是否包含if是無關緊要的。不管宏體內部內容如何,只要使用一組括號來指定宏體,就會碰到相同的問題。
引入do/while(0)技巧能夠克服前面所出現的所有問題,現在我們可以編寫FREE4。
將FREE4和其他宏一樣插入相同代碼之後,這段代碼當然可以正確執行。編譯器能夠優化這個偽循環,捨棄循環控制,因此執行代碼並沒有速度的損失,我們也從而得到了能夠實現理想功能的宏。
雖然這是一個可以接受的解決方案,但是我們不能不提到的是編寫函數要比編寫宏好得多。不過如果你不能提供函數調用所需的開銷,那麼就需要使用內聯函數。這種情況雖然在內核中經常出現,但是在其他地方就要少得多。(不可否認,當使用C++、gcc或者任何實現了將要出現的修正版ISO標准C的編譯器時,這種方案只是一種選擇,就是最後為C增加內聯函數。)
3. __wait_event
16824:__wait_event使當前進程在等待隊列wq中等待,直至condition為真。
16829:通過調用add_wait_queue(16791行),局部變量__wait可以被鏈接到隊列上。注意__wait是在堆棧中而不是在內核堆中分配空間,這是內核中常用的一種技巧。在宏運行結束之前,__wait就已經被從等待隊列中移走了,因此等待隊列中指向它的指針總是有效的。
16830:重復分配CPU給另一個進程直至條件滿足,這一點將在下面幾節中討論。
16831:進程被置為TASK_UNINTERRUPTIBLE狀態(16190行)。這意味著進程處於休眠狀態,不應被喚醒,即使是信號也不能打斷該進程的休眠。信號在第6章中介紹,而進程狀態則在第7章中介紹。
16832:如果條件已經滿足,則可以退出循環。
請注意如果在第一次循環時條件就已經滿足,那麼前面一行的賦值就浪費了(因為在循環結束之後進程狀態會立刻被再次賦值)。__wait_event假定宏開始執行時條件還沒有得到滿足。而且,這種對進程狀態變量state的延遲賦值也並沒有什麼害處。在某些特殊情況下,這種方法還十分有益。例如當__wait_event開始執行時條件為假,但是在執行到16832行時就為真了。這種變化只有在為有關進程狀態的代碼計算condition變量值時才會出現問題。但是在代碼中這種情況我沒有發現。
16834:調用schedule(26686行,在第7章中討論)將CPU轉移給另一個進程。直到進程再次獲得CPU時,對schedule的調用才會返回。這種情況只有當等待隊列中的進程被喚醒時才會發生。
16836:進程已經退出了,因此條件必定已經得到了滿足。進程重置TASK_RUNNING的狀態(16188行),使其適合CPU運行。
16837:通過調用remove_wait_queue(16814行)將進程從等待隊列中移去。wait_event_interruptible和__wait_event_interruptible(分別參見16868行和16847)基本上與wait_event和__wait_event相同,但不同的是它們允許休眠的進程可以被信號中斷。信號將在第6章中介紹。
請注意wait_event是被如下結構所包含的。
和do/while(0)技巧一樣,這樣可以使被封閉起來的代碼能夠像一個單元一樣運行。這樣的封閉代碼就是一個獨立的表達式,而不是一個獨立的語句。也就是說,它可以求值以供其他更復雜的表達式使用。發生這種情況的原因主要在於一些不可移植的gcc特有代碼的存在。通過使用這類技巧,一個程序塊中的最後一個表達式的值將定義為整個程序塊的最終值。當在表達式中使用wait_event_interruptible時,執行宏體後賦__ret的值為宏體的值(參見16873行)。對於有Lisp背景知識的程序員來說,這是個很常見的概念。但是如果你僅僅了解一點C和其他一些相關的過程性程序設計語言,你可能就會覺得比較奇怪。
__wake_up
26829:該函數用來喚醒等待隊列中正在休眠的進程。它由wake_up和wake_up_ interruptible調用(請分別參見16612行和16614行)。這些宏提供mode參數,只有狀態滿足mode所包含的狀態之一的進程才可能被喚醒。
26833:正如將在第10章中詳細討論的那樣,鎖(lock)是用來限制對資源的訪問,這在SMP邏輯單元中尤其重要,因為在這種情況下當一個CPU在修改某數據結構時,另一個CPU可能正在從該數據結構中讀取數據,或者也有可能兩個CPU同時對同一個數據結構進行修改,等等。在這種情況下,受保護的資源顯然是等待隊列。非常有趣的是所有的等待隊列都使用同一個鎖來保護。雖然這種方法要比為每一個等待隊列定義一個新鎖簡單得多,但是這就意味著SMP邏輯單元可能經常會發現自己正在等待一個實際上並不必須的鎖。
26838:本段代碼遍歷非空隊列,為隊列中正確狀態的每一個進程調用wake_up_process(26356行)。如前所述,進程(隊列節點)在此可能並沒有從隊列中移走。這在很大程度上是由於即使隊列中的進程正在被喚醒,它仍然可能希望繼續存在於等待隊列中,這一點正如我們在__wait_event中發現的問題一樣。
2.2.3 內核模塊
整個內核並不需要同時裝入內存。應該確認,為保證系統能夠正常運行,一些特定的內核必須總是駐留在內存中,例如,進程調度代碼就必須常駐內存。但是內核其他部分,例如大部分的設備驅動就應該僅在內核需要的時候才裝載,而在其他情況下則無需占用內存。
舉例來說,只有在內核真正和CD-ROM通訊時才需要使用完成內核與CD-ROM通訊的設備驅動程序,因此內核可以被設置為在和設備通訊之前才裝載相應代碼。內核完成和設備的通訊之後可以將這部分代碼丟棄。也就是說,一旦代碼不再需要,就可以從內存中移走。系統運行過程中可以增減的這部分內核稱為內核模塊。
內核模塊的優點是可以簡化內核自身的開發。假設你購買了一個新的高速CD-ROM驅動器,但是現有的CD-ROM驅動程序並不支持該設備。你自然就希望增加對這種高速模式的支持以提高系統光驅設備的性能。如果作為內核模塊來編譯驅動程序,你的工作將會方便得多:編譯驅動程序、加載到內核、測試、卸載驅動程序、修改驅動程序、再次加載驅動程序到內核、測試,如此周而復始。如果你的驅動程序是直接編輯在內核中的,那麼你就必須重新編譯整個內核並且在每次修改驅動程序之後重新啟動機器。這樣慢得很多。
自然,你也必須留意內核模塊。對於指明其他內核模塊在磁盤上的駐留位置的那些模塊,一定不能從內存中卸載,否則,內核將只能通過訪問磁盤來裝載處理磁盤訪問的內核模塊,這是不可能實現的。這也是我們要選擇把部分內核作為模塊編譯還是直接編譯進內核使其常駐內存的又一個原因。知道自己系統的設置方式,因而也就可以選擇正確使用的方式(如果為了確保安全,可以簡單的忽略內核模塊系統的優點,而把所有的內容都編譯到內核裡面)。
內核模塊會帶來一些速度上的損失,這是因為一些必需的代碼現在並不在RAM中,必需要從磁盤讀入。但是整個系統的性能通常會有所提高,這主要是因為通過丟棄暫時不使用的模塊可以釋放出額外的RAM供應用程序使用。如果這部分內存被內核所占用,應用程序將只能更加頻繁地進行磁盤交換,而這種磁盤交換會顯著地降低應用程序的性能(磁盤交換將在第8章中討論)。
內核模塊還會帶來因復雜度的增加所造成的開銷,這是因為在系統運行的過程中,移進移出部分內核需要額外的代碼。然而,復雜度的開銷是可以管理的。通過使用外部程序來代理一些必需的工作還可以更進一步降低復雜度的開銷(更為確切的說法是,這樣做不是減少了復雜度的開銷,而是把復雜度的開銷重新分配了一下)。這是對內核模塊原理的一個小小的擴展:即使是內核的支持模塊,對於內核來說也只是外部的、部分可用的,只有在需要的時候才被裝入內存。
通常用於這種目的程序稱為modprobe。有關的modprobe代碼超出了本書的范圍,但是在Linux的每個發行版本中都包含有它。本節的剩余部分將討論同modprobe協同工作,以裝載內核模塊的內核代碼。
1. request_module
24432:作為函數說明之前的注釋,request_module是一個函數。內核的其他模塊在需要裝載其他內核模塊的時候,都必須調用這個函數。就像內核處理其他工作一樣,這種調用也是為當前運行的進程進行的。從進程的角度來看,這種調用的請求通常是隱含的—正在執行進程其他請求的內核可能會發現,必須調入一個模塊才能夠完成該請求。例如,請參見10070行,這裡是一些將在第7章中討論的代碼。
24446:以內核中的一個獨立進程的形式執行exec_modprobe函數(24384行)。這並不能只通過函數的簡單調用實現,因為exec_modprobe要繼續調用exec來執行一個程序。因此,對函數exec_modprobe的簡單調用將永遠不會有返回。
這和使用fork以准備exec調用十分類似,你可以認為kernel_thread對內核來說就是較低版本的fork,雖然兩者有很大不同。fork是從指定函數開始執行新的進程,而不是從調用者的當前位置開始運行。正如fork一樣,kernel_thread返回的值是新進程的進程號。
24448:和fork一樣,從kernel_thread返回的負值表示內部錯誤。
24455:正如函數中論述的一樣,大部分的信號將因當前進程而被暫時阻塞。
24462:等待exec_modprobe執行完畢,同時指出所需要的模塊是已經成功裝入內存,還是裝載失敗了。
24465:結束運行,恢復信號。如果exec_modprobe返回錯誤代碼,則打印錯誤消息。
2. exec_modprobe
24384:exec_modprobe運行為內核增加內核模塊的程序。這裡的模塊名是一個void*的指針,而不是char*的指針。原因簡單說來就是kernel_thread 產生的函數通常都使用void*指針參數。
24386:設置modprobe的參數列表和環境。modprobe_path(24363行)用來定位modprobe程序的位置。它可以通過內核的sysctl特性來修改,這一點將在第11章中介紹(參見30388行)。這意味著root可以動態選擇不同於/sbin/modprobe的程序來運行,以適應當modprobe被安裝到其他地方或者使用修改過的modprobe替換掉了原有的modprobe之類的情況。
24400:(正如代碼中描述的一樣)出於安全性考慮,丟棄所有掛起的信號和信號句柄(handl-ers)。這裡最重要的部分是對flush_signal_handlers的調用(28041行),它使用內核默認的信號句柄代替所有用戶定義的信號句柄。如果在此時有信號被傳送到內核,它將獲得默認響應—通常是忽略信號或殺死進程。但是不管怎樣都不會引起安全風險。由於該函數從觸發它的進程中分離出來(如前所述),所以,不管原始進程在此處是否改變其原來分配的信號,句柄都不會產生任何影響。
24405:關閉調用進程打開的所有文件。最重要的是,這意味著modprobe程序不再從調用進程中繼承標准輸入輸出和標准錯誤。這很有可能會引起安全漏洞(這可能是在替代modprobe的程序中引起的問題,但是modprobe本身實際上並不關心這個差異)。
24413:modprobe程序作為root運行,它擁有root所擁有的所有權限。和整個內核中其他地方一樣,請注意root使用用戶ID號0的假定在這裡已經被寫入程序。用戶ID號和權能系統(capability system,在接下來的幾行中會用到)將在第7章中介紹。
24421:試圖執行modprobe程序。如果嘗試失敗,內核將使用printk打印錯誤消息並返回錯誤代碼。這裡是可能產生printk的緩沖器過載的地點之一。module_name的長度並沒有明確限制,就我們對該調用的看法而言,它可能長達一百萬個字符。為防止printk緩沖器過載,你必需遍歷所有對於該函數的調用(實際上是對request_module的調用),以保證每個調用者使用足夠短的、不會為printk造成麻煩的模塊名。
24427:當execve成功執行時,它不會返回任何結果,因此本處是不可能執行到的。但是編譯器卻並不知道這一點,因此,此處使用了return語句以保證gcc不出錯。
對於內核的進一步討論將超出本章的既定范圍,因此在這個問題上我們到此為止。然而本書中也包括了其他必需的內核代碼。在讀完第4章和第5章之後,也許你會希望再次仔細研讀一下這部分內容。有關這個問題的兩個文件是include/linux/module.h(從15529行開始)和/kernel/module.c(從24476行開始)。和sys_create_module(24586行)、sys_init_module(24637行)、sys_delete_module(24860行)和sys_query_module(25148行)四個函數需要特別注意一樣,struct module(15581行)也要特別引起注意。這些函數實現了modprobe及insmod、lsmod和rmmod所使用的系統調用,以完成模塊的裝載、定位和卸載。
內核觸發直接回調內核程序的現象看起來很令人奇怪。但是,實際上進行的工作不止於此。例如,modprobe必須實際訪問磁盤以搜尋要裝載的模塊。而且更為重要的一點是,這種方法賦予root對內核模塊系統更多的控制能力。這主要是因為root也可以運行modprobe及相關程序。因此,root既可以手工裝載、查詢、卸載模塊,也可以由內核自動完成。
2.3 配置與編譯內核
你可能僅僅研讀、欣賞而並不修改Linux內核源代碼。但是,更普遍的情況是,用戶有強烈的願望去改進內核代碼並完成相應的測試,這樣我們就需要知道如何重建內核。本節就是要告訴你如何實現這一點,而最終則歸結於如何把你所做的修改發行給別人,以使得每個人都能從你的工作中受益。
2.3.1 配置內核
編譯內核的第一步就是配置內核,這是增加或者減少對內核特性的支持及修改內核的一些特性的必要步驟。例如,你可以要求內核為自己的聲卡指定一個不同的DMA通道。如果內核配置和你的需要相同,那麼你可以直接跳過本節,否則請繼續閱讀以下內容。
為了完成內核的配置,請先切換到root用戶,然後轉入如下內核源程序目錄:
cd /usr/src/linux
接著敲入如下命令組:
make config make menuconfig make xconfig
這三條命令都可以用來配置內核,但它們發揮作用的方式各不相同:
* make config—三種方法中最簡單也是最枯燥的一種。但是最基本的一點是,它可以適應任何情況。通過為每一個內核支持的特性向用戶提問的方式來決定在內核中需要包含哪些特性。對於大多數問題,你只要回答y(yes,把該特性編譯進內核中)、m(作為模塊編譯)或者n(no,根本不對該特性提供支持)。在決定之前用戶應該考慮清楚,因為這個過程是不可逆的。如果你在該過程中犯了錯誤,就只能按Ctrl+C退出。你也可以敲入?以獲取幫助。圖2-1顯示了這種方法在X終端上運行的情況。
圖2-1 運行中的make config
幸運的是,這種方法還有一些智能。例如,如果你對SCSI支持回答no,那麼系統就不會再詢問你有關SCSI的細節問題了。而且你可以只按回車鍵以接受默認的選擇,也就是當前的設置(因此,如果當前內核將對於SCSI的支持編譯進了內核,在這個問題上按回車鍵就意味著繼續把對SCSI的支持編譯進內核中)。即使是這樣,大部分用戶還是寧願使用另外的兩種方法。
* make menuconfig—一種基於終端的配置機制,用戶擁有通過移動光標來進行浏覽等功能。圖2-2顯示了在X終端上運行的make menuconfig。雖然在控制台上顯示的是彩色,但是在終端上的顯示仍然相當單調。使用menuconfig必須要有相應的ncurses類庫。
* make xconfig—這是我最喜歡的一種配置方式。只有你能夠在X server上用root用戶身份運行X應用程序時,這種配置方式才可以使用(有些偏執的用戶就不願意使用這種方式)。你還必須擁有Tcl窗口系統,這實際上還意味著你必須擁有Tcl、Tk以及一個正在運行的X安裝程序。作為補償,用戶獲得的是更漂亮的、基於X系統的以及和menuconfig功能相同的配置方法。圖2-3顯示了在這種方法運行過程中打開“可裝載模塊支持(Loadable module support)”子窗口的情況。
如上所述,這三種方法都實現了相同的功能:它們都生成在構建內核時使用的.config文件。而唯一的區別在於創建這個文件時的難易程度不同。
2.3.2 構建內核
構建內核要做的工作要比配置內核所做的工作少得多。雖然有幾種方式都能實現這一功能,但是選擇哪一種依賴於你希望怎樣對系統進行設置。長期以來,我已經形成了如下的習慣。雖然這種習慣比我所必須要做的略微多一些,但是它包含了所有基本的問題。首先,如果你還不在內核源程序目錄中,請先再次轉入這一目錄:
cd /usr/src/linux
現在,切換到root用戶,使用下面顯示的命令生成內核。現在在shell中敲入下面的命令,注意make命令因為空間關系分成了兩行,但實際上這在shell輸入時是一個只有一行的命令:
make dep clean zlilo boot modules modules_install
當給出了如上多個目標時,除非前面所有的目標都成功了,否則make能夠知道沒有必要繼續嘗試下面的目標。因此,如果make能夠運行結束,成功退出,那麼這就意味著所有的目標都正確構建了。現在你可以重新啟動機器以運行新的內核。
2.3.3 備份的重要性
當修改(fooling)內核時,你必須准備一個能夠啟動的備用內核。實現該目的的一種方式是通過配置Linux加載程序(LILO)以允許用戶選擇啟動的內核映象,其中之一是從沒有修改過的內核的備份(我總是這樣做的)。
如果你比較有耐心,那麼你就可以使用zdisk目標而不使用zlilo目標;它可以把能夠啟動的內核映象寫入軟盤中。這樣你就可以通過在啟動時插入軟盤的方式啟動你的測試內核;如果沒有插入軟盤,則啟動正常的內核。
但是請注意:內核模塊並沒有被裝載到軟盤中,它們實際上是裝在硬盤中的(除非你願意承擔更多的麻煩)。因此,如果你弄亂了內核模塊,即使是zdisk目標也救不了你。實際上,上面提到的這兩種方法都存在這個問題。雖然有比較好的解決方法可用,但是最簡單的方法(也就是我所使用的方法)是把備份內核作為嚴格獨立的內核來編譯,而不使用可裝載模塊的支持。通過這種方法,即使我弄亂了內核而不得不使用備份啟動系統,那麼不管問題是實驗性內核不正確還是內核模塊的原因都無關緊要。不管怎樣,在備份的內核中已經有我需要的所有東西了。
由於用戶所做的修改可能導致系統的崩潰,如損壞磁盤上的數據等等,並不僅僅只是打亂設備驅動程序或文件系統,在測試新內核之前,備份系統的最新數據也是一個英明的決策(雖然設備驅動程序的開發不是本書的主題,但是必需指出的是,設備驅動程序的缺陷可能會引起系統的物理損壞。例如顯示器是不能備份的,而且因價格昂貴而不易替換)。作為一個潛在的內核黑客,你的最佳投資(當然是讀過本書以後)是一個磁帶驅動器和充足的磁帶。
2.3.4 發布你的改進
下面是有關發布你所做修改的一些基本規則:
* 檢查最新發行版本,確保你所處理的不是已經解決了的問題。
* 遵守Linux 內核代碼編寫的風格。簡要的說就是8字符縮進以及K&R括號風格(if,else,for,while,switch或者do後面同一行中緊跟著開括號)。在內核源程序目錄下面的文檔編寫和代碼風格文件給出了完整的規則,不過我們已經介紹了其中的關鍵部分。注意本書中包含的源程序代碼為節省空間而進行了大量的重新編輯,在該過程中我可能打破了其中的一些規則。
* 獨立發行相對無關的修改。這樣,只想使用你所做的某部分修改的人就可以十分方便地獲得想要的東西,而不用一次檢驗所有的修改內容。
* 讓使用你所做修改的用戶清楚他們可以從你的修改中獲取什麼。同樣,你也應該給出這些問題的可信度。你是15min之前才匆匆完成你的修改,甚至還沒有時間對它們進行編譯,還是已經在你和你的朋友的系統中已長期穩定地運行過這個修改?
假設現在你已經准備好發行自己的修改版本了,那麼要做的第一步是建立一個說明你所做的修改的文件。你可以使用diff程序自動創建這個文件。結果或者被稱為diffs,或者在Linux中更普遍的被稱為補丁(patch)。
發布的過程十分簡單。假設原來沒有修改過的源程序代碼在linux-2.2.5目錄下,而你修改過的源程序代碼在linux-my目錄下,那麼只要進行如下的簡單工作就可以了(只有在鏈接不存在的情況下才需要執行ln):
現在,輸出文件my.patch包含了其他用戶應用這個修改程序時所需要的一切內容。(警告:如上所述,兩個源程序間的所有差別都會包含在這個補丁文件中。diff不能區分修改部分之間的關系,所以就把它們都羅列了出來。)如果補丁文件相對較小,你可以使用郵件直接發往內核郵件列表。如果補丁很大,那麼就需要通過FTP或者Web站點發布。這時發給郵件列表的信件中就只需要包含一個URL。
Linux內核郵件列表的常見問題解答(FAQ)文件位於http://www.ececs.uc.edu/~rreilova/ linux/lkmlfaq.Html。該FAQ中包含了郵件列表的訂閱、郵件發布及閱讀郵件列表的注意事項等等。