Linux驅(qū)動(dòng)之內(nèi)核編程

Linux操作系統(tǒng)是UNIX操作系統(tǒng)的一種克隆系統(tǒng)个束,誕生于1991年10月5日(第一次正式向外公布的時(shí)間)。Linux操作系統(tǒng)的誕生渣叛、發(fā)展和成長過程依賴者5個(gè)重要支柱:UNIX操作系統(tǒng)丈探、Minix操作系統(tǒng)、GNU計(jì)劃桦卒、Posix標(biāo)準(zhǔn)和Internet立美。

一、Linux內(nèi)核的組成

1.1 Linux內(nèi)核源碼目錄結(jié)構(gòu)

本書范例程序基于Linux 2.6.28.6內(nèi)核源碼方灾,其目錄結(jié)構(gòu)如下:

  • 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等憋肖;

1.2 Linux內(nèi)核的組成部分

如圖1.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.1 Linux內(nèi)核的組成部分與關(guān)系


image

1.2.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)程责循。

圖1.2 Linux進(jìn)程狀態(tài)轉(zhuǎn)換


image

如圖1.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ì)醒。

在設(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);

1.2.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)換块蚌。Linux 2.6引入了對(duì)無MMU CPU的支持。

一般而言膘格,Linux的每個(gè)進(jìn)程享有4GB的內(nèi)存空間峭范,03GB屬于用戶空間,34GB屬于內(nèi)核空間瘪贱,內(nèi)核空間對(duì)常規(guī)內(nèi)存纱控、I/O設(shè)備內(nèi)存以及高端內(nèi)存存在不同的處理方式。

1.2.3 虛擬文件系統(tǒng)

如圖1.3所示政敢,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存放文件的邏輯信息。

圖1.3 Linux文件系統(tǒng)


image

1.2.4 網(wǎng)絡(luò)接口

網(wǎng)絡(luò)接口提供了對(duì)各種網(wǎng)絡(luò)標(biāo)準(zhǔn)的存取和各種網(wǎng)絡(luò)硬件的支持河哑。如圖1.4所示避诽,在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)程序。

圖1.4 Linux網(wǎng)絡(luò)體系結(jié)構(gòu)


image

1.2.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>

1.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模式轉(zhuǎn)入內(nèi)核態(tài)svc模式蚀浆。

又如缀程,X86處理器包含4個(gè)不同的特權(quán)級(jí),稱為Ring0~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)移脚猾。

二葱峡、Linux內(nèi)核的編譯及引導(dǎo)

2.1 Linux內(nèi)核的編譯

Linux驅(qū)動(dòng)工程師需要牢固地掌握Linux內(nèi)核的編譯方法以為嵌入式系統(tǒng)構(gòu)建可運(yùn)行的Linux操作系統(tǒng)映像。在編譯Linux內(nèi)核時(shí)龙助,需要配置內(nèi)核砰奕,可以使用下面的命令中的一個(gè)來配置:

# make config  (基于文本的最為傳統(tǒng)的配置界面,不推薦使用)
# make menuconfig  (基于文本菜單的配置界面)
# make xconfig  (要求QT被安裝)
# make gconfig  (要求GTK+被安裝)

在配置Linux 2.6內(nèi)核所使用的make config泌参、make menuconfig脆淹、make xconfig和make gconfig這4重方式中,最值得推薦的是make menuconfig沽一,它不依賴于QT或GTK+盖溺,且非常直觀。輸入make menuconfig命令后進(jìn)入配置界面如圖3.5所示铣缠。

內(nèi)核配置包含的項(xiàng)目相當(dāng)多烘嘱,在arch/arm/configs/目錄中包含了ARM芯片不同的demo板的默認(rèn)配置。因此蝗蛙,在內(nèi)核頂層目錄下輸入make board_defconfig就可以為demo配置內(nèi)核蝇庭,如輸入:make smdk2410_defconfig就可以為SMDK2410配置默認(rèn)內(nèi)核。

圖 1.5 Linux內(nèi)核編譯配置


