- Android底層接口與驅動開發技術詳解
- 陳強
- 3683字
- 2019-10-12 15:38:30
1.6.3 Linux內核的顯著特性
下載Linux內核代碼并安裝瀏覽工具后,即可瀏覽并分析Linux內核的源碼。接下來將簡要講解Linux內核的顯著特性。
1.GCC特性
Linux內核使用GNU Compiler Collection(GCC)套件的幾個特殊功能。這些功能包括提供快捷方式和簡化及向編譯器提供優化提示等。GCC和Linux是出色的組合。盡管它們是獨立的軟件,但是Linux完全依靠GCC在新的體系結構上運行。Linux還利用GCC中的特性(稱為擴展)實現更多功能和優化。
(1)基本功能
概括來說,GCC具有如下兩大功能。
● 功能性:擴展提供新功能。
● 優化:擴展幫助生成更高效的代碼。
GCC允許通過變量的引用識別類型,這種操作支持泛型編程。在C++、Ada和Java語言等許多現代編程語言中都可以找到相似的功能。例如,Linux使用typeof構建min和max等依賴于類型的操作。使用typeof構建一個泛型宏的代碼如下。

GCC還支持范圍,在C語言的許多方面都可以使用范圍。最常見的是在switch/case塊中的case語句中使用。使用switch/case也可以通過使用跳轉表來實現編譯器優化。在復雜的條件結構中,通常依靠嵌套的if語句實現與下面代碼相同的結果,但是下面的代碼更簡潔。具體代碼如下。

(2)屬性
GCC允許聲明函數、變量和類型的特殊屬性,以便指示編譯器進行特定方面的優化和更仔細的代碼檢查。使用方式非常簡單,只需在聲明后面加上如下代碼即可。

其中ATTRIBUTE是屬性的說明,多個說明之間以逗號分隔。GCC可以支持十幾個屬性,接下來介紹一些比較常用的屬性。
①屬性noreturn:屬性noreturn用在函數中,表示該函數從不返回。它能夠讓編譯器生成較為優化的代碼,消除不必要的警告信息。
②屬性format(archetype, string-index, first-to-check):屬性format用在函數中,表示該函數使用printf、scanf、strftime或strfmon風格的參數,并可使編譯器檢查函數聲明和函數實際調用參數之間的格式化字符串是否匹配。
● archetype:指定是哪種風格。
● string-index:指定傳入函數的第幾個參數是格式化字符串。
● first-to-check:指定從函數的第幾個參數開始按照上述規則進行檢查。
如下面的內核代碼。

這表示printk的第一個參數是格式化字符串,從第二個參數開始根據格式化字符串檢查參數。
③屬性unused:屬性unused用于函數和變量,表示該函數或變量可能并不使用,這個屬性能夠避免編譯器產生警告信息。
④屬性aligned(ALIGNMENT):屬性aligned常用在變量、結構或聯合中,用于設定一個指定大小的對齊格式,以字節為單位,如下面的內核代碼。

上述代碼表示結構體ohci_hcca的成員以256字節對齊。如果aligned后面不緊跟一個指定的數字值,那么編譯器將依據目標主機情況使用最大、最有益的對齊方式。
需要注意的是,attribute屬性的效果與用戶的連接器也有關,如果用戶的連接器最大只支持16字節對齊,那么此時定義32字節對齊也是無濟于事的。
● 屬性packed:屬性packed用在變量和類型中,當用在變量或結構體成員時,表示使用最小可能的對齊。當用在枚舉、結構體或聯合類型時,表示該類型使用最小的內存。屬性packed多用于定義硬件相關的結構時,使元素之間不會因對齊產生問題。例如,下面的內核代碼。


在上述代碼中,__attribute__((packed))告訴編譯器usb_interface_descriptor的元素為1字節對齊,不要再添加填充位。因為定義此結構的代碼和usb spec中的完全一樣。如果不給編譯器這個暗示,則編譯器會依據平臺的類型在結構的每個元素之間添加一定的填充位,使用這個結構時就不能達到預期的結果。
(3)內建函數。在GCC中提供了大量的內建函數,其中有很多是標準C庫函數的內建版本,如memcpy(),它們的功能與對應的C庫函數的功能相同,在此不再進行講解。
在內建函數中,還有很多函數的名字是以__builtin開始的,接下來對__builtin_expect()進行詳細分析,其他__builtin_xxx()函數的原理和此函數類似,不再一一介紹。
函數__builtin_expect()的格式如下。

