內核開發不是洪水猛獸。一旦你了解到其中的規則,你就會發現,跟開發應用程序一樣;兩者區別在於要遵守的規則集合不一樣。
Linux是UNIX家族的一員,而且其內核源代碼唾手可得,因此這裡用其來作說明。
規則上,與應用程序(運行於用戶空間)的開發不同,主要表現在:
沒有C庫
用GNU C編程(對於Linux內核而言)
沒有內存保護
在內核中很難使用浮點數
內核棧大小固定且很小
由於異步中斷、搶占以及支持SMP,需要額外小心同步和並發
移植性問題
下面就來逐個解釋。
內核沒有鏈接任何C庫。這裡面涉及到很多問題,比如雞生蛋還是蛋生雞的問題:因為C庫都會包裹一些系統調用,可是沒有內核時就沒有系統調用,那麼……呵呵,明白了吧?另一個問題就是大小問題。任何一個C庫,甚至其一個子集,對於內核來說都太大了。不過,不要著急,很多常用的庫函數在內核中都有實現。
這裡面涉及到一個著名的函數:printf(),內核提供了一個替代品:printk()。如果你要做內核開發,就會頻繁使用該函數。記住:Linus本人不允許在內核中嵌入調試器(這是另外的話題,有興趣的可以自己去google一下),因此很多情況下要依靠printk()。
毫無疑問,Linux的內核是用C語言寫的。但所用的C並不是ANSI C,而是經過GNU擴展之後的C,這就是為什麼Linux內核對於gcc編譯器的依賴程度如此之高。GNU對C的擴展中就包括:內聯函數(inline functions),分支預測和內聯匯編(inline assembly)。分支預測用於判斷哪些情況是幾乎永遠不可能發生的,或者哪些情況幾乎永遠都會發生——unlikely()和likely()。
當用戶空間的代碼訪問非法地址時,內核能夠捕獲該錯誤,然後向進程發送SIGSEGV並終止進程。在UNIX世界,人們總是說kill/殺掉進程,其實,kill僅僅是用來向進程發送信號的,並不是殺掉它——太殘忍了。這是題外話了,呵呵。那麼,當內核代碼訪問非法地址時,誰來照顧內核呢?只能自己照顧自己了。非法的地址訪問將導致oops,這是重大的問題,沒人會告訴你訪問了非法地址,但是你可以通過日志來查詢/調試。
另外,內核內存是不分頁,因此你沒申請一個字節,物理內存就少掉一個字節。小心了!
在內核中使用浮點數非常困難,如果你想給自己找麻煩的話,可以試試。用戶空間代碼要使用浮點數指令時,一般來說會產生一個中斷,內核捕獲該中斷並作相應的處理。然而,內核沒法捕獲自己。而且,要使用浮點指令,不但要保存浮點寄存器,還要做很多繁瑣的事情——光看看內核如何為用戶空間代碼使用浮點數就知道了,可以參考進程調度裡的上下文切換。
用戶空間的程序可以在棧上申請大量的空間——定義足夠多的局部變量,因為用戶空間的棧非常大,而且可以動態增長。不過有些不夠智能的系統做不到動態增長。然而,內核的棧非常小,而且無法動態增長。
作為一個搶占式多任務、支持對稱多處理(SMP)的系統,同步和並發是任何一個內核hacker都需要時時刻刻小心的問題。調度器“興之所至”,調度進程,這就需要同步;加上來自CPU外的各種中斷導致內核需要對某些代碼或數據加以保護。而搶占的意思就是,無論誰占用了CPU,都有可能被其他進程搶掉,內核也不例外。Linux對於競態條件提供了spinlock(自旋鎖)和semaphore(信號量)。
最後就是移植性問題。這個問題從來都不見簡單,而且linux的目標是多種平台都能運行,因此移植性顯得更為重要。字節序問題就是一個典型的移植性問題。然而,應用程序可能僅僅為一個平台開發。
看了這些,是不是覺得,其實內核開發也不難呢?
有雄心的hacker們,不要被OS kernel這個名詞嚇倒。看看Linux Kernel Development,准備好Linux內核源碼,沏上一杯茶,然後你就可以開發內核了!
參考:
Linux Kernel Development, 2nd edition, by Robert Love
Copyleft (C) 2007 raof01. 本文可以用於除商業用途外的所有用途。若要用於商業用途,請與作者聯系。