image

編譯內(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)核模塊壮韭。

Linux 2.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引入下一層的Kconifg乳规,配置工具依據(jù)這些Kconfig包含的菜單和項(xiàng)目即可描繪出一個(gè)如圖3.6所示的分層結(jié)構(gòu)。例如合呐,/arch/arm/Kconfig文件的結(jié)構(gòu)如下:

   1 #
   2 # For a description of the syntax of this configuration file,
   3 # see Documentation/kbuild/kconfig-language.txt.
   4 #
   5 
   6 mainmenu "Linux Kernel Configuration"
   7 
   8 config ARM
   9         bool
  10         default y
  11         select HAVE_AOUT
  12         select HAVE_IDE
  13         select RTC_LIB
  14         select SYS_SUPPORTS_APM_EMULATION
  15         select HAVE_OPROFILE
  16         select HAVE_ARCH_KGDB
  17         select HAVE_KPROBES if (!XIP_KERNEL)
  18         select HAVE_KRETPROBES if (HAVE_KPROBES)
  19         select HAVE_FUNCTION_TRACER if (!XIP_KERNEL)
  20         select HAVE_GENERIC_DMA_COHERENT
  21         help
  22           The ARM series is a line of low-power-consumption RISC chip designs
  23           licensed by ARM Ltd and targeted at embedded applications and
  24           handhelds such as the Compaq IPAQ.  ARM-based PCs are no longer
  25           manufactured, but legacy ARM-based PC hardware remains popular in
  26           Europe.  There is an ARM Linux project with a web page at
  27           <http://www.arm.linux.org.uk/>.
……
 198 config MMU
 199         bool "MMU-based Paged Memory Management Support"
 200         default y
 201         help
 202           Select if you want MMU-based virtualised addressing space
 203           support by paged memory management. If unsure, say 'Y'.
……
 614 config ARCH_S3C64XX
 615         bool "Samsung S3C64XX"
 616         select GENERIC_GPIO
 617         select HAVE_CLK
 618         select ARCH_HAS_CPUFREQ
 619         help
 620           Samsung S3C64XX series based systems
……

2.2 Kconfig和Makefile

在Linux內(nèi)核中增加程序需要完成以下3項(xiàng)工作:

  • 將編寫的源代碼拷入Linux內(nèi)核源代碼的相應(yīng)目錄暮的;
  • 在目錄的Kconfig文件中增加關(guān)于新源代碼對(duì)應(yīng)的編譯配置選項(xiàng);
  • 在目錄的Makefile文件中增加對(duì)新代碼的編譯條目淌实。

一般而言冻辩,驅(qū)動(dòng)工程師只會(huì)在內(nèi)核源代碼的drivers目錄的相應(yīng)子目錄中增加新設(shè)備驅(qū)動(dòng)的源代碼,并增加或修改Kconfig配置腳本和Makefile腳本即可拆祈。接下來就講解一下Kconfig和Makefile的語法恨闪。

1.Makefile

這里主要對(duì)內(nèi)核源代碼各級(jí)子目錄中的kbuild(內(nèi)核的編譯系統(tǒng))Makefile進(jì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_BSDCOMP) += isdn_bsdcomp.o

除了obj-形式的目標(biāo)以外,還有l(wèi)ib-y library庫弄诲,hostprogs-y主機(jī)程序等目標(biāo)愚战,但是基本都應(yīng)用在特定的目錄和場合下。

(2)多文件模塊的定義

最簡單的Makefile如前面的一句話的形式就夠了齐遵,如果一個(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)中。

2.Kconfig

內(nèi)核配置腳本文件的語法也比較簡單挚币,主要包括以下幾個(gè)方面:

(1)菜單入口

大多數(shù)的內(nèi)核配置選項(xiàng)都對(duì)應(yīng)Kconfig中的一個(gè)菜單入口:

 614 config ARCH_S3C64XX
 615         bool "Samsung S3C64XX"
 616         select GENERIC_GPIO
 617         select HAVE_CLK
 618         select ARCH_HAS_CPUFREQ
 619         help
 620           Samsung S3C64XX series based systems

