- Linux設(shè)備驅(qū)動(dòng)開發(fā)詳解(第2版)
- 華清遠(yuǎn)見嵌入式培訓(xùn)中心 宋寶華編著
- 5766字
- 2018-12-27 10:06:07
第3章 Linux內(nèi)核及內(nèi)核編程
本章導(dǎo)讀
本章為讀者打下Linux驅(qū)動(dòng)編程的軟件基礎(chǔ)。由于Linux驅(qū)動(dòng)編程本質(zhì)屬于Linux內(nèi)核編程,因此我們有必要熟悉Linux內(nèi)核及內(nèi)核編程的基礎(chǔ)知識(shí)。
3.1~3.2節(jié)講解了Linux內(nèi)核的演變及新版Linux2.6內(nèi)核的特點(diǎn)。
3.3節(jié)分析了Linux內(nèi)核源代碼目錄結(jié)構(gòu)和Linux內(nèi)核的組成部分及其關(guān)系,并對(duì)Linux的用戶空間和內(nèi)核空間進(jìn)行了說明。
3.4節(jié)講述了Linux2.6內(nèi)核的編譯及內(nèi)核引導(dǎo)過程。除此之外,還描述了在 Linux 內(nèi)核中新增程序的方法,驅(qū)動(dòng)工程師編寫的設(shè)備驅(qū)動(dòng)也應(yīng)該以此方式被添加。
3.5節(jié)闡述了Linux下C編程的命名習(xí)慣以及Linux所使用的GNU C針對(duì)標(biāo)準(zhǔn)C的擴(kuò)展語法。
3.1 Linux內(nèi)核的發(fā)展與演變
Linux操作系統(tǒng)是UNIX操作系統(tǒng)的一種克隆系統(tǒng),誕生于1991年10月5日(第一次正式向外公布的時(shí)間)。Linux操作系統(tǒng)的誕生、發(fā)展和成長(zhǎng)過程依賴著5個(gè)重要支柱:UNIX操作系統(tǒng)、Minix操作系統(tǒng)、GNU計(jì)劃、Posix標(biāo)準(zhǔn)和Internet。
1.UNIX操作系統(tǒng)
UNIX 操作系統(tǒng)是美國(guó)貝爾實(shí)驗(yàn)室的Ken. Thompson和Dennis Ritchie于1969年夏在DEC PDP-7小型計(jì)算機(jī)上開發(fā)的一個(gè)分時(shí)操作系統(tǒng)。Linux操作系統(tǒng)可看作UNIX操作系統(tǒng)的一個(gè)克隆版本。
2.Minix操作系統(tǒng)
Minix 操作系統(tǒng)也是 UNIX 的一種克隆系統(tǒng),它于1987年由著名計(jì)算機(jī)教授 Andrew S. Tanenbaum開發(fā)完成。開放源代碼Minix系統(tǒng)的出現(xiàn)在全世界的大學(xué)中刮起了學(xué)習(xí)UNIX系統(tǒng)的旋風(fēng)。Linux剛開始就是參照Minix系統(tǒng)于1991年才開始開發(fā)。
3.GNU計(jì)劃
GNU計(jì)劃和自由軟件基金會(huì)(FSF)是由Richard M. Stallman于1984年創(chuàng)辦的,GNU是“GNU's Not UNIX”的縮寫。到20世紀(jì)90年代初,GNU項(xiàng)目已經(jīng)開發(fā)出許多高質(zhì)量的免費(fèi)軟件,其中包括emacs 編輯系統(tǒng)、bash shell程序、gcc系列編譯程序、gdb調(diào)試程序等。這些軟件為L(zhǎng)inux操作系統(tǒng)的開發(fā)創(chuàng)造了一個(gè)合適的環(huán)境,是Linux誕生的基礎(chǔ)之一。沒有GNU軟件環(huán)境,Linux將寸步難行。因此,嚴(yán)格而言,“Linux”應(yīng)該被稱為“GNU/Linux”系統(tǒng)。
4.Posix標(biāo)準(zhǔn)
Posix(Portable Operating System Interface for Computing Systems,可移植的操作系統(tǒng)接口)是由IEEE 和ISO/IEC 開發(fā)的一組標(biāo)準(zhǔn)。該標(biāo)準(zhǔn)基于現(xiàn)有的UNIX實(shí)踐和經(jīng)驗(yàn)完成,描述了操作系統(tǒng)的調(diào)用服務(wù)接口,用于保證編制的應(yīng)用程序可以在源代碼一級(jí)上在多種操作系統(tǒng)上移植。該標(biāo)準(zhǔn)在推動(dòng)Linux操作系統(tǒng)朝著正規(guī)化發(fā)展起著重要的作用,是Linux前進(jìn)的燈塔。
5.Internet
如果沒有Intenet,沒有遍布全世界的無數(shù)計(jì)算機(jī)駭客的無私奉獻(xiàn),那么Linux最多只能發(fā)展到0.13(0.95)版的水平。從0.95版開始,對(duì)內(nèi)核的許多改進(jìn)和擴(kuò)充均以其他人為主了,而Linus以及其他maintainer的主要任務(wù)開始變成對(duì)內(nèi)核的維護(hù)和決定是否采用某個(gè)補(bǔ)丁程序。
表3.1描述了Linux 操作系統(tǒng)重要版本的變遷歷史及各版本的主要特點(diǎn)。
表3.1 Linux 操作系統(tǒng)版本歷史

