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

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

圖3.1 Linux內核的組成部分與關系
如圖3.2所示,Linux 的進程在幾個狀態間進行切換。在設備驅動編程中,當請求的資源不能得到滿足時,驅動一般會調度其他進程執行,并使本進程進入睡眠狀態,直到它請求的資源被釋放,才會被喚醒而進入就緒態。睡眠分成可被打斷的睡眠和不可被打斷的睡眠,兩者的區別在于可被打斷的睡眠在收到信號的時候會醒。

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

圖3.3 Linux進程地址空間
3.虛擬文件系統
如圖3.4所示,Linux虛擬文件系統(VFS)隱藏各種了硬件的具體細節,為所有的設備提供了統一的接口。而且,它獨立于各個具體的文件系統,是對各種文件系統的一個抽象,它使用超級塊super block存放文件系統相關信息,使用索引節點inode存放文件的物理信息,使用目錄項dentry存放文件的邏輯信息。

圖3.4 Linux文件系統
4.網絡接口
網絡接口提供了對各種網絡標準的存取和各種網絡硬件的支持。如圖3.5所示,在Linux中網絡接口可分為網絡協議和網絡驅動程序,網絡協議部分負責實現每一種可能的網絡傳輸協議,網絡設備驅動程序負責與硬件設備通信,每一種可能的硬件設備都有相應的設備驅動程序。

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

圖3.6 Linux內核編譯配置
內核配置包含的項目相當多,arch/arm/configs/ldd6410lcd_defconfig文件包含了LDD6410 的默認配置,因此,只需要運行make ldd6410lcd_defconfig就可以為LDD6410開發板配置內核。
編譯內核和模塊的方法是:
make zImage make modules
執行完上述命令后,在源代碼的根目錄下會得到未壓縮的內核映像vmlinux和內核符號表文件System.map,在arch/arm/boot/目錄會得到壓縮的內核映像 zImage,在內核各對應目錄得到選中的內核模塊。
Linux2.6內核的配置系統由以下3個部分組成。
● Makefile:分布在Linux內核源代碼中的Makefile,定義Linux內核的編譯規則。
● 配置文件(Kconfig):給用戶提供配置選擇的功能。
● 配置工具:包括配置命令解釋器(對配置腳本中使用的配置命令進行解釋)和配置用戶界面(提供基于字符界面和圖形界面)。這些配置工具都是使用腳本語言,如 Tcl/TK、Perl等編寫。
使用make config、make menuconfig等命令后,會生成一個.config配置文件,記錄哪些部分被編譯入內核、哪些部分被編譯為內核模塊。
運行make menuconfig等時,配置工具首先分析與體系結構對應的/arch/xxx/Kconfig文件(xxx即為傳入的ARCH 參數),/arch/xxx/Kconfig 文件中除本身包含一些與體系結構相關的配置項和配置菜單以外,還通過source語句引入了一系列Kconfig文件,而這些Kconfig又可能再次通過source引入下一層的Kconfig,配置工具依據這些Kconfig包含的菜單和項目即可描繪出一個如圖3.6所示的分層結構。例如,/arch/arm/Kconfig文件的結構如下:
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內核中增加程序需要完成以下3項工作。
● 將編寫的源代碼拷入Linux內核源代碼的相應目錄。
● 在目錄的Kconfig文件中增加關于新源代碼對應項目的編譯配置選項。
● 在目錄的Makefile文件中增加對新源代碼的編譯條目。
1.實例引導:S3C6410處理器的RTC驅動配置
在講解Kconfig和Makefile的語法之前,我們先利用兩個簡單的實例引導讀者建立初步的認識。
首先,在linux-2.6.28-samsung/drivers/rtc目錄中包含了S3C6410處理器的RTC設備驅動源代碼rtc-s3c.c。
而在該目錄的Kconfig文件中包含關于RTC_DRV_S3C的配置項目:
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項目之一被配置的情況下,才會出現RTC_DRV_S3C配置項目,這個配置項目為三態(可編譯入內核,可不編譯,也可編譯為內核模塊,選項分別為“Y”、“N”和“M”),菜單上顯示的字符串為“Samsung S3C series SoC RTC”,“help”后面的內容為幫助信息。圖3.7顯示了RTC_DRV_S3C菜單以及其help在運行make menuconfig時的情況。

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

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

