寫在前面:
本文是我學習《Linux內(nèi)核設計與實現(xiàn)》 一書的學習筆記柿冲,以書的章節(jié)目錄為框架较性,同時加入一定的網(wǎng)上搜索的資料以及自己的理解瘩欺。供自己學習外,也希望能分享出去沫屡,幫助其他學習者饵隙。
差異一? 內(nèi)核編程時既不能訪問C庫也不能訪問標準 C 頭文件
由于大小和速度的限制,無論是完整的C庫還是它的子集沮脖,都太大太低效金矛。 所以內(nèi)核不能鏈接使用標準C函數(shù)庫。
大部分常用的C庫函數(shù)在內(nèi)核中得到了實現(xiàn)倘潜,Linux下的庫文件分為共享庫和靜態(tài)庫兩大類绷柒,它們兩者的差別僅在程序執(zhí)行時所需的代碼是在運行時動態(tài)加載的,還是在編譯時靜態(tài)加載的涮因。?Linux的庫一般在/lib?或/usr/lib?目錄下废睦,例如操作字符串的函數(shù)組就位于lib/string.c 文件中,想要使用它們的話养泡,只要包含 <linux/string.h> 嗜湃。
在所以沒有實現(xiàn)的函數(shù)中,最著名的就是printf函數(shù)澜掩,但是內(nèi)核代碼提供了幾乎相同的printk函數(shù)购披。printk相對于printf的一個顯著區(qū)別在于printk允許通過制定log level來設置優(yōu)先級。具體的優(yōu)先級如下表所示肩榕。
printk prototype: int printk(const char *fmt,...);
內(nèi)核基本的頭文件位于內(nèi)核源代碼書頂級目錄下的include目錄下刚陡, 例如 include/linux/inotify.h , 內(nèi)核代碼通過形如 <linux/inotify.h>的方式包含內(nèi)核基本頭文件株汉。 (inotify是一種在Linux 2.6新添加的特性筐乳,它可以實現(xiàn)對文件系統(tǒng)進行監(jiān)控,通過觸發(fā)的方式告訴你文件的變化)乔妈。
體系結(jié)構(gòu)相關(guān)的頭文件集位于內(nèi)核源代碼樹的 arch/<architecture>/include/asm 目錄下蝙云, architecture 表示所用的體系結(jié)構(gòu),例如x86體系結(jié)構(gòu)的相關(guān)頭文件就在?arch/x86/include/asm 目錄下路召。 內(nèi)核代碼通過形如<asm/ioctrl.h>的方式包含體系結(jié)構(gòu)相關(guān)的頭文件勃刨。(ioctl是設備驅(qū)動程序中對設備的I/O通道進行管理的函數(shù)波材。所謂對I/O通道進行管理,就是對設備的一些特性進行控制身隐,例如串口的傳輸波特率廷区、馬達的轉(zhuǎn)速等等。)?
差異二? 內(nèi)核編程必須使用 GNU C
Linux庫的命名比較簡單抡医,第一個特點是所有的庫以lib開頭躲因,GCC命令在在-l?選項所指定的文件名前會自動加入lib。第二個特點文件名以.a結(jié)尾的庫是靜態(tài)庫?忌傻。第三個特點文件名是.so的庫為共享庫(共享庫是在運行的時候動態(tài)加載的?)?大脉。默認情況下,GCC在鏈接時優(yōu)先使用共享庫水孩,只有當共享庫不存在時才考慮使用靜態(tài)庫镰矿。?
內(nèi)聯(lián)函數(shù) - inline關(guān)鍵字定義,一般定義在頭文件中(若是內(nèi)聯(lián)函數(shù)僅僅在某個源文件中使用俘种,也可以把它定義在文件開始的地方秤标。),形如下面的例子宙刘。
static inline void wolf (unsigned long tail_size)
它會在調(diào)用它的地方展開苍姜,因此內(nèi)聯(lián)函數(shù)在使用前必須定義好,否則編譯器沒法展開悬包。這樣可以消除函數(shù)調(diào)用和返回所帶來的開銷衙猪,包括寄存器的存儲和恢復。并且編譯器會將調(diào)用函數(shù)的代碼以及函數(shù)本身放在一起優(yōu)化布近。這樣做的代價就是占用更多的內(nèi)存空間或者占用更多的指令緩存垫释。內(nèi)核開發(fā)這通常會把那些對時間要求較高,而本身長度比較短的函數(shù)定義成內(nèi)聯(lián)函數(shù)撑瞧。
很自然的棵譬,人們會把內(nèi)聯(lián)函數(shù)與宏定義做比較,一個顯著的區(qū)別在于预伺,宏定義在預處理是展開订咸,因此不檢查函數(shù)參數(shù),返回值什么的酬诀,只是展開脏嚷,相對來說,內(nèi)聯(lián)函數(shù)在編譯時展開料滥,因此會檢查參數(shù)類型然眼,所以更安全艾船。此外葵腹,內(nèi)聯(lián)函數(shù)能有效避免使用宏定義可能產(chǎn)生的由優(yōu)先級引起的錯誤以及有宏定義中有多個相同操作數(shù)且有自增操作時高每,回到導致展開的代碼不符合預期。因此內(nèi)聯(lián)函數(shù)更被推薦使用践宴。
GCC 編譯器支持 在C函數(shù)中嵌入會變指令(這樣做的前提是知道對應的體系結(jié)構(gòu))鲸匿。我們通常使用asm()指令嵌入?yún)R編代碼,直接對寄存器進行操作阻肩。
asm(
"mov? ? r0, r0\n\t"
"mov? ? r0, r0\n\t"
"mov? ? r0, r0\n\t"
"mov? ? r0, r0"
);
對于條件選擇語句带欢,gcc內(nèi)建了一條指令用于優(yōu)化,當一個條件經(jīng)常出現(xiàn)/很好出現(xiàn)的時候烤惊,編譯器可以根據(jù)這條指令對條件分之選擇進行優(yōu)化乔煞,這條指令被內(nèi)核封裝成了 likely 與 unlikely 宏。?
例如當error錯誤分支很少發(fā)生柒室,我們可以定義:
if(unlikely(error)){
}
相反如果我們想把一個分支標記為通常為真的選擇渡贾,可以定義:
if(likely(success)){
}。
使用likely一定要非常小心雄右,如果某個條件不是占壓倒性的低位時空骚,使用likely反而會降低性能。而unlikely則被廣泛使用去標記異常分支擂仍。
差異三 內(nèi)核沒有內(nèi)存保護機制
當用戶進程試圖非法訪問時囤屹,內(nèi)核會發(fā)現(xiàn)這個錯誤,發(fā)送SIGSEGV信號逢渔,并結(jié)束整個進程肋坚,而內(nèi)核自己非法訪問內(nèi)存時,會導致oops錯誤。因此木缝,在內(nèi)核中不應該去訪問非法的內(nèi)存地址拦焚,引用空指針之類的事情,否則它可能死掉去根本不通知你一聲峦剔。
此外,內(nèi)核中的內(nèi)存不分頁角钩,也就是說吝沫,內(nèi)核中用掉每一字節(jié),物理內(nèi)存就會少掉一個字節(jié)递礼。
差異四 不要輕易在內(nèi)核中使用浮點數(shù)
差異五 內(nèi)核有容積小且固定的棧
用戶空間的棧比較大惨险,且可以動態(tài)增長,所以用戶進程可以從棧上分配大量的空間存放變量脊髓,甚至包含巨大的結(jié)構(gòu)體和數(shù)組辫愉。而內(nèi)核的棧隨體系結(jié)構(gòu)而變化,到那時固定而小将硝,每個處理器都有自己的棧恭朗。
差異六 同步和并發(fā)
Linux內(nèi)核支持SMP屏镊,兩個或多個處理器上執(zhí)行的中斷代碼可能會同時訪問共享的資源。
Linux內(nèi)核可以搶占痰腮,內(nèi)核中一段正在執(zhí)行的代碼可能被另一段代碼搶占而芥,從而導致幾段代碼訪問相同資源。
中斷是異步到來的膀值,不會顧及到正在執(zhí)行的代碼棍丐。如果不加以適當?shù)谋Wo,中斷完全可能會在代碼訪問資源時到來沧踏,這樣中斷處理函數(shù)就可能訪問同一資源歌逢。
為防止上述的問題,自旋鎖以及信號量是常用的解決并發(fā)中的資源競爭問題的方法翘狱。