從表3.1可以看出,Linux 的開發(fā)一直朝著支持更多的CPU、硬件體系結(jié)構(gòu)和外部設(shè)備,支持更廣泛領(lǐng)域的應(yīng)用,提供更好的性能3個(gè)方向發(fā)展。
除了Linux內(nèi)核本身可提供免費(fèi)下載以外,一些廠商封裝了Linux內(nèi)核和大量有用的軟件包,制定了相應(yīng)的Linux發(fā)布版,如Red Hat Linux、TurboLinux、Debian、SuSe、Ubuntu,國(guó)內(nèi)的RedFlag和xteam等。
再者,針對(duì)嵌入式系統(tǒng)的應(yīng)用,一些改進(jìn)內(nèi)核的Linux被開發(fā)出來,如改進(jìn)實(shí)時(shí)性的Hard Hat Linux和RTLinux、支持不含MMU CPU的μClinux(日前Linux mainline已經(jīng)支持MMU-less系統(tǒng))、面向數(shù)字相機(jī)和MP3等微型嵌入式設(shè)備的ThinLinux和以及頗有商業(yè)背景的MontaVista等。
3.2 Linux 2.6內(nèi)核的特點(diǎn)
本書基于的是Linux2.6內(nèi)核,LDD6410開發(fā)板內(nèi)核的完整版本號(hào)為2.6.28.6。Linux2.6內(nèi)核是Linux開發(fā)者群落一個(gè)寄予厚望的版本,從2003年12月Linux2.6.0發(fā)布至今,一直還處于開發(fā)之中,并還將穩(wěn)定較長(zhǎng)一段時(shí)間。Linux2.6相對(duì)于Linux2.4有相當(dāng)大的改進(jìn),主要體現(xiàn)在如下幾個(gè)方面:
1.新的調(diào)度器
2.6 版本的Linux 內(nèi)核使用了新的進(jìn)程調(diào)度算法,它在高負(fù)載的情況下執(zhí)行得極其出色,并且當(dāng)有很多處理器時(shí)也可以很好地?cái)U(kuò)展。
2.內(nèi)核搶占
在2.6版本的Linux 內(nèi)核中,一個(gè)內(nèi)核任務(wù)可以被搶占,從而提高系統(tǒng)的實(shí)時(shí)性。這樣做最主要的優(yōu)勢(shì)在于,可以極大地增強(qiáng)系統(tǒng)的用戶交互性,用戶將會(huì)覺得鼠標(biāo)單擊和擊鍵的事件得到了更快速的響應(yīng)。
3.改進(jìn)的線程模型
2.6 版本的Linux 中線程操作速度得以提高,可以處理任意數(shù)目的線程,最大可以到20億。
4.虛擬內(nèi)存的變化
從虛擬內(nèi)存的角度來看,新內(nèi)核融合了 r-map(反向映射)技術(shù),顯著改善虛擬內(nèi)存在一定程度負(fù)載下的性能。
5.文件系統(tǒng)
2.6版內(nèi)核增加了對(duì)日志文件系統(tǒng)功能的支持,解決了2.4版在這方面的不足。2.6版內(nèi)核在文件系統(tǒng)上的關(guān)鍵變化還包括對(duì)擴(kuò)展屬性及 Posix 標(biāo)準(zhǔn)訪問控制的支持。ext2/ext3作為大多數(shù)Linux系統(tǒng)缺省安裝的文件系統(tǒng),在2.6版內(nèi)核中增加了對(duì)擴(kuò)展屬性的支持,可以給指定的文件在文件系統(tǒng)中嵌入元數(shù)據(jù)。
6.音頻
新的Linux音頻體系結(jié)構(gòu)ALSA(Advanced Linux Sound Architecture)取代了缺陷很多的舊的OSS(Open Sound System)。新的聲音體系結(jié)構(gòu)支持USB音頻和MIDI設(shè)備,并支持全雙工重放等功能。
7.總線
SCSI/IDE 子系統(tǒng)經(jīng)過大幅度的重寫,解決和改善了以前的一些問題。比如2.6版內(nèi)核可以直接通過IDE驅(qū)動(dòng)程序來支持IDE CD/RW設(shè)備,而不必像以前一樣要使用一個(gè)特別的SCSI模擬驅(qū)動(dòng)程序。
8.電源管理
支持ACPI(高級(jí)電源配置管理界面,Advanced Configuration and Power Interface),用于調(diào)整CPU在不同的負(fù)載下工作于不同的時(shí)鐘頻率以降低功耗。
9.聯(lián)網(wǎng)和IPSec
2.6內(nèi)核中加入了對(duì)IPSec的支持,刪除了原來內(nèi)核內(nèi)置的HTTP服務(wù)器khttpd,加入了對(duì)新的NFSv4(網(wǎng)絡(luò)文件系統(tǒng))客戶機(jī)/服務(wù)器的支持,并改進(jìn)了對(duì)IPv6的支持。
10.用戶界面層
2.6 內(nèi)核重寫了幀緩沖/控制臺(tái)層,人機(jī)界面層還加入了對(duì)近乎所有接口設(shè)備的支持(從觸摸屏到盲人用的設(shè)備和各種各樣的鼠標(biāo))。
在設(shè)備驅(qū)動(dòng)程序的方面,Linux2.6相對(duì)于Linux2.4也有較大的改動(dòng),這主要表現(xiàn)在內(nèi)核API中增加了不少新功能(例如內(nèi)存池)、sysfs文件系統(tǒng)、內(nèi)核模塊從.o變?yōu)?ko、驅(qū)動(dòng)模塊編譯方式、模塊使用計(jì)數(shù)、模塊加載和卸載函數(shù)的定義等方面。
3.3 Linux內(nèi)核的組成
3.3.1 Linux內(nèi)核源代碼目錄結(jié)構(gòu)
本書范例程序所基于的Linux2.6.28.6內(nèi)核源代碼包含如下目錄。
● arch:包含和硬件體系結(jié)構(gòu)相關(guān)的代碼,每種平臺(tái)占一個(gè)相應(yīng)的目錄,如 i386、arm、powerpc、mips等。
● block:塊設(shè)備驅(qū)動(dòng)程序I/O調(diào)度。
● crypto:常用加密和散列算法(如AES、SHA等),還有一些壓縮和CRC校驗(yàn)算法。● Documentation:內(nèi)核各部分的通用解釋和注釋。
● drivers:設(shè)備驅(qū)動(dòng)程序,每個(gè)不同的驅(qū)動(dòng)占用一個(gè)子目錄,如 char、block、net、mtd、i2c等。
● fs:支持的各種文件系統(tǒng),如EXT、FAT、NTFS、JFFS2等。
● include:頭文件,與系統(tǒng)相關(guān)的頭文件被放置在include/linux子目錄下。
● init:內(nèi)核初始化代碼。
● ipc:進(jìn)程間通信的代碼。
● kernel:內(nèi)核的最核心部分,包括進(jìn)程調(diào)度、定時(shí)器等,而和平臺(tái)相關(guān)的一部分代碼放在arch/*/kernel目錄下。
● lib:庫文件代碼。
● mm:內(nèi)存管理代碼,和平臺(tái)相關(guān)的一部分代碼放在arch/*/mm目錄下。
● net:網(wǎng)絡(luò)相關(guān)代碼,實(shí)現(xiàn)了各種常見的網(wǎng)絡(luò)協(xié)議。
● scripts:用于配置內(nèi)核的腳本文件。
● security:主要是一個(gè)SELinux的模塊。
● sound:ALSA、OSS音頻設(shè)備的驅(qū)動(dòng)核心代碼和常用設(shè)備驅(qū)動(dòng)。
● usr:實(shí)現(xiàn)了用于打包和壓縮的cpio等。
3.3.2 Linux內(nèi)核的組成部分
如圖3.1所示,Linux內(nèi)核主要由進(jìn)程調(diào)度(SCHED)、內(nèi)存管理(MM)、虛擬文件系統(tǒng)(VFS)、網(wǎng)絡(luò)接口(NET)和進(jìn)程間通信(IPC)5個(gè)子系統(tǒng)組成。
1.進(jìn)程調(diào)度
進(jìn)程調(diào)度控制系統(tǒng)中的多個(gè)進(jìn)程對(duì) CPU的訪問,使得多個(gè)進(jìn)程能在 CPU 中“微觀串行,宏觀并行”地執(zhí)行。進(jìn)程調(diào)度處于系統(tǒng)的中心位置,內(nèi)核中其他的子系統(tǒng)都依賴它,因?yàn)槊總€(gè)子系統(tǒng)都需要掛起或恢復(fù)進(jìn)程。

圖3.1 Linux內(nèi)核的組成部分與關(guān)系
如圖3.2所示,Linux 的進(jìn)程在幾個(gè)狀態(tài)間進(jìn)行切換。在設(shè)備驅(qū)動(dòng)編程中,當(dāng)請(qǐng)求的資源不能得到滿足時(shí),驅(qū)動(dòng)一般會(huì)調(diào)度其他進(jìn)程執(zhí)行,并使本進(jìn)程進(jìn)入睡眠狀態(tài),直到它請(qǐng)求的資源被釋放,才會(huì)被喚醒而進(jìn)入就緒態(tài)。睡眠分成可被打斷的睡眠和不可被打斷的睡眠,兩者的區(qū)別在于可被打斷的睡眠在收到信號(hào)的時(shí)候會(huì)醒。

圖3.2 Linux進(jìn)程狀態(tài)轉(zhuǎn)換
在設(shè)備驅(qū)動(dòng)編程中,當(dāng)請(qǐng)求的資源不能得到滿足時(shí),驅(qū)動(dòng)一般會(huì)調(diào)度其他進(jìn)程執(zhí)行,其對(duì)應(yīng)進(jìn)程進(jìn)入睡眠狀態(tài),直到它請(qǐng)求的資源被釋放,才會(huì)被喚醒而進(jìn)入就緒態(tài)。
設(shè)備驅(qū)動(dòng)中,如果需要幾個(gè)并發(fā)執(zhí)行的任務(wù),可以啟動(dòng)內(nèi)核線程,啟動(dòng)內(nèi)核線程的函數(shù)為:
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
2.內(nèi)存管理
內(nèi)存管理的主要作用是控制多個(gè)進(jìn)程安全地共享主內(nèi)存區(qū)域。當(dāng) CPU 提供內(nèi)存管理單元(MMU)時(shí),Linux內(nèi)存管理完成為每個(gè)進(jìn)程進(jìn)行虛擬內(nèi)存到物理內(nèi)存的轉(zhuǎn)換。Linux2.6引入了對(duì)無MMU CPU的支持。
如圖3.3所示,一般而言,Linux的每個(gè)進(jìn)程享有4GB的內(nèi)存空間,0~3GB屬于用戶空間,3~4GB屬于內(nèi)核空間,內(nèi)核空間對(duì)常規(guī)內(nèi)存、I/O設(shè)備內(nèi)存以及高端內(nèi)存存在不同的處理方式。

圖3.3 Linux進(jìn)程地址空間
3.虛擬文件系統(tǒng)
如圖3.4所示,Linux虛擬文件系統(tǒng)(VFS)隱藏各種了硬件的具體細(xì)節(jié),為所有的設(shè)備提供了統(tǒng)一的接口。而且,它獨(dú)立于各個(gè)具體的文件系統(tǒng),是對(duì)各種文件系統(tǒng)的一個(gè)抽象,它使用超級(jí)塊super block存放文件系統(tǒng)相關(guān)信息,使用索引節(jié)點(diǎn)inode存放文件的物理信息,使用目錄項(xiàng)dentry存放文件的邏輯信息。

圖3.4 Linux文件系統(tǒng)
4.網(wǎng)絡(luò)接口
網(wǎng)絡(luò)接口提供了對(duì)各種網(wǎng)絡(luò)標(biāo)準(zhǔn)的存取和各種網(wǎng)絡(luò)硬件的支持。如圖3.5所示,在Linux中網(wǎng)絡(luò)接口可分為網(wǎng)絡(luò)協(xié)議和網(wǎng)絡(luò)驅(qū)動(dòng)程序,網(wǎng)絡(luò)協(xié)議部分負(fù)責(zé)實(shí)現(xiàn)每一種可能的網(wǎng)絡(luò)傳輸協(xié)議,網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序負(fù)責(zé)與硬件設(shè)備通信,每一種可能的硬件設(shè)備都有相應(yīng)的設(shè)備驅(qū)動(dòng)程序。