為什么Linux會推出__builtin_xxx()函數呢?這是因為大部分代碼在分支預測方面做的比較糟糕,所以GCC提供了此內建函數來幫助處理分支預測,并優化程序。
● 第一個參數exp:是一個整型的表達式,返回值也是此exp。
● 第二個參數c:其值必須是一個編譯期的常量。
由此可見,此內建函數的意思就是exp的預期值為c,編譯器可以根據這個信息適當地重排條件語句塊的順序,將符合這個條件的分支放在合適的地方。
此函數在Linux內核中的應用,具體代碼如下。

unlikely(x)用于告訴編譯器條件x發生的可能性不大,likely用于告訴編譯器條件x發生的可能性很大。它們一般用在條件語句里,if語句不變,當if條件為1的可能性非常小時,可以在條件表達式外面包裝一個unlikely(),那么這個條件塊里語句的目標碼可能就會被放在一個比較遠的位置,以保證經常執行的目標碼更緊湊。如果可能性非常大,則使用likely()包裝。
2.鏈表的重要性
鏈表和本書講解的驅動密切相關,如USB驅動。鑒于鏈表在內核中的特殊地位,有必要在此對其作一番陳述。內核中鏈表的實現位于include/linux/list.h文件,鏈表數據結構的定義也很簡單,具體代碼如下。

結構list_head包含兩個指向list_head結構的指針prev和next,由此可見,內核中的鏈表實際上都是雙鏈表。學習過C語言的讀者應該知道,鏈表的定義結構如下。

通過上述格式使用鏈表,對于每一種數據類型都要定義它們各自的鏈表結構。而內核中的鏈表卻與此不同,它并沒有數據域,不是在鏈表結構中包含數據,而是在描述數據類型的結構中包含鏈表。
如果在hub驅動中使用struct usb_hub來描述hub設備,hub需要處理一系列的事件,如當探測到一個設備鏈進來時,就會執行一些代碼去初始化該設備,所以hub就創建了一個鏈表來處理各種事件,這個鏈表的結構如圖1-6所示。

圖1-6 hub鏈表的結構
在Linux代碼中,完整展示了鏈表的操作過程,接下來簡單剖析對應的Linux代碼。
(1)聲明與初始化
可以使用以下兩種方式來聲明鏈表。
● 使用LIST_HEAD宏在編譯時靜態初始化;
● 使用INIT_LIST_HEAD()在運行時進行初始化。
對應的Linux代碼如下。

無論采用哪種方式,新生成的鏈表頭的兩個指針next、prev都初始化為指向自己。
(2)判斷鏈表
判斷鏈表是否為空,對應的Linux代碼如下。

(3)插入操作
建立鏈表后,就不可避免地對其進行操作,如向里面添加數據。使用函數list_add()和list_add_tail()可以完成添加數據的工作。對應的Linux代碼如下。

其中,函數list_add()將數據插入在head后,函數list_add_tail()將數據插入在head->prev后。對于循環鏈表來說,因為表頭的next、prev分別指向鏈表中的第一個和最后一個節點,所以函數list_add()和list_add_tail()的區別并不大。
(4)刪除操作
可以使用函數list_replace_init()從鏈表里刪除一個元素,并且將其初始化。對應的Linux代碼如下。

(5)遍歷操作
在內核中的鏈表僅僅保存了list_head結構的地址,應該如何通過它獲取一個鏈表節點真正的數據項呢?此時就需要使用list_entry宏,通過它可以很容易地獲得一個鏈表節點的數據。對應的Linux代碼如下。

假如以hub驅動為例,當要處理hub的事件時,需要知道具體是哪個hub觸發了這起事件。而list_entry的作用是從struct list_head event_list中得到它所對應的struct usb_hub結構體變量。例如,下面的代碼。