“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”

輸入提示的一般格式如下随抠,其中可選的if用來表示該提示的依賴關(guān)系:

prompt <prompt> [if <expr>]

默認(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)目僅僅是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)可以是前面描述的任何屬性,例如S3C6410芯片設(shè)計(jì)的產(chǎn)品的VGA輸出分辨率可以是1024768或者800600幸海,在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 “1024*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文件屋厘。

3.應(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文件:

# drivers/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對(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文件,在對(duì)應(yīng)位置增加:

source “driver/test/Kconfig”

腳本中的source意味著引用新的Kconfig文件忧吟。

在新增的test目錄下砌函,應(yīng)該包含如下Makefile文件:

# driver/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目錄加入列表促绵。

# drivers/test/cpu/Makefile
# Makefile for the TEST CPU
#
obj-$(CONFIG_TEST_CPU) += cpu.o

為了使得整個(gè)test目錄能夠被編譯命令作用到界阁,test目錄父目錄中的Makefile也需新增如下腳本才能夠進(jìn)入test目錄:

obj-$(CONFIG_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
      | -- Config

2.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盆顾;在X86 PC上的Bootloader一般為GRUB诬像,而在嵌入式系統(tǒng)中的Bootloader一般為U-Boot屋群。

圖1.6所示為X86 PC上電/復(fù)位到運(yùn)行Linux用戶空間初始化進(jìn)程的流程,在進(jìn)入與Linux相關(guān)代碼之間坏挠,會(huì)經(jīng)歷如下階段芍躏。

圖1.6 X86 PC上的Linux引導(dǎo)流程


image
  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)過程與之類似菲驴,但一般更加簡潔荐吵。不論具體以怎樣的方式實(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è)簡化版的操作系統(tǒng)艺演。著名的Linux Bootloader包括應(yīng)用于PC的LILO和GRUB,應(yīng)用于嵌入式系統(tǒng)的U-Boot、Redboot等胎撤。

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é)議(BOOT次伶、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解壓縮代碼聚磺。

如圖1.7所示,當(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ì)初始化頁表准夷,并啟動(dòng)內(nèi)存分頁機(jī)制,接著為任何可選的浮點(diǎn)單元(FPU)檢測CPU的類型莺掠,并將其存儲(chǔ)起來供以后使用衫嵌。這些都昨晚之后,/init/main.c中的start_kernel()函數(shù)被調(diào)用彻秆,進(jìn)入與體系結(jié)構(gòu)無關(guān)的Linux內(nèi)核部分楔绞。

圖1.7 X86 PC上的Linux內(nèi)核初始化
[圖片上傳失敗...(image-76a602-1551370457159)]

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è)簡單的shell腳本來啟動(dòng)必須的嵌入式應(yīng)用程序胖烛。

至此佩番,漫長的Linux內(nèi)核應(yīng)道和啟動(dòng)過程就此結(jié)束趟畏,而init()對(duì)應(yīng)的這個(gè)由start_kernel()創(chuàng)建的第一個(gè)線程也進(jìn)入用戶模式。

三律想、Linux下的C編程特點(diǎn)

3.1 Linux編碼風(fēng)格

Linux程序的命名習(xí)慣和Windows程序的命名習(xí)慣及著名的匈牙利命名法有很大的不同技即。在Windows程序中身笤,習(xí)慣以如下方式命名宏展鸡、變量和函數(shù):

#define PI 2.141592653  /* 用大寫字母代表宏 */
int minValue, maxValue;  /* 變量:第一個(gè)單詞全小寫涤久,其后的單詞第一個(gè)字母大寫 */
void SendData(vodi);   /* 函數(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.141592653
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)“{”和“}”的使用原則如下:

  • 對(duì)于結(jié)構(gòu)體、if/for/while/switch語句梆砸,“{”不另起一行帖世,例如:
struct var_data {
        int len;  /* 一個(gè)TAB日矫,8個(gè)字符 */
        char data[0];
};
if ( a == b ) {
        a = c;
        d = a;
}
for ( i = 0; i < 10; i++ ) {
        a = c;
        d = a;
}
  • 如果if、for循環(huán)后只有1行翔怎,不要加“{”和“}”,例如:
for ( i = 0; i < 10; i++ )
        a = c;
  • if和else混用的情況下珊膜,else語句不另起一行车柠,例如:
if ( x == y ) {
        ……
} else if ( x > y ) {
        ……
} else {
        ……
}

對(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)格的腳本芬萍。如果我們使用script/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)用==。

3.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.零長度和變量長度數(shù)組

GNU C允許使用零長度數(shù)組,在定義變長對(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(chǎn)’… ‘f’ : c -= ‘a(chǎn)’ - 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了__x和__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的類型葫慎,因此,我們可以借助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,
};

但是焰望,Linux 2.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,
   .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__零抬。

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ù)測信息,其返回值是整數(shù)表達(dá)式EXP的值锨络,C的值必須是編譯時(shí)常數(shù)赌躺。

例如,下面的檢測第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.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è)簡單的宏來演示:

#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è)問題:

  • 因?yàn)閕f分支后面有兩個(gè)語句领铐,導(dǎo)致else分支沒有對(duì)應(yīng)的if悯森,編譯失敗绪撵;
  • 假設(shè)沒有else分支瓢姻,則SAFE_FREE中的第二個(gè)語句無論if測試是否通過,都會(huì)執(zhí)行音诈。

的確幻碱,將SAFE_FREE的定義加上{}就可以解決上述問題了,即:

#define SAFE_FREE(P) { free(p); p = NULL; }
    這樣细溅,代碼展開為:
if(NULL != P)
    {free(p); p = NULL;}
else
    …/* do something */

但是褥傍,在C程序中,每個(gè)語句后面加分號(hào)是一種約定俗成的習(xí)慣喇聊,那么恍风,如下代碼:

if(NULL != p)
    SAFE_DELETE(p);
else
    …/* do something */

會(huì)被展開為:

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.4 goto

用不用goto一直是一個(gè)著名的爭議話題御滩,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;
……
   unregister_d();
err3:
   unregister_c();
err2:
   unregister_b();
err1:
   unregister_a();
err:
   return ret;

這種goto用于錯(cuò)誤處理的用法實(shí)在是簡單而高效削解,只需保證在錯(cuò)誤處理時(shí)注銷富弦、資源釋放等與正常的注冊(cè)、資源申請(qǐng)順序相反氛驮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腕柜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子矫废,更是在濱河造成了極大的恐慌盏缤,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蓖扑,死亡現(xiàn)場離奇詭異唉铜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)律杠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門潭流,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柜去,你說我怎么就攤上這事灰嫉。” “怎么了嗓奢?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵讼撒,是天一觀的道長。 經(jīng)常有香客問我蔓罚,道長椿肩,這世上最難降的妖魔是什么瞻颂? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任豺谈,我火速辦了婚禮,結(jié)果婚禮上贡这,老公的妹妹穿的比我還像新娘茬末。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布丽惭。 她就那樣靜靜地躺著击奶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪责掏。 梳的紋絲不亂的頭發(fā)上柜砾,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音换衬,去河邊找鬼痰驱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瞳浦,可吹牛的內(nèi)容都是我干的担映。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼叫潦,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼蝇完!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矗蕊,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤短蜕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后傻咖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體忿危,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年没龙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铺厨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硬纤,死狀恐怖解滓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情筝家,我是刑警寧澤洼裤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站溪王,受9級(jí)特大地震影響腮鞍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莹菱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一移国、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧道伟,春花似錦迹缀、人聲如沸使碾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽票摇。三九已至,卻和暖如春砚蓬,著一層夾襖步出監(jiān)牢的瞬間矢门,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國打工灰蛙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留颅和,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓缕允,卻偏偏與公主長得像峡扩,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子障本,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容