圖3.5 Linux網(wǎng)絡(luò)體系結(jié)構(gòu)
5.進(jìn)程通信
進(jìn)程通信支持提供進(jìn)程之間的通信,Linux 支持進(jìn)程間的多種通信機(jī)制,包含信號(hào)量、共享內(nèi)存、管道等,這些機(jī)制可協(xié)助多個(gè)進(jìn)程、多資源的互斥訪問、進(jìn)程間的同步和消息傳遞。
Linux內(nèi)核的5個(gè)組成部分之間的依賴關(guān)系如下。
● 進(jìn)程調(diào)度與內(nèi)存管理之間的關(guān)系:這兩個(gè)子系統(tǒng)互相依賴。在多道程序環(huán)境下,程序要運(yùn)行必須為之創(chuàng)建進(jìn)程,而創(chuàng)建進(jìn)程的第一件事情,就是將程序和數(shù)據(jù)裝入內(nèi)存。
● 進(jìn)程間通信與內(nèi)存管理的關(guān)系:進(jìn)程間通信子系統(tǒng)要依賴內(nèi)存管理支持共享內(nèi)存通信機(jī)制,這種機(jī)制允許兩個(gè)進(jìn)程除了擁有自己的私有空間,還可以存取共同的內(nèi)存區(qū)域。
● 虛擬文件系統(tǒng)與網(wǎng)絡(luò)接口之間的關(guān)系:虛擬文件系統(tǒng)利用網(wǎng)絡(luò)接口支持網(wǎng)絡(luò)文件系統(tǒng)(NFS),也利用內(nèi)存管理支持RAMDISK設(shè)備。
● 內(nèi)存管理與虛擬文件系統(tǒng)之間的關(guān)系:內(nèi)存管理利用虛擬文件系統(tǒng)支持交換,交換進(jìn)程(swapd)定期由調(diào)度程序調(diào)度,這也是內(nèi)存管理依賴于進(jìn)程調(diào)度的惟一原因。當(dāng)一個(gè)進(jìn)程存取的內(nèi)存映射被換出時(shí),內(nèi)存管理向文件系統(tǒng)發(fā)出請(qǐng)求,同時(shí),掛起當(dāng)前正在運(yùn)行的進(jìn)程。
除了這些依賴關(guān)系外,內(nèi)核中的所有子系統(tǒng)還要依賴于一些共同的資源。這些資源包括所有子系統(tǒng)都用到的例程,如分配和釋放內(nèi)存空間的函數(shù)、打印警告或錯(cuò)誤信息的函數(shù)及系統(tǒng)提供的調(diào)試?yán)痰取?/p>
3.3.3 Linux內(nèi)核空間與用戶空間
現(xiàn)代CPU內(nèi)部往往實(shí)現(xiàn)了不同的操作模式(級(jí)別),不同的模式有不同的功能,高層程序往往不能訪問低級(jí)功能,而必須以某種方式切換到低級(jí)模式。
例如,ARM處理器分為7種工作模式。
● 用戶模式(usr):大多數(shù)的應(yīng)用程序運(yùn)行在用戶模式下,當(dāng)處理器運(yùn)行在用戶模式下時(shí),某些被保護(hù)的系統(tǒng)資源是不能被訪問的。
● 快速中斷模式(fiq):用于高速數(shù)據(jù)傳輸或通道處理。
● 外部中斷模式(irq):用于通用的中斷處理。
● 管理模式(svc):操作系統(tǒng)使用的保護(hù)模式。
● 數(shù)據(jù)訪問終止模式(abt):當(dāng)數(shù)據(jù)或指令預(yù)取終止時(shí)進(jìn)入該模式,可用于虛擬存儲(chǔ)及存儲(chǔ)保護(hù)。
● 系統(tǒng)模式(sys):運(yùn)行具有特權(quán)的操作系統(tǒng)任務(wù)。
● 未定義指令中止模式(und):當(dāng)未定義的指令執(zhí)行時(shí)進(jìn)入該模式,可用于支持硬件協(xié)處理器的軟件仿真。
ARM Linux的系統(tǒng)調(diào)用實(shí)現(xiàn)原理是采用swi軟中斷從用戶態(tài)usr模式陷入內(nèi)核態(tài)svc模式。
又如,X86處理器包含4個(gè)不同的特權(quán)級(jí),稱為Ring 0~Ring3。Ring0下,可以執(zhí)行特權(quán)級(jí)指令,對(duì)任何I/O設(shè)備都有訪問權(quán)等,而Ring3則被限制很多操作。
Linux系統(tǒng)充分利用CPU的這一硬件特性,但它只使用了兩級(jí)。在Linux系統(tǒng)中,內(nèi)核可進(jìn)行任何操作,而應(yīng)用程序則被禁止對(duì)硬件的直接訪問和對(duì)內(nèi)存的未授權(quán)訪問。例如,若使用X86處理器,則用戶代碼運(yùn)行在特權(quán)級(jí)3,而系統(tǒng)內(nèi)核代碼則運(yùn)行在特權(quán)級(jí)0。
內(nèi)核空間和用戶空間這兩個(gè)名詞被用來區(qū)分程序執(zhí)行的這兩種不同狀態(tài),它們使用不同的地址空間。Linux只能通過系統(tǒng)調(diào)用和硬件中斷完成從用戶空間到內(nèi)核空間的控制轉(zhuǎn)移。
3.4 Linux內(nèi)核的編譯及加載
3.4.1 Linux內(nèi)核的編譯
Linux 驅(qū)動(dòng)工程師需要牢固地掌握 Linux 內(nèi)核的編譯方法以為嵌入式系統(tǒng)構(gòu)建可運(yùn)行的Linux 操作系統(tǒng)映像。在編譯 LDD6410 的內(nèi)核時(shí),需要配置內(nèi)核,可以使用下面命令中的一個(gè):
#make config(基于文本的最為傳統(tǒng)的配置界面,不推薦使用) #make menuconfig(基于文本菜單的配置界面) #make xconfig(要求QT被安裝) #make gconfig(要求GTK+被安裝)
在配置Linux2.6內(nèi)核所使用的make config、make menuconfig、make xconfig和make gconfig這4種方式中,最值得推薦的是 make menuconfig,它不依賴于QT 或GTK+,且非常直觀,對(duì)LDD6410的Linux2.6.28內(nèi)核運(yùn)行make menuconfig后的界面如圖3.6。

圖3.6 Linux內(nèi)核編譯配置
內(nèi)核配置包含的項(xiàng)目相當(dāng)多,arch/arm/configs/ldd6410lcd_defconfig文件包含了LDD6410 的默認(rèn)配置,因此,只需要運(yùn)行make ldd6410lcd_defconfig就可以為L(zhǎng)DD6410開發(fā)板配置內(nèi)核。
編譯內(nèi)核和模塊的方法是:
make zImage make modules
執(zhí)行完上述命令后,在源代碼的根目錄下會(huì)得到未壓縮的內(nèi)核映像vmlinux和內(nèi)核符號(hào)表文件System.map,在arch/arm/boot/目錄會(huì)得到壓縮的內(nèi)核映像 zImage,在內(nèi)核各對(duì)應(yīng)目錄得到選中的內(nèi)核模塊。
Linux2.6內(nèi)核的配置系統(tǒng)由以下3個(gè)部分組成。
● Makefile:分布在Linux內(nèi)核源代碼中的Makefile,定義Linux內(nèi)核的編譯規(guī)則。
● 配置文件(Kconfig):給用戶提供配置選擇的功能。
● 配置工具:包括配置命令解釋器(對(duì)配置腳本中使用的配置命令進(jìn)行解釋)和配置用戶界面(提供基于字符界面和圖形界面)。這些配置工具都是使用腳本語言,如 Tcl/TK、Perl等編寫。
使用make config、make menuconfig等命令后,會(huì)生成一個(gè).config配置文件,記錄哪些部分被編譯入內(nèi)核、哪些部分被編譯為內(nèi)核模塊。
運(yùn)行make menuconfig等時(shí),配置工具首先分析與體系結(jié)構(gòu)對(duì)應(yīng)的/arch/xxx/Kconfig文件(xxx即為傳入的ARCH 參數(shù)),/arch/xxx/Kconfig 文件中除本身包含一些與體系結(jié)構(gòu)相關(guān)的配置項(xiàng)和配置菜單以外,還通過source語句引入了一系列Kconfig文件,而這些Kconfig又可能再次通過source引入下一層的Kconfig,配置工具依據(jù)這些Kconfig包含的菜單和項(xiàng)目即可描繪出一個(gè)如圖3.6所示的分層結(jié)構(gòu)。例如,/arch/arm/Kconfig文件的結(jié)構(gòu)如下:
mainmenu "Linux Kernel Configuration" config ARM bool default y select HAVE_AOUT select HAVE_IDE select RTC_LIB select SYS_SUPPORTS_APM_EMULATION select HAVE_OPROFILE select HAVE_ARCH_KGDB select HAVE_KPROBES if (!XIP_KERNEL) select HAVE_KRETPROBES if (HAVE_KPROBES) select HAVE_FUNCTION_TRACER if (!XIP_KERNEL) select HAVE_GENERIC_DMA_COHERENT help The ARM series is a line of low-power-consumption RISC chip designs licensed by ARM Ltd and targeted at embedded applications and handhelds such as the Compaq IPAQ. ARM-based PCs are no longer manufactured, but legacy ARM-based PC hardware remains popular in Europe. There is an ARM Linux project with a web page at <http://www.arm.linux.org.uk/>. ... config MMU bool default y ... config ARCH_S3C64XX bool "Samsung S3C64XX" select GENERIC_GPIO select HAVE_CLK help Samsung S3C64XX series based systems ... if ARCH_S3C64XX source "arch/arm/mach-s3c6400/Kconfig" source "arch/arm/mach-s3c6410/Kconfig" endif ...
3.4.2 Kconfig和Makefile
在Linux內(nèi)核中增加程序需要完成以下3項(xiàng)工作。
● 將編寫的源代碼拷入Linux內(nèi)核源代碼的相應(yīng)目錄。
● 在目錄的Kconfig文件中增加關(guān)于新源代碼對(duì)應(yīng)項(xiàng)目的編譯配置選項(xiàng)。
● 在目錄的Makefile文件中增加對(duì)新源代碼的編譯條目。
1.實(shí)例引導(dǎo):S3C6410處理器的RTC驅(qū)動(dòng)配置
在講解Kconfig和Makefile的語法之前,我們先利用兩個(gè)簡(jiǎn)單的實(shí)例引導(dǎo)讀者建立初步的認(rèn)識(shí)。
首先,在linux-2.6.28-samsung/drivers/rtc目錄中包含了S3C6410處理器的RTC設(shè)備驅(qū)動(dòng)源代碼rtc-s3c.c。
而在該目錄的Kconfig文件中包含關(guān)于RTC_DRV_S3C的配置項(xiàng)目:
config RTC_DRV_S3C tristate "Samsung S3C series SoC RTC" depends on ARCH_S3C2410 || ARCH_S3C64XX || ARCH_S5PC1XX || ARCH_S5P64XX help RTC (Realtime Clock) driver for the clock inbuilt into the Samsung S3C24XX series of SoCs. This can provide periodic interrupt rates from 1Hz to 64Hz for user programs, and wakeup from Alarm. The driver currently supports the common features on all the S3C24XX range, such as the S3C2410, S3C2412, S3C2413, S3C2440 and S3C2442. This driver can also be build as a module. If so, the module will be called rtc-s3c.
上述 Kconfig 文件的這段腳本意味著只有在 ARCH_S3C2410、ARCH_S3C64XX、ARCH_S5PC1XX或ARCH_S5P64XX項(xiàng)目之一被配置的情況下,才會(huì)出現(xiàn)RTC_DRV_S3C配置項(xiàng)目,這個(gè)配置項(xiàng)目為三態(tài)(可編譯入內(nèi)核,可不編譯,也可編譯為內(nèi)核模塊,選項(xiàng)分別為“Y”、“N”和“M”),菜單上顯示的字符串為“Samsung S3C series SoC RTC”,“help”后面的內(nèi)容為幫助信息。圖3.7顯示了RTC_DRV_S3C菜單以及其help在運(yùn)行make menuconfig時(shí)的情況。

圖3.7 Kconfig菜單項(xiàng)目與幫助信息
除了布爾型的配置項(xiàng)目外,還存在一種布爾型(bool)配置選項(xiàng),它意味著要么編譯入內(nèi)核,要么不編譯,選項(xiàng)為“Y”或“N”。
在目錄的Makefile中關(guān)于RTC_DRV_S3C的編譯腳本為:
obj-$(CONFIG_RTC_DRV_S3C) += rtc-s3c.o
上述腳本意味著如果RTC_DRV_S3C配置選項(xiàng)被選擇為“Y”或“M”,即obj-$(CONFIG_RTC_DRV_S3C)等同于obj-y或obj-m時(shí),則編譯rtc-s3c.c,選“Y”的情況直接會(huì)將生成的目標(biāo)代碼直接連接到內(nèi)核,為“M”的情況則會(huì)生成模塊rtc-s3c.ko;如果RTC_DRV_S3C配置選項(xiàng)被選擇為“N”,即obj-$(CONFIG_RTC_DRV_S3C)等同于obj-n時(shí),則不編譯rtc-s3c.c。
一般而言,驅(qū)動(dòng)工程師只會(huì)在內(nèi)核源代碼的drivers 目錄的相應(yīng)子目錄中增加新設(shè)備驅(qū)動(dòng)的源代碼,并增加或修改Kconfig配置腳本和Makefile腳本,完全仿照上述過程執(zhí)行即可。
2.Makefile
這里主要對(duì)內(nèi)核源代碼各級(jí)子目錄中的kbuild(內(nèi)核的編譯系統(tǒng))Makefile 進(jìn)行簡(jiǎn)單介紹,這部分是內(nèi)核模塊或設(shè)備驅(qū)動(dòng)的開發(fā)者最常接觸到的。
Makefile的語法包括如下幾個(gè)方面。
(1)目標(biāo)定義。
目標(biāo)定義就是用來定義哪些內(nèi)容要作為模塊編譯,哪些要編譯并連接進(jìn)內(nèi)核。
例如:
obj-y += foo.o
表示要由foo.c或者foo.s文件編譯得到foo.o并連接進(jìn)內(nèi)核,而obj-m則表示該文件要作為模塊編譯。除了y、m以外的obj-x形式的目標(biāo)都不會(huì)被編譯。
而更常見的做法是根據(jù).config文件的CONFIG_變量來決定文件的編譯方式,如:
obj-$(CONFIG_ISDN) += isdn.o obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o
除了obj-形式的目標(biāo)以外,還有l(wèi)ib-y library庫,hostprogs-y主機(jī)程序等目標(biāo),但是基本都應(yīng)用在特定的目錄和場(chǎng)合下。
(2)多文件模塊的定義。
最簡(jiǎn)單的Makefile如上一節(jié)一句話的形式就夠了,如果一個(gè)模塊由多個(gè)文件組成,會(huì)稍微復(fù)雜一些,這時(shí)候應(yīng)采用模塊名加-y或-objs后綴的形式來定義模塊的組成文件。如以下例子:
# # Makefile for the linux ext2-filesystem routines. # obj-$(CONFIG_EXT2_FS) += ext2.o ext2-y := balloc.o dir.o file.o fsync.o ialloc.o inode.o \ ioctl.o namei.o super.o symlink.o ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o ext2-$(CONFIG_EXT2_FS_POSIX_ACL) += acl.o ext2-$(CONFIG_EXT2_FS_SECURITY) += xattr_security.o ext2-$(CONFIG_EXT2_FS_XIP) += xip.o
模塊的名字為ext2,由balloc.o、dir.o、file.o等多個(gè)目標(biāo)文件最終鏈接生成ext2.o直至ext2.ko文件,并且是否包括 xattr.o、acl.o 等則取決于內(nèi)核配置文件的配置情況,例如,如果 CONFIG_EXT2_FS_POSIX_ACL被選擇,則編譯acl.c得到acl.o并最終鏈接進(jìn)ext2。
(3)目錄層次的迭代。
如下例:
obj-$(CONFIG_EXT2_FS) += ext2/
當(dāng)CONFIG_EXT2_FS 的值為y或m時(shí),kbuild將會(huì)把ext2目錄列入向下迭代的目標(biāo)中。
3.Kconfig
內(nèi)核配置腳本文件的語法也比較簡(jiǎn)單,主要包括如下幾個(gè)方面。
(1)菜單入口。
大多數(shù)的內(nèi)核配置選項(xiàng)都對(duì)應(yīng)Kconfig中的一個(gè)菜單入口:
config MODVERSIONS bool "Module versioning support" help Usually, you have to use modules compiled with your kernel. Saying Y here makes it ...
“config”關(guān)鍵字定義新的配置選項(xiàng),之后的幾行定義了該配置選項(xiàng)的屬性。配置選項(xiàng)的屬性包括類型、數(shù)據(jù)范圍、輸入提示、依賴關(guān)系、選擇關(guān)系及幫助信息和默認(rèn)值等。
每個(gè)配置選項(xiàng)都必須指定類型,類型包括 bool、tristate、string、hex和int,其中 tristate 和string 是兩種基本的類型,其他類型都基于這兩種基本類型。類型定義后可以緊跟輸入提示,下面的兩段腳本是等價(jià)的:
bool "Networking support"
和
bool prompt "Networking support"
輸入提示的一般格式為:
prompt <prompt> [if <expr>]
其中可選的if用來表示該提示的依賴關(guān)系。
默認(rèn)值的格式為:
default <expr> [if <expr>]
一個(gè)配置選項(xiàng)可以存在任意多個(gè)默認(rèn)值,這種情況下,只有第一個(gè)被定義的值是可用的。如果用戶不設(shè)置對(duì)應(yīng)的選項(xiàng),配置選項(xiàng)的值就是默認(rèn)值。
依賴關(guān)系的格式為:
depends on(或者requires) <expr>
如果定義了多重依賴關(guān)系,它們之間用“&&”間隔。依賴關(guān)系也可以應(yīng)用到該菜單中所有的其他選項(xiàng)(同樣接受if表達(dá)式),下面的兩段腳本是等價(jià)的:
bool "foo" if BAR default y if BAR
和
depends on BAR bool "foo" default y
選擇關(guān)系(也稱為反向依賴關(guān)系)的格式為:
select <symbol> [if <expr>]
A如果選擇了B,則在A被選中的情況下,B自動(dòng)被選中。
kbuild Makefile中的expr(表達(dá)式)定義為:
<expr> ::= <symbol> <symbol> '=' <symbol> <symbol> '!=' <symbol> '(' <expr> ')' '!' <expr> <expr> '&&' <expr> <expr> '||' <expr>
也就是說expr是由symbol、兩個(gè)symbol相等、兩個(gè)symbol不等以及expr的賦值、非、與或運(yùn)算構(gòu)成。而 symbol 分為兩類,一類是由菜單入口定義配置選項(xiàng)定義的非常數(shù) symbol,另一類是作為expr組成部分的常數(shù)symbol。
數(shù)據(jù)范圍的格式為:
range <symbol> <symbol> [if <expr>]
為int和hex類型的選項(xiàng)設(shè)置可以接受輸入值范圍,用戶只能輸入大于等于第一個(gè)symbol,小于等于第二個(gè)symbol的值。
幫助信息的格式為:
help(或---help---) 開始 … 結(jié)束
幫助信息完全靠文本縮進(jìn)識(shí)別結(jié)束。“---help---”和“help”在作用上沒有區(qū)別,設(shè)計(jì)“---help---”的初衷在于將文件中的配置邏輯與給開發(fā)人員的提示分開。
menuconfig關(guān)鍵字的作用與config類似,但它在config的基礎(chǔ)上要求所有的子選項(xiàng)作為獨(dú)立的行顯示。
(2)菜單結(jié)構(gòu)。
菜單入口在菜單樹結(jié)構(gòu)中的位置可由兩種方法決定。第一種方式為:
menu "Network device support" depends on NET config NETDEVICES … endmenu
所有處于“menu”和“endmenu”之間的菜單入口都會(huì)成為“Network device support”的子菜單。而且,所有子菜單選項(xiàng)都會(huì)繼承父菜單的依賴關(guān)系,比如,“Network device support”對(duì)“NET”的依賴會(huì)被加到了配置選項(xiàng)NETDEVICES的依賴列表中。
注意menu后面跟的“Network device support”項(xiàng)目?jī)H僅是1個(gè)菜單,沒有對(duì)應(yīng)真實(shí)的配置選項(xiàng),也不具備3種不同的狀態(tài)。這是它和config的區(qū)別。
另一種方式是通過分析依賴關(guān)系生成菜單結(jié)構(gòu)。如果菜單選項(xiàng)在一定程度上依賴于前面的選項(xiàng),它就能成為該選項(xiàng)的子菜單。如果父選項(xiàng)為“N”,子選項(xiàng)不可見;如果父選項(xiàng)可見,子選項(xiàng)才能可見。例如:
config MODULES bool "Enable loadable module support" config MODVERSIONS bool "Set version information on all module symbols" depends on MODULES comment "module support disabled" depends on !MODULES
MODVERSIONS直接依賴 MODULES,只有MODULES不為“n”時(shí),該選項(xiàng)才可見。
除此之外,Kconfig中還可能使用“choices … endchoice”、“comment”、“if…endif”這樣的語法結(jié)構(gòu)。其中“choices … endchoice”的結(jié)構(gòu)為:
choice <choice options> <choice block> endchoice"
它定義一個(gè)選擇群,其接受的選項(xiàng)(choice options)可以是前面描述的任何屬性,例如 LDD6410的VGA輸出分辨率可以是1024×768或者800×600,在drivers/video/samsung/Kconfig就定義了如下的choice:
choice depends on FB_S3C_VGA prompt "Select VGA Resolution for S3C Framebuffer" default FB_S3C_VGA_1024_768 config FB_S3C_VGA_1024_768 bool "1 024*768@60Hz" ---help--- TBA config FB_S3C_VGA_640_480 bool "640*480@60Hz" ---help--- TBA endchoice
Kconfig配置腳本和Makefile腳本編寫的更詳細(xì)信息,可以分別參看內(nèi)核文檔Documentation目錄的kbuild子目錄下的Kconfig-language.txt和Makefiles.txt文件。
4.應(yīng)用實(shí)例:在內(nèi)核中新增驅(qū)動(dòng)代碼目錄和子目錄
下面來看一個(gè)綜合實(shí)例,假設(shè)我們要在內(nèi)核源代碼drivers目錄下為ARM體系結(jié)構(gòu)新增如下用于test driver的樹型目錄:
|--test |-- cpu | -- cpu.c |-- test.c |-- test_client.c |-- test_ioctl.c |-- test_proc.c |-- test_queue.c
在內(nèi)核中增加目錄和子目錄,我們需為相應(yīng)的新增目錄創(chuàng)建Makefile和Kconfig文件,而新增目錄的父目錄中的Kconfig和Makefile也需修改,以便新增的Kconfig和Makefile能被引用。
在新增的test目錄下,應(yīng)該包含如下Kconfig文件:
# # TEST driver configuration # menu "TEST Driver " comment " TEST Driver" config CONFIG_TEST bool "TEST support " config CONFIG_TEST_USER tristate "TEST user-space interface" depends on CONFIG_TEST endmenu
由于 test driver 對(duì)于內(nèi)核來說是新的功能,所以需首先創(chuàng)建一個(gè)菜單TEST Driver。然后,顯示“TEST support”,等待用戶選擇;接下來判斷用戶是否選擇了 TEST Driver,如果是(CONFIG_TEST=y),則進(jìn)一步顯示子功能:用戶接口與CPU功能支持;由于用戶接口功能可以被編譯成內(nèi)核模塊,所以這里的詢問語句使用了 tristate。
為了使這個(gè)Kconfig能起作用,修改arch/arm/Kconfig文件,增加:
source "drivers/test/Kconfig"
腳本中的source意味著引用新的Kconfig文件。
在新增的test目錄下,應(yīng)該包含如下Makefile文件:
# drivers/test/Makefile # # Makefile for the TEST. # obj-$(CONFIG_TEST) += test.o test_queue.o test_client.o obj-$(CONFIG_TEST_USER) += test_ioctl.o obj-$(CONFIG_PROC_FS) += test_proc.o obj-$(CONFIG_TEST_CPU) += cpu/
該腳本根據(jù)配置變量的取值,構(gòu)建 obj-*列表。由于 test 目錄中包含一個(gè)子目錄 cpu,當(dāng)CONFIG_TEST_CPU=y時(shí),需要將cpu目錄加入列表。
test目錄中的cpu子目錄也需包含如下的Makefile:
# drivers/test/test/Makefile # # Makefile for the TEST CPU # obj-$(CONFIG_TEST_CPU) += cpu.o
為了使得整個(gè)test目錄能夠被編譯命令作用到,test目錄父目錄中的Makefile也需新增如下腳本:
obj-$(CONFIG_TEST) += test/
在 drivers/Makefile 中加入obj-$(CONFIG_TEST) += test/,使得在用戶在進(jìn)行內(nèi)核編譯時(shí)能夠進(jìn)入test目錄。
增加了Kconfig和Makefile之后的新的test樹型目錄為:
|--test |-- cpu | -- cpu.c | -- Makefile |-- test.c |-- test_client.c |-- test_ioctl.c |-- test_proc.c |-- test_queue.c |-- Makefile |-- Kconfig
3.4.3 Linux內(nèi)核的引導(dǎo)
引導(dǎo)Linux系統(tǒng)的過程包括很多階段,這里將以引導(dǎo)X86 PC為例來進(jìn)行講解。引導(dǎo)X86 PC上的Linux的過程和引導(dǎo)嵌入式系統(tǒng)上的Linux的過程基本類似。不過在X86 PC上有一個(gè)從BIOS (基本輸入/輸出系統(tǒng))轉(zhuǎn)移到Bootloader的過程,而嵌入式系統(tǒng)往往復(fù)位后就直接運(yùn)行Bootloader。
圖3.8所示為X86 PC上從上電/復(fù)位到運(yùn)行Linux用戶空間初始進(jìn)程的流程。在進(jìn)入與Linux相關(guān)代碼之間,會(huì)經(jīng)歷如下階段。