通過上述代碼,從全局鏈表hub_event_list中取出一個叫作tmp的結構體變量,然后通過tmp獲得它所對應的struct usb_hub。
3.Kconfig和Makefile
Kconfig和Makefile是瀏覽內核代碼時最常用的兩個文件之一。幾乎Linux內核中的每一個目錄下都有一個Kconfig文件和一個Makefile文件。通過Kconfig和Makefile,可以讓用戶了解一個內核目錄下面的結構。在研究內核的某個子系統、某個驅動或其他某個部分之前,需要仔細閱讀目錄下對應的Kconfig和Makefile文件。
(1)Kconfig結構
每種平臺對應的目錄下面都有一個Kconfig文件,如arch/i386/Kconfig。Kconfig文件通過source語句可以構建一個Kconfig樹。文件“arch/i386/Kconfig”的代碼片段如下。

Kconfig的詳細語法規則可以參看內核文檔Documentation/kbuild/kconfig-language.txt,下面對其簡單介紹。
①菜單項:關鍵字config可以定義一個新的菜單項,如下面的代碼。

后面的代碼定義了該菜單項的屬性,包括類型、依賴關系、選擇提示、幫助信息和默認值等。
常用的類型有bool、tristate、string、hex和int。類型bool的只能被選中或不選中,類型tristate的菜單項多了編譯成內核模塊的選項。
依賴關系是通過depends on或requires定義的,指出此菜單項是否依賴于另外一個菜單項。
幫助信息需要使用help或---help---來指出。
②菜單組織結構:菜單選項通過兩種方式來組成樹狀結構,具體說明如下。
第一種方式:使用關鍵字menu顯式聲明為菜單,如下面的代碼。

第二種方式:也可以使用依賴關系確定菜單結構,如下面的代碼。

其中菜單項MODVERSIONS依賴于MODULES,所以它就是一個子菜單項。這要求菜單項和它的子菜單項同步顯示或不顯示。
③關鍵字Kconfig:Kconfig文件描述了一系列的菜單選項,除幫助信息外,文件中的每一行都以一個關鍵字開始,主要有config、menuconfig、choice/endchoice、comments、menu/endmenu、if/endif、source等,只有前5個可以用在菜單項定義的開始,它們都可以結束一個菜單項。
(2)Makefile
Linux內核的Makefile分為以下5個組成部分。
● Makefile:最頂層的Makefile。
● .config:內核的當前配置文檔,編譯時成為頂層Makefile的一部分。
● arch/$(ARCH)/Makefile:和體系結構相關的Makefile。
● Makefile.*:一些特定Makefile的規則。
● kbuild級別Makefile:各級目錄下的大約500個文檔,編譯時根據上層Makefile傳下來的宏定義和其他編譯規則,將源代碼編譯成模塊或編入內核。頂層的Makefile文檔讀取.config文檔的內容,并總體上負責build內核和模塊。Arch Makefile則提供補充體系結構相關的信息。其中.config的內容是在make menuconfig時通過Kconfig文檔配置的結果。
假如想把自己寫的一個Flash的驅動程式加載到工程中,并且能夠通過menuconfig配置內核時會選擇該驅動,此時該怎么辦呢?其實借助Makefile就可以實現,解決流程如下。
將編寫的flashtest.c文檔添加到/driver/mtd/maps/目錄下。
修改/driver/mtd/maps目錄下的kconfig文檔,修改代碼如下。

當運行make menuconfig時會出現ap71 flash選項。
修改該目錄下makefile文檔,添加下面的代碼內容。

此時當運行make menucofnig時會發現ap71 flash選項,假如選擇了此選項,該選擇就會保存在.config文檔中。當編譯內核時會讀取.config文檔,當發現ap71 flash選項為yes時,系統在調用/driver/mtd/maps/下的makefile時,會把flashtest.o加入內核中。
- Android項目開發入門教程
- Test-Driven Development with Django
- Domain-Driven Design in PHP
- Android嵌入式系統程序開發(基于Cortex-A8)
- Unity 3D UI Essentials
- Python程序設計教程
- Learning Zimbra Server Essentials
- Python Natural Language Processing
- Mastering VMware Horizon 6
- Java程序設計
- 小學生Python創意編程(視頻教學版)
- 面向WebAssembly編程:應用開發方法與實踐
- Unreal Engine 4 Scripting with C++ Cookbook
- Python商業數據分析:零售和電子商務案例詳解(雙色)
- Meteor Design Patterns