圖3.9 X86 PC上的Linux內核初始化
start_kernel()會調用一系列初始化函數來設置中斷,執行進一步的內存配置。之后,/arch/i386/kernel/process.c中kernel_thread()被調用以啟動第一個核心線程,該線程執行init()函數,而原執行序列會調用cpu_idle()等待調度。
作為核心線程的init()函數完成外設及其驅動程序的加載和初始化,掛接根文件系統。init()打開/dev/console設備,重定向stdin、stdout和stderr到控制臺。之后,它搜索文件系統中的init程序(也可以由“init=”命令行參數指定init程序),并使用execve()系統調用執行init程序。搜索 init 程序的順序為:/sbin/init、/etc/init、/bin/init 和/bin/sh。在嵌入式系統中,多數情況下,可以給內核傳入一個簡單的shell 腳本來啟動必需的嵌入式應用程序。
至此,漫長的Linux內核引導和啟動過程就此結束,而init()對應的這個由start_kernel()創建的第一個線程也進入用戶模式。
3.5 Linux下的C編程特點
3.5.1 Linux編碼風格
Linux程序的命名習慣和Windows程序的命名習慣及著名的匈牙利命名法有很大的不同。在Windows程序中,習慣以如下方式命名宏、變量和函數:
#define PI 3.141 592 6 /*用大寫字母代表宏*/ int minValue, maxValue; /*變量:第一個單詞全寫,其后的單詞第一個字母小寫*/ void SendData(void); /*函數:所有單詞第一個字母都大寫定義*/
這種命名方式在程序員中非常盛行,意思表達清晰且避免了匈牙利法的臃腫,單詞之間通過首字母大寫來區分。通過第1個單詞的首字母是否大寫可以區分名稱屬于變量還是屬于函數,而看到整串的大寫字母可以斷定為宏。實際上,Windows的命名習慣并非僅限于Windows編程,大多數領域的程序開發都遵照此習慣。
但是Linux不以這種習慣命名,對應于上面的一段程序,在Linux中會被命名為:
#define PI 3.141 592 6 int min_value, max_value; void send_data(void);
上述命名方式中,下劃線大行其道,不依照Windows所采用的首字母大寫以區分單詞的方式。Linux 的命名習慣與 Windows命名習慣各有千秋,但是既然本書和本書的讀者立足于編寫 Linux程序,代碼風格理應保持與Linux開發社區的一致性。
Linux的代碼縮進使用“TAB”(8個字符)。
Linux的代碼括號“{”和“}”的使用原則如下。
(1)對于結構體、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循環后只有1行,不要加“{”和“}”,例如:
for (i = 0; i < 10; i++) { a = c; }
應該改為:
for (i = 0; i < 10; i++) a = c;
(3)if和else混用的情況下,else語句不另起一行,例如:
if (x == y) { ... } else if (x > y) { ... } else { ... }
(4)對于函數,“{”另起一行,譬如:
int add(int a, int b) { return a + b; }
在switch/case語句方面,Linux建議switch和case對齊,例如:
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; }
內核下的Documentation/CodingStyle 描述了 Linux 內核對編碼風格的要求,內核下的scripts/checkpatch.pl提供了1個檢查代碼風格的腳本。如果我們使用scripts/checkpatch.pl檢查包含如下代碼塊的源程序:
for (i = 0; i < 10; i++) { a = c; }
就會產生“WARNING: braces {} are not necessary for single statement blocks”的警告。
另外,請注意代碼中空格的應用,譬如“for?(i?=?0; ?i?<?10; ?i++)?{”語句中“?”都是空格。
3.5.2 GNU C與ANSI C
Linux上可用的C編譯器是GNU C編譯器,它建立在自由軟件基金會的編程許可證的基礎上,因此可以自由發布。GNU C對標準C進行一系列擴展,以增強標準C的功能。
1.零長度和變量長度數組
GNU C允許使用零長度數組,在定義變長對象的頭結構時,這個特性非常有用。例如:
struct var_data { int len; char data[0]; };
char data[0]僅僅意味著程序中通過var_data結構體實例的data[index]成員可以訪問len之后的第index個地址,它并沒有為data[]數組分配內存,因此sizeof(struct var_data)=sizeof(int)。
假設struct var_data的數據域就保存在struct var_data緊接著的內存區域,則通過如下代碼可以遍歷這些數據:
struct var_data s; ... for (i = 0; i < s.len; i++) printf("%02x", s.data[i]);
GNU C中也可以使用1個變量定義數組,例如如下代碼中定義的“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這樣的語法,區間[x,y]的數都會滿足這個case的條件,請看下面的代碼:
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'等價于標準C中的:
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
3.語句表達式
GNU C把包含在括號中的復合語句看做是一個表達式,稱為語句表達式,它可以出現在任何允許表達式的地方。我們可以在語句表達式中使用原本只能在復合語句中使用的循環、局部變量等,例如:
#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);
因為重新定義了_ _xx和_ _y這兩個局部變量,所以以上述方式定義的宏將不會有副作用。在標準C中,對應的如下宏則會產生副作用:
#define min(x,y) ((x) < (y) ? (x) : (y))
代碼min(++ia,++ib)會被展開為((++ia) < (++ib) ? (++ia): (++ib)),傳入宏的“參數”被增加2次。
4.typeof關鍵字
typeof(x)語句可以獲得x的類型,因此,我們可以借助typeof重新定義min這個宏:
#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)這個宏那樣把type傳入,因為通過typeof(x)、typeof(y)可以獲得type。代碼行(void) (&_x == &_y)的作用是檢查_x和_y的類型是否一致。
5.可變參數宏
標準C就支持可變參數函數,意味著函數的參數是不固定的,例如printf()函數的原型為:
int printf( const char *format [, argument]... );
而在 GNU C中,宏也可以接受可變數目的參數,例如:
#define pr_debug(fmt,arg...) \ printk(fmt,##arg)
這里arg表示其余的參數,可以是零個或多個,這些參數以及參數之間的逗號構成arg的值,在宏擴展時替換arg,例如下列代碼:
pr_debug("%s:%d",filename,line)
會被擴展為:
printk("%s:%d", filename, line)
使用“##”的原因是處理 arg 不代表任何參數的情況,這時候,前面的逗號就變得多余了。使用“##”之后,GNU C預處理器會丟棄前面的逗號,這樣,代碼:
pr_debug("success!\n")
會被正確地擴展為:
printk("success!\n")
而不是:
printk("success!\n",)
這正是我們希望看到的。
6.標號元素
標準C要求數組或結構體的初始化值必須以固定的順序出現,在GNU C中,通過指定索引或結構體成員名,允許初始化值以任意順序出現。
指定數組索引的方法是在初始化值前添加“[INDEX] =”,當然也可以用“[FIRST ... LAST] =”的形式指定一個范圍。例如,下面的代碼定義一個數組,并把其中的所有元素賦值為0:
unsigned char data[MAX] = { [0 ... MAX-1] = 0 };
下面的代碼借助結構體成員名初始化結構體:
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推薦類似的代碼應該盡量采用標準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.當前函數名
GNU C預定義了兩個標志符保存當前函數的名字,_ _FUNCTION_ _保存函數在源碼中的名字,_ _PRETTY_FUNCTION_ _保存帶語言特色的名字。在C函數中,這兩個名字是相同的。
void example() { printf("This is function:%s", __FUNCTION__); }
代碼中的_ _FUNCTION_ _意味著字符串“example”。C99已經支持_ _func_ _宏,因此建議在Linux編程中不再使用_ _FUNCTION_ _,而轉而使用_ _func_ _:
void example() { printf("This is function:%s", __func__); }
8.特殊屬性聲明
GNU C允許聲明函數、變量和類型的特殊屬性,以便進行手工的代碼優化和定制代碼檢查的方法。要指定一個聲明的屬性,只需要在聲明后添加_ _attribute_ _ (( ATTRIBUTE ))。其中ATTRIBUTE為屬性說明,如果存在多個屬性,則以逗號分隔。GNU C支持noreturn、format、section、aligned、packed等十多個屬性。
noreturn 屬性作用于函數,表示該函數從不返回。這會讓編譯器優化代碼,并消除不必要的警告信息。例如:
# define ATTRIB_NORET __attribute__((noreturn)) .... asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
format 屬性也用于函數,表示該函數使用 printf、scanf或strftime 風格的參數,指定 format屬性可以讓編譯器根據格式串檢查參數類型。例如:
asmlinkage int printk(const char * fmt, ...) __attribute__ ((format (printf, 1, 2)));
上述代碼中的第1個參數是格式串,從第2個參數開始都會根據printf()函數的格式串規則檢查參數。
unused屬性作用于函數和變量,表示該函數或變量可能不會被用到,這個屬性可以避免編譯器產生警告信息。
aligned屬性用于變量、結構體或聯合體,指定變量、結構體或聯合體的對界方式,以字節為單位,例如:
struct example_struct { char a; int b; long c; } __attribute_ _((aligned(4)));
表示該結構類型的變量以4字節對界。
packed屬性作用于變量和類型,用于變量或結構體成員時表示使用最小可能的對界,用于枚舉、結構體或聯合體類型時表示該類型使用最小的內存。例如:
struct example_struct { char a; int b; long c __attribute__((packed)); };
編譯器對結構體成員及變量對界的目的是為了更快地訪問結構體成員及變量占據的內存。例如,對于一個32位的整型變量,若以4字節方式存放(即低兩位地址為00),則CPU在一個總線周期內就可以讀取32位;若不然,CPU需要兩次總線周期才能組合為一個32位整型。
9.內建函數
GNU C提供了大量的內建函數,其中大部分是標準C庫函數的GNU C編譯器內建版本,例如memcpy()等,它們與對應的標準C庫函數功能相同。
不屬于庫函數的其他內建函數的命名通常以_ _builtin開始,如下所示。
● 內建函數_ _builtin_return_address (LEVEL)返回當前函數或其調用者的返回地址,參數LEVEL 指定調用棧的級數,如0表示當前函數的返回地址,1表示當前函數的調用者的返回地址。
● 內建函數_ _builtin_constant_p(EXP)用于判斷一個值是否為編譯時常數,如果參數 EXP的值是常數,函數返回1,否則返回0。
● 內建函數_ _builtin_expect(EXP, C)用于為編譯器提供分支預測信息,其返回值是整數表達式EXP的值,C的值必須是編譯時常數。
例如,下面的代碼檢測第1個參數是否為編譯時常數以確定采用參數版本還是非參數版本的代碼:
#define test_bit(nr,addr) \ (__builtin_constant_p(nr) ? \ constant_test_bit((nr),(addr)) : \ variable_test_bit((nr),(addr)))
在使用gcc編譯C程序的時候,如果使用“-ansi -pedantic”編譯選項,則會告訴編譯器不使用GNU擴展語法。例如對于如下C程序test.c:
struct var_data { int len; char data[0]; }; struct var_data a;
直接編譯可以通過:
gcc -c test.c
如果使用“-ansi -pedantic”編譯選項,編譯會報警:
gcc -ansi -pedantic -c test.c test.c:3: warning: ISO C forbids zero-size array ‘data’
3.5.3 do { } while(0)
在Linux內核中,經常會看到do {} while(0)這樣的語句,許多人開始都會疑惑,認為do {}while(0)毫無意義,因為它只會執行一次,加不加 do {} while(0)效果是完全一樣的,其實 do {}while(0)的用法主要用于宏定義中。
這里用一個簡單點的宏來演示:
#define SAFE_FREE(p) do{ free(p); p = NULL;} while(0)
假設這里去掉do...while(0),即定義SAFE_DELETE為:
#define SAFE_FREE(p) free(p); p = NULL;
那么以下代碼
if(NULL != p) SAFE_DELETE(p) else .../* do something */
會被展開為:
if(NULL != p) free(p); p = NULL; else .../* do something */
展開的代碼中存在兩個問題。
(1)因為if分支后有兩個語句,導致else分支沒有對應的if,編譯失敗。
(2)假設沒有else分支,則SAFE_FREE中的第二個語句無論if測試是否通過,都會執行。
的確,將SAFE_FREE的定義加上{}就可以解決上述問題了,即:
#define SAFE_FREE(p) { free(p); p = NULL;}
這樣,代碼:
if(NULL != p) SAFE_DELETE(p) else ... /* do something */
會被展開為:
if(NULL != p) { free(p); p = NULL; } else ... /* do something */
但是,在C程序中,每個語句后面加分號是一種約定俗成的習慣,那么,如下代碼:
if(NULL != p) SAFE_DELETE(p); else ... /* do something */
將被擴展為:
if(NULL != p) { free(p); p = NULL; }; else ... /* do something */
這樣,else分支就又沒有對應的if了,編譯將無法通過。假設用了do {} while(0),情況就不一樣了,同樣的代碼會被展開為:
if(NULL != p) do{ free(p); p = NULL;} while(0); else ... /* do something */
不會再出現編譯問題。do while(0)的使用完全是為了保證宏定義的使用者能無編譯錯誤地使用宏,它不對其使用者做任何假設。
3.5.4 goto
用不用goto一直是一個著名的爭議話題,Linux內核源代碼中對goto的應用非常廣泛,但是一般只限于錯誤處理中,其結構如:
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用于錯誤處理的用法實在是簡單而高效,只需保證在錯誤處理時注銷、資源釋放等與正常的注冊、資源申請順序相反。
3.6 總結
本章主要講解了Linux內核和Linux內核編程的基礎知識,為進行Linux驅動開發打下軟件基礎。
在Linux內核方面,主要介紹了Linux內核的發展史、組成、特點、源代碼結構、內核編譯方法及內核引導過程。
由于Linux驅動編程本質屬于內核編程,因此掌握內核編程的基礎知識顯得尤為重要。本章在這方面主要講解了在內核中新增程序及目錄和編寫Kconfig和Makefile的方法,并分析了Linux下C編程習慣以及Linux所使用的GNU C針對標準C的擴展語法。
- 30天自制操作系統
- Windows Server 2019 Cookbook
- Implementing Cisco UCS Solutions
- Linux系統文件安全實戰全攻略
- Linux操作系統應用編程
- Instant Optimizing Embedded Systems using Busybox
- Python基礎教程(第3版)
- 零基礎學鴻蒙PC:新一代國產操作系統
- 網絡操作系統管理與應用(第三版)
- Linux使用和管理指南:從云原生到可觀測性
- Dreamweaver CS5.5 Mobile and Web Development with HTML5,CSS3,and jQuery
- 一學就會:Windows Vista應用完全自學手冊
- NetDevOps入門與實踐
- Cassandra 3.x High Availability(Second Edition)
- Windows 10從新手到高手