圖3.8 X86 PC上的Linux引導(dǎo)流程
(1)當(dāng)系統(tǒng)上電或復(fù)位時(shí),CPU會(huì)將PC指針賦值為一個(gè)特定的地址0xFFFF0并執(zhí)行該地址處的指令。在PC機(jī)中,該地址位于BIOS中,它保存在主板上的ROM或Flash中。
(2)BIOS運(yùn)行時(shí)按照CMOS的設(shè)置定義的啟動(dòng)設(shè)備順序來搜索處于活動(dòng)狀態(tài)并且可以引導(dǎo)的設(shè)備。若從硬盤啟動(dòng),BIOS會(huì)將硬盤MBR(主引導(dǎo)記錄)中的內(nèi)容加載到RAM。MBR是一個(gè)512字節(jié)大小的扇區(qū),位于磁盤上的第一個(gè)扇區(qū)中(0道0柱面1扇區(qū))。當(dāng)MBR被加載到RAM中之后,BIOS就會(huì)將控制權(quán)交給 MBR。
(3)主引導(dǎo)加載程序查找并加載次引導(dǎo)加載程序。它在分區(qū)表中查找活動(dòng)分區(qū),當(dāng)找到一個(gè)活動(dòng)分區(qū)時(shí),掃描分區(qū)表中的其他分區(qū),以確保它們都不是活動(dòng)的。當(dāng)這個(gè)過程驗(yàn)證完成之后,就將活動(dòng)分區(qū)的引導(dǎo)記錄從這個(gè)設(shè)備中讀入RAM中并執(zhí)行它。
(4)次引導(dǎo)加載程序加載Linux內(nèi)核和可選的初始RAM磁盤,將控制權(quán)交給Linux內(nèi)核源代碼。(5)運(yùn)行被加載的內(nèi)核,并啟動(dòng)用戶空間應(yīng)用程序。
嵌入式系統(tǒng)中Linux的引導(dǎo)過程與之類似,但一般更加簡(jiǎn)潔。不論具體以怎樣的方式實(shí)現(xiàn),只要具備如下特征就可以稱其為Bootloader。
● 可以在系統(tǒng)上電或復(fù)位的時(shí)候以某種方式執(zhí)行,這些方式包括被 BIOS 引導(dǎo)執(zhí)行、直接在NOR Flash中執(zhí)行、NAND Flash中的代碼被MCU自動(dòng)拷入內(nèi)部或外部RAM執(zhí)行等。
● 能將U盤、磁盤、光盤、NOR/NAND Flash、ROM、SD卡等存儲(chǔ)介質(zhì),甚或網(wǎng)口、串口中的操作系統(tǒng)加載到RAM并把控制權(quán)交給操作系統(tǒng)源代碼執(zhí)行。
完成上述功能的Bootloader的實(shí)現(xiàn)方式非常多樣化,甚至本身也可以是一個(gè)簡(jiǎn)化版的操作系統(tǒng)。著名的Linux Bootloader包括應(yīng)用于PC的LILO和GRUB,應(yīng)用于嵌入式系統(tǒng)的U-Boot、RedBoot等。
相比較于LILO,GRUB本身能理解EXT2、EXT3文件系統(tǒng),因此可在文件系統(tǒng)中加載Linux,而LILO只能識(shí)別“裸扇區(qū)”。
U-Boot的定位為“Universal Bootloader”,其功能比較強(qiáng)大,涵蓋了包括PowerPC、ARM、MIPS和X86在內(nèi)的絕大部分處理器構(gòu)架,提供網(wǎng)卡、串口、Flash等外設(shè)驅(qū)動(dòng),提供必要的網(wǎng)絡(luò)協(xié)議(BOOTP、DHCP、TFTP),能識(shí)別多種文件系統(tǒng)(cramfs、fat、jffs2和registerfs等),并附帶了調(diào)試、腳本、引導(dǎo)等工具,應(yīng)用十分廣泛。
Redboot是Redhat公司隨eCos發(fā)布的Bootloader開源項(xiàng)目,除了包含U-Boot類似的強(qiáng)大功能外,它還包含GDB stub(插樁),因此能通過串口或網(wǎng)口與GDB進(jìn)行通信,調(diào)試GCC產(chǎn)生的任何程序(包括內(nèi)核)。
我們有必要對(duì)上述流程的第5個(gè)階段進(jìn)行更詳細(xì)的分析,它完成啟動(dòng)內(nèi)核并運(yùn)行用戶空間的init進(jìn)程。
當(dāng)內(nèi)核映像被加載到RAM之后,Bootloader的控制權(quán)被釋放,內(nèi)核階段就開始了。內(nèi)核映像并不是完全可直接執(zhí)行的目標(biāo)代碼,而是一個(gè)壓縮過的zImage(小內(nèi)核)或bzImage(大內(nèi)核, bzImage中的b是“big”的意思)。
但是,并非zImage和bzImage映像中的一切都被壓縮了,否則Bootloader把控制權(quán)交給這個(gè)內(nèi)核映像它就“傻”了。實(shí)際上,映像中包含未被壓縮的部分,這部分中包含解壓縮程序,解壓縮程序會(huì)解壓映像中被壓縮的部分。zImage和bzImage都是用gzip壓縮的,它們不僅是一個(gè)壓縮文件,而且在這兩個(gè)文件的開頭部分內(nèi)嵌有g(shù)zip解壓縮代碼。
如圖3.9所示,當(dāng)bzImage(用于i386映像)被調(diào)用時(shí),它從/arch/i386/boot/head.S的start匯編例程開始執(zhí)行。這個(gè)程序執(zhí)行一些基本的硬件設(shè)置,并調(diào)用/arch/i386/boot/compressed/head.S中的startup_32例程。startup_32程序設(shè)置一些基本的運(yùn)行環(huán)境(如堆棧)后,清除BSS段,調(diào)用/arch/i386/boot/compressed/misc.c中的decompress_kernel() C函數(shù)解壓內(nèi)核。內(nèi)核被解壓到內(nèi)存中之后,會(huì)再調(diào)用/arch/i386/kernel/head.S文件中的startup_32例程,這個(gè)新的startup_32例程(稱為清除程序或進(jìn)程 0)會(huì)初始化頁表,并啟用內(nèi)存分頁機(jī)制,接著為任何可選的浮點(diǎn)單元(FPU)檢測(cè)CPU 的類型,并將其存儲(chǔ)起來供以后使用。這些都做完之后,/init/main.c 中的start_kernel()函數(shù)被調(diào)用,進(jìn)入與體系結(jié)構(gòu)無關(guān)的Linux內(nèi)核部分。

圖3.9 X86 PC上的Linux內(nèi)核初始化
start_kernel()會(huì)調(diào)用一系列初始化函數(shù)來設(shè)置中斷,執(zhí)行進(jìn)一步的內(nèi)存配置。之后,/arch/i386/kernel/process.c中kernel_thread()被調(diào)用以啟動(dòng)第一個(gè)核心線程,該線程執(zhí)行init()函數(shù),而原執(zhí)行序列會(huì)調(diào)用cpu_idle()等待調(diào)度。
作為核心線程的init()函數(shù)完成外設(shè)及其驅(qū)動(dòng)程序的加載和初始化,掛接根文件系統(tǒng)。init()打開/dev/console設(shè)備,重定向stdin、stdout和stderr到控制臺(tái)。之后,它搜索文件系統(tǒng)中的init程序(也可以由“init=”命令行參數(shù)指定init程序),并使用execve()系統(tǒng)調(diào)用執(zhí)行init程序。搜索 init 程序的順序?yàn)椋?sbin/init、/etc/init、/bin/init 和/bin/sh。在嵌入式系統(tǒng)中,多數(shù)情況下,可以給內(nèi)核傳入一個(gè)簡(jiǎn)單的shell 腳本來啟動(dòng)必需的嵌入式應(yīng)用程序。
至此,漫長(zhǎng)的Linux內(nèi)核引導(dǎo)和啟動(dòng)過程就此結(jié)束,而init()對(duì)應(yīng)的這個(gè)由start_kernel()創(chuàng)建的第一個(gè)線程也進(jìn)入用戶模式。
3.5 Linux下的C編程特點(diǎn)
3.5.1 Linux編碼風(fēng)格
Linux程序的命名習(xí)慣和Windows程序的命名習(xí)慣及著名的匈牙利命名法有很大的不同。在Windows程序中,習(xí)慣以如下方式命名宏、變量和函數(shù):
#define PI 3.141 592 6 /*用大寫字母代表宏*/ int minValue, maxValue; /*變量:第一個(gè)單詞全寫,其后的單詞第一個(gè)字母小寫*/ void SendData(void); /*函數(shù):所有單詞第一個(gè)字母都大寫定義*/
這種命名方式在程序員中非常盛行,意思表達(dá)清晰且避免了匈牙利法的臃腫,單詞之間通過首字母大寫來區(qū)分。通過第1個(gè)單詞的首字母是否大寫可以區(qū)分名稱屬于變量還是屬于函數(shù),而看到整串的大寫字母可以斷定為宏。實(shí)際上,Windows的命名習(xí)慣并非僅限于Windows編程,大多數(shù)領(lǐng)域的程序開發(fā)都遵照此習(xí)慣。
但是Linux不以這種習(xí)慣命名,對(duì)應(yīng)于上面的一段程序,在Linux中會(huì)被命名為:
#define PI 3.141 592 6 int min_value, max_value; void send_data(void);
上述命名方式中,下劃線大行其道,不依照Windows所采用的首字母大寫以區(qū)分單詞的方式。Linux 的命名習(xí)慣與 Windows命名習(xí)慣各有千秋,但是既然本書和本書的讀者立足于編寫 Linux程序,代碼風(fēng)格理應(yīng)保持與Linux開發(fā)社區(qū)的一致性。
Linux的代碼縮進(jìn)使用“TAB”(8個(gè)字符)。
Linux的代碼括號(hào)“{”和“}”的使用原則如下。
(1)對(duì)于結(jié)構(gòu)體、if/for/while/switch語句,“{”不另起一行,例如:
struct var_data { int len; char data[0]; }; if (a == b) { a = c; d = a; } for (i = 0; i < 10; i++) { a = c; d = a; }
(2)如果if、for循環(huán)后只有1行,不要加“{”和“}”,例如:
for (i = 0; i < 10; i++) { a = c; }
應(yīng)該改為:
for (i = 0; i < 10; i++) a = c;
(3)if和else混用的情況下,else語句不另起一行,例如:
if (x == y) { ... } else if (x > y) { ... } else { ... }
(4)對(duì)于函數(shù),“{”另起一行,譬如:
int add(int a, int b) { return a + b; }
在switch/case語句方面,Linux建議switch和case對(duì)齊,例如:
switch (suffix) { case 'G': case 'g': mem <<= 30; break; case 'M': case 'm': mem <<= 20; break; case 'K': case 'k': mem <<= 10; /* fall through */ default: break; }
內(nèi)核下的Documentation/CodingStyle 描述了 Linux 內(nèi)核對(duì)編碼風(fēng)格的要求,內(nèi)核下的scripts/checkpatch.pl提供了1個(gè)檢查代碼風(fēng)格的腳本。如果我們使用scripts/checkpatch.pl檢查包含如下代碼塊的源程序:
for (i = 0; i < 10; i++) { a = c; }
就會(huì)產(chǎn)生“WARNING: braces {} are not necessary for single statement blocks”的警告。
另外,請(qǐng)注意代碼中空格的應(yīng)用,譬如“for?(i?=?0; ?i?<?10; ?i++)?{”語句中“?”都是空格。
3.5.2 GNU C與ANSI C
Linux上可用的C編譯器是GNU C編譯器,它建立在自由軟件基金會(huì)的編程許可證的基礎(chǔ)上,因此可以自由發(fā)布。GNU C對(duì)標(biāo)準(zhǔn)C進(jìn)行一系列擴(kuò)展,以增強(qiáng)標(biāo)準(zhǔn)C的功能。
1.零長(zhǎng)度和變量長(zhǎng)度數(shù)組
GNU C允許使用零長(zhǎng)度數(shù)組,在定義變長(zhǎng)對(duì)象的頭結(jié)構(gòu)時(shí),這個(gè)特性非常有用。例如:
struct var_data { int len; char data[0]; };
char data[0]僅僅意味著程序中通過var_data結(jié)構(gòu)體實(shí)例的data[index]成員可以訪問len之后的第index個(gè)地址,它并沒有為data[]數(shù)組分配內(nèi)存,因此sizeof(struct var_data)=sizeof(int)。
假設(shè)struct var_data的數(shù)據(jù)域就保存在struct var_data緊接著的內(nèi)存區(qū)域,則通過如下代碼可以遍歷這些數(shù)據(jù):
struct var_data s; ... for (i = 0; i < s.len; i++) printf("%02x", s.data[i]);
GNU C中也可以使用1個(gè)變量定義數(shù)組,例如如下代碼中定義的“double x[n]”:
int main (int argc, char *argv[]) { int i, n = argc; double x[n]; for (i = 0; i < n; i++) x[i] = i; return 0; }
2.case范圍
GNU C支持case x…y這樣的語法,區(qū)間[x,y]的數(shù)都會(huì)滿足這個(gè)case的條件,請(qǐng)看下面的代碼:
switch (ch) { case '0'... '9': c -= '0'; break; case 'a'... 'f': c -= 'a' - 10; break; case 'A'... 'F': c -= 'A' - 10; break; }
代碼中的case '0'... '9'等價(jià)于標(biāo)準(zhǔn)C中的:
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
3.語句表達(dá)式
GNU C把包含在括號(hào)中的復(fù)合語句看做是一個(gè)表達(dá)式,稱為語句表達(dá)式,它可以出現(xiàn)在任何允許表達(dá)式的地方。我們可以在語句表達(dá)式中使用原本只能在復(fù)合語句中使用的循環(huán)、局部變量等,例如:
#define min_t(type,x,y) \ ({ type __x = (x); type __y = (y); __x < __y ? __x: __y; }) int ia, ib, mini; float fa, fb, minf; mini = min_t(int, ia, ib); minf = min_t(float, fa, fb);
因?yàn)橹匦露x了_ _xx和_ _y這兩個(gè)局部變量,所以以上述方式定義的宏將不會(huì)有副作用。在標(biāo)準(zhǔn)C中,對(duì)應(yīng)的如下宏則會(huì)產(chǎn)生副作用:
#define min(x,y) ((x) < (y) ? (x) : (y))
代碼min(++ia,++ib)會(huì)被展開為((++ia) < (++ib) ? (++ia): (++ib)),傳入宏的“參數(shù)”被增加2次。
4.typeof關(guān)鍵字
typeof(x)語句可以獲得x的類型,因此,我們可以借助typeof重新定義min這個(gè)宏:
#define min(x,y) ({ \ const typeof(x) _x = (x); \ const typeof(y) _y = (y); \ (void) (&_x == &_y); \ _x < _y ? _x : _y; })
我們不需要像min_t(type,x,y)這個(gè)宏那樣把type傳入,因?yàn)橥ㄟ^typeof(x)、typeof(y)可以獲得type。代碼行(void) (&_x == &_y)的作用是檢查_x和_y的類型是否一致。
5.可變參數(shù)宏
標(biāo)準(zhǔn)C就支持可變參數(shù)函數(shù),意味著函數(shù)的參數(shù)是不固定的,例如printf()函數(shù)的原型為:
int printf( const char *format [, argument]... );
而在 GNU C中,宏也可以接受可變數(shù)目的參數(shù),例如:
#define pr_debug(fmt,arg...) \ printk(fmt,##arg)
這里arg表示其余的參數(shù),可以是零個(gè)或多個(gè),這些參數(shù)以及參數(shù)之間的逗號(hào)構(gòu)成arg的值,在宏擴(kuò)展時(shí)替換arg,例如下列代碼:
pr_debug("%s:%d",filename,line)
會(huì)被擴(kuò)展為:
printk("%s:%d", filename, line)
使用“##”的原因是處理 arg 不代表任何參數(shù)的情況,這時(shí)候,前面的逗號(hào)就變得多余了。使用“##”之后,GNU C預(yù)處理器會(huì)丟棄前面的逗號(hào),這樣,代碼:
pr_debug("success!\n")
會(huì)被正確地?cái)U(kuò)展為:
printk("success!\n")
而不是:
printk("success!\n",)
這正是我們希望看到的。
6.標(biāo)號(hào)元素
標(biāo)準(zhǔn)C要求數(shù)組或結(jié)構(gòu)體的初始化值必須以固定的順序出現(xiàn),在GNU C中,通過指定索引或結(jié)構(gòu)體成員名,允許初始化值以任意順序出現(xiàn)。
指定數(shù)組索引的方法是在初始化值前添加“[INDEX] =”,當(dāng)然也可以用“[FIRST ... LAST] =”的形式指定一個(gè)范圍。例如,下面的代碼定義一個(gè)數(shù)組,并把其中的所有元素賦值為0:
unsigned char data[MAX] = { [0 ... MAX-1] = 0 };
下面的代碼借助結(jié)構(gòu)體成員名初始化結(jié)構(gòu)體:
struct file_operations ext2_file_operations = { llseek: generic_file_llseek, read: generic_file_read, write: generic_file_write, ioctl: ext2_ioctl, mmap: generic_file_mmap, open: generic_file_open, release: ext2_release_file, fsync: ext2_sync_file, };
但是,Linux2.6推薦類似的代碼應(yīng)該盡量采用標(biāo)準(zhǔn)C的方式:
struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = generic_file_read, .write = generic_file_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .ioctl = ext2_ioctl, .mmap = generic_file_mmap, .open = generic_file_open, .release= ext2_release_file, .fsync = ext2_sync_file, .readv = generic_file_readv, .writev = generic_file_writev, .sendfile = generic_file_sendfile, };
7.當(dāng)前函數(shù)名
GNU C預(yù)定義了兩個(gè)標(biāo)志符保存當(dāng)前函數(shù)的名字,_ _FUNCTION_ _保存函數(shù)在源碼中的名字,_ _PRETTY_FUNCTION_ _保存帶語言特色的名字。在C函數(shù)中,這兩個(gè)名字是相同的。
void example() { printf("This is function:%s", __FUNCTION__); }
代碼中的_ _FUNCTION_ _意味著字符串“example”。C99已經(jīng)支持_ _func_ _宏,因此建議在Linux編程中不再使用_ _FUNCTION_ _,而轉(zhuǎn)而使用_ _func_ _:
void example() { printf("This is function:%s", __func__); }
8.特殊屬性聲明
GNU C允許聲明函數(shù)、變量和類型的特殊屬性,以便進(jìn)行手工的代碼優(yōu)化和定制代碼檢查的方法。要指定一個(gè)聲明的屬性,只需要在聲明后添加_ _attribute_ _ (( ATTRIBUTE ))。其中ATTRIBUTE為屬性說明,如果存在多個(gè)屬性,則以逗號(hào)分隔。GNU C支持noreturn、format、section、aligned、packed等十多個(gè)屬性。
noreturn 屬性作用于函數(shù),表示該函數(shù)從不返回。這會(huì)讓編譯器優(yōu)化代碼,并消除不必要的警告信息。例如:
# define ATTRIB_NORET __attribute__((noreturn)) .... asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
format 屬性也用于函數(shù),表示該函數(shù)使用 printf、scanf或strftime 風(fēng)格的參數(shù),指定 format屬性可以讓編譯器根據(jù)格式串檢查參數(shù)類型。例如:
asmlinkage int printk(const char * fmt, ...) __attribute__ ((format (printf, 1, 2)));
上述代碼中的第1個(gè)參數(shù)是格式串,從第2個(gè)參數(shù)開始都會(huì)根據(jù)printf()函數(shù)的格式串規(guī)則檢查參數(shù)。
unused屬性作用于函數(shù)和變量,表示該函數(shù)或變量可能不會(huì)被用到,這個(gè)屬性可以避免編譯器產(chǎn)生警告信息。
aligned屬性用于變量、結(jié)構(gòu)體或聯(lián)合體,指定變量、結(jié)構(gòu)體或聯(lián)合體的對(duì)界方式,以字節(jié)為單位,例如:
struct example_struct { char a; int b; long c; } __attribute_ _((aligned(4)));
表示該結(jié)構(gòu)類型的變量以4字節(jié)對(duì)界。
packed屬性作用于變量和類型,用于變量或結(jié)構(gòu)體成員時(shí)表示使用最小可能的對(duì)界,用于枚舉、結(jié)構(gòu)體或聯(lián)合體類型時(shí)表示該類型使用最小的內(nèi)存。例如:
struct example_struct { char a; int b; long c __attribute__((packed)); };
編譯器對(duì)結(jié)構(gòu)體成員及變量對(duì)界的目的是為了更快地訪問結(jié)構(gòu)體成員及變量占據(jù)的內(nèi)存。例如,對(duì)于一個(gè)32位的整型變量,若以4字節(jié)方式存放(即低兩位地址為00),則CPU在一個(gè)總線周期內(nèi)就可以讀取32位;若不然,CPU需要兩次總線周期才能組合為一個(gè)32位整型。
9.內(nèi)建函數(shù)
GNU C提供了大量的內(nèi)建函數(shù),其中大部分是標(biāo)準(zhǔn)C庫函數(shù)的GNU C編譯器內(nèi)建版本,例如memcpy()等,它們與對(duì)應(yīng)的標(biāo)準(zhǔn)C庫函數(shù)功能相同。
不屬于庫函數(shù)的其他內(nèi)建函數(shù)的命名通常以_ _builtin開始,如下所示。
● 內(nèi)建函數(shù)_ _builtin_return_address (LEVEL)返回當(dāng)前函數(shù)或其調(diào)用者的返回地址,參數(shù)LEVEL 指定調(diào)用棧的級(jí)數(shù),如0表示當(dāng)前函數(shù)的返回地址,1表示當(dāng)前函數(shù)的調(diào)用者的返回地址。
● 內(nèi)建函數(shù)_ _builtin_constant_p(EXP)用于判斷一個(gè)值是否為編譯時(shí)常數(shù),如果參數(shù) EXP的值是常數(shù),函數(shù)返回1,否則返回0。
● 內(nèi)建函數(shù)_ _builtin_expect(EXP, C)用于為編譯器提供分支預(yù)測(cè)信息,其返回值是整數(shù)表達(dá)式EXP的值,C的值必須是編譯時(shí)常數(shù)。
例如,下面的代碼檢測(cè)第1個(gè)參數(shù)是否為編譯時(shí)常數(shù)以確定采用參數(shù)版本還是非參數(shù)版本的代碼:
#define test_bit(nr,addr) \ (__builtin_constant_p(nr) ? \ constant_test_bit((nr),(addr)) : \ variable_test_bit((nr),(addr)))
在使用gcc編譯C程序的時(shí)候,如果使用“-ansi -pedantic”編譯選項(xiàng),則會(huì)告訴編譯器不使用GNU擴(kuò)展語法。例如對(duì)于如下C程序test.c:
struct var_data { int len; char data[0]; }; struct var_data a;
直接編譯可以通過:
gcc -c test.c
如果使用“-ansi -pedantic”編譯選項(xiàng),編譯會(huì)報(bào)警:
gcc -ansi -pedantic -c test.c test.c:3: warning: ISO C forbids zero-size array ‘data’
3.5.3 do { } while(0)
在Linux內(nèi)核中,經(jīng)常會(huì)看到do {} while(0)這樣的語句,許多人開始都會(huì)疑惑,認(rèn)為do {}while(0)毫無意義,因?yàn)樗粫?huì)執(zhí)行一次,加不加 do {} while(0)效果是完全一樣的,其實(shí) do {}while(0)的用法主要用于宏定義中。
這里用一個(gè)簡(jiǎn)單點(diǎn)的宏來演示:
#define SAFE_FREE(p) do{ free(p); p = NULL;} while(0)
假設(shè)這里去掉do...while(0),即定義SAFE_DELETE為:
#define SAFE_FREE(p) free(p); p = NULL;
那么以下代碼
if(NULL != p) SAFE_DELETE(p) else .../* do something */
會(huì)被展開為:
if(NULL != p) free(p); p = NULL; else .../* do something */
展開的代碼中存在兩個(gè)問題。
(1)因?yàn)閕f分支后有兩個(gè)語句,導(dǎo)致else分支沒有對(duì)應(yīng)的if,編譯失敗。
(2)假設(shè)沒有else分支,則SAFE_FREE中的第二個(gè)語句無論if測(cè)試是否通過,都會(huì)執(zhí)行。
的確,將SAFE_FREE的定義加上{}就可以解決上述問題了,即:
#define SAFE_FREE(p) { free(p); p = NULL;}
這樣,代碼:
if(NULL != p) SAFE_DELETE(p) else ... /* do something */
會(huì)被展開為:
if(NULL != p) { free(p); p = NULL; } else ... /* do something */
但是,在C程序中,每個(gè)語句后面加分號(hào)是一種約定俗成的習(xí)慣,那么,如下代碼:
if(NULL != p) SAFE_DELETE(p); else ... /* do something */
將被擴(kuò)展為:
if(NULL != p) { free(p); p = NULL; }; else ... /* do something */
這樣,else分支就又沒有對(duì)應(yīng)的if了,編譯將無法通過。假設(shè)用了do {} while(0),情況就不一樣了,同樣的代碼會(huì)被展開為:
if(NULL != p) do{ free(p); p = NULL;} while(0); else ... /* do something */
不會(huì)再出現(xiàn)編譯問題。do while(0)的使用完全是為了保證宏定義的使用者能無編譯錯(cuò)誤地使用宏,它不對(duì)其使用者做任何假設(shè)。
3.5.4 goto
用不用goto一直是一個(gè)著名的爭(zhēng)議話題,Linux內(nèi)核源代碼中對(duì)goto的應(yīng)用非常廣泛,但是一般只限于錯(cuò)誤處理中,其結(jié)構(gòu)如:
if(register_a()!=0) goto err; if(register_b()!=0) goto err1; if(register_c()!=0) goto err2; if(register_d()!=0) goto err3; ... err3: unregister_c(); err2: unregister_b(); err1: unregister_a(); err: return ret;
這種goto用于錯(cuò)誤處理的用法實(shí)在是簡(jiǎn)單而高效,只需保證在錯(cuò)誤處理時(shí)注銷、資源釋放等與正常的注冊(cè)、資源申請(qǐng)順序相反。
3.6 總結(jié)
本章主要講解了Linux內(nèi)核和Linux內(nèi)核編程的基礎(chǔ)知識(shí),為進(jìn)行Linux驅(qū)動(dòng)開發(fā)打下軟件基礎(chǔ)。
在Linux內(nèi)核方面,主要介紹了Linux內(nèi)核的發(fā)展史、組成、特點(diǎn)、源代碼結(jié)構(gòu)、內(nèi)核編譯方法及內(nèi)核引導(dǎo)過程。
由于Linux驅(qū)動(dòng)編程本質(zhì)屬于內(nèi)核編程,因此掌握內(nèi)核編程的基礎(chǔ)知識(shí)顯得尤為重要。本章在這方面主要講解了在內(nèi)核中新增程序及目錄和編寫Kconfig和Makefile的方法,并分析了Linux下C編程習(xí)慣以及Linux所使用的GNU C針對(duì)標(biāo)準(zhǔn)C的擴(kuò)展語法。
- Mastering ElasticSearch
- 每天5分鐘玩轉(zhuǎn)Kubernetes
- Learning Windows Server Containers
- Mastering Distributed Tracing
- Linux網(wǎng)絡(luò)內(nèi)核分析與開發(fā)
- 云原生落地:產(chǎn)品、架構(gòu)與商業(yè)模式
- Cassandra 3.x High Availability(Second Edition)
- 鴻蒙操作系統(tǒng)設(shè)計(jì)原理與架構(gòu)
- μC/OS-III內(nèi)核實(shí)現(xiàn)與應(yīng)用開發(fā)實(shí)戰(zhàn)指南:基于STM32
- iOS 10快速開發(fā):18天零基礎(chǔ)開發(fā)一個(gè)商業(yè)應(yīng)用
- 完美應(yīng)用Ubuntu(第2版)
- Linux內(nèi)核修煉之道
- Linux操作系統(tǒng)案例教程(第2版)
- 辦公自動(dòng)化教程(Windows7+Office2010)
- 鴻蒙HarmonyOS應(yīng)用開發(fā)入門