1.6.3 Linux内核的显著特性
下载Linux内核代码并安装浏览工具后,即可浏览并分析Linux内核的源码。接下来将简要讲解Linux内核的显著特性。
1.GCC特性
Linux内核使用GNU Compiler Collection(GCC)套件的几个特殊功能。这些功能包括提供快捷方式和简化及向编译器提供优化提示等。GCC和Linux是出色的组合。尽管它们是独立的软件,但是Linux完全依靠GCC在新的体系结构上运行。Linux还利用GCC中的特性(称为扩展)实现更多功能和优化。
(1)基本功能
概括来说,GCC具有如下两大功能。
● 功能性:扩展提供新功能。
● 优化:扩展帮助生成更高效的代码。
GCC允许通过变量的引用识别类型,这种操作支持泛型编程。在C++、Ada和Java语言等许多现代编程语言中都可以找到相似的功能。例如,Linux使用typeof构建min和max等依赖于类型的操作。使用typeof构建一个泛型宏的代码如下。
GCC还支持范围,在C语言的许多方面都可以使用范围。最常见的是在switch/case块中的case语句中使用。使用switch/case也可以通过使用跳转表来实现编译器优化。在复杂的条件结构中,通常依靠嵌套的if语句实现与下面代码相同的结果,但是下面的代码更简洁。具体代码如下。
(2)属性
GCC允许声明函数、变量和类型的特殊属性,以便指示编译器进行特定方面的优化和更仔细的代码检查。使用方式非常简单,只需在声明后面加上如下代码即可。
其中ATTRIBUTE是属性的说明,多个说明之间以逗号分隔。GCC可以支持十几个属性,接下来介绍一些比较常用的属性。
①属性noreturn:属性noreturn用在函数中,表示该函数从不返回。它能够让编译器生成较为优化的代码,消除不必要的警告信息。
②属性format(archetype, string-index, first-to-check):属性format用在函数中,表示该函数使用printf、scanf、strftime或strfmon风格的参数,并可使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。
● archetype:指定是哪种风格。
● string-index:指定传入函数的第几个参数是格式化字符串。
● first-to-check:指定从函数的第几个参数开始按照上述规则进行检查。
如下面的内核代码。
这表示printk的第一个参数是格式化字符串,从第二个参数开始根据格式化字符串检查参数。
③属性unused:属性unused用于函数和变量,表示该函数或变量可能并不使用,这个属性能够避免编译器产生警告信息。
④属性aligned(ALIGNMENT):属性aligned常用在变量、结构或联合中,用于设定一个指定大小的对齐格式,以字节为单位,如下面的内核代码。
上述代码表示结构体ohci_hcca的成员以256字节对齐。如果aligned后面不紧跟一个指定的数字值,那么编译器将依据目标主机情况使用最大、最有益的对齐方式。
需要注意的是,attribute属性的效果与用户的连接器也有关,如果用户的连接器最大只支持16字节对齐,那么此时定义32字节对齐也是无济于事的。
● 属性packed:属性packed用在变量和类型中,当用在变量或结构体成员时,表示使用最小可能的对齐。当用在枚举、结构体或联合类型时,表示该类型使用最小的内存。属性packed多用于定义硬件相关的结构时,使元素之间不会因对齐产生问题。例如,下面的内核代码。
在上述代码中,__attribute__((packed))告诉编译器usb_interface_descriptor的元素为1字节对齐,不要再添加填充位。因为定义此结构的代码和usb spec中的完全一样。如果不给编译器这个暗示,则编译器会依据平台的类型在结构的每个元素之间添加一定的填充位,使用这个结构时就不能达到预期的结果。
(3)内建函数。在GCC中提供了大量的内建函数,其中有很多是标准C库函数的内建版本,如memcpy(),它们的功能与对应的C库函数的功能相同,在此不再进行讲解。
在内建函数中,还有很多函数的名字是以__builtin开始的,接下来对__builtin_expect()进行详细分析,其他__builtin_xxx()函数的原理和此函数类似,不再一一介绍。
函数__builtin_expect()的格式如下。
为什么Linux会推出__builtin_xxx()函数呢?这是因为大部分代码在分支预测方面做的比较糟糕,所以GCC提供了此内建函数来帮助处理分支预测,并优化程序。
● 第一个参数exp:是一个整型的表达式,返回值也是此exp。
● 第二个参数c:其值必须是一个编译期的常量。
由此可见,此内建函数的意思就是exp的预期值为c,编译器可以根据这个信息适当地重排条件语句块的顺序,将符合这个条件的分支放在合适的地方。
此函数在Linux内核中的应用,具体代码如下。
unlikely(x)用于告诉编译器条件x发生的可能性不大,likely用于告诉编译器条件x发生的可能性很大。它们一般用在条件语句里,if语句不变,当if条件为1的可能性非常小时,可以在条件表达式外面包装一个unlikely(),那么这个条件块里语句的目标码可能就会被放在一个比较远的位置,以保证经常执行的目标码更紧凑。如果可能性非常大,则使用likely()包装。
2.链表的重要性
链表和本书讲解的驱动密切相关,如USB驱动。鉴于链表在内核中的特殊地位,有必要在此对其作一番陈述。内核中链表的实现位于include/linux/list.h文件,链表数据结构的定义也很简单,具体代码如下。
结构list_head包含两个指向list_head结构的指针prev和next,由此可见,内核中的链表实际上都是双链表。学习过C语言的读者应该知道,链表的定义结构如下。
通过上述格式使用链表,对于每一种数据类型都要定义它们各自的链表结构。而内核中的链表却与此不同,它并没有数据域,不是在链表结构中包含数据,而是在描述数据类型的结构中包含链表。
如果在hub驱动中使用struct usb_hub来描述hub设备,hub需要处理一系列的事件,如当探测到一个设备链进来时,就会执行一些代码去初始化该设备,所以hub就创建了一个链表来处理各种事件,这个链表的结构如图1-6所示。
图1-6 hub链表的结构
在Linux代码中,完整展示了链表的操作过程,接下来简单剖析对应的Linux代码。
(1)声明与初始化
可以使用以下两种方式来声明链表。
● 使用LIST_HEAD宏在编译时静态初始化;
● 使用INIT_LIST_HEAD()在运行时进行初始化。
对应的Linux代码如下。
无论采用哪种方式,新生成的链表头的两个指针next、prev都初始化为指向自己。
(2)判断链表
判断链表是否为空,对应的Linux代码如下。
(3)插入操作
建立链表后,就不可避免地对其进行操作,如向里面添加数据。使用函数list_add()和list_add_tail()可以完成添加数据的工作。对应的Linux代码如下。
其中,函数list_add()将数据插入在head后,函数list_add_tail()将数据插入在head->prev后。对于循环链表来说,因为表头的next、prev分别指向链表中的第一个和最后一个节点,所以函数list_add()和list_add_tail()的区别并不大。
(4)删除操作
可以使用函数list_replace_init()从链表里删除一个元素,并且将其初始化。对应的Linux代码如下。
(5)遍历操作
在内核中的链表仅仅保存了list_head结构的地址,应该如何通过它获取一个链表节点真正的数据项呢?此时就需要使用list_entry宏,通过它可以很容易地获得一个链表节点的数据。对应的Linux代码如下。
假如以hub驱动为例,当要处理hub的事件时,需要知道具体是哪个hub触发了这起事件。而list_entry的作用是从struct list_head event_list中得到它所对应的struct usb_hub结构体变量。例如,下面的代码。
通过上述代码,从全局链表hub_event_list中取出一个叫作tmp的结构体变量,然后通过tmp获得它所对应的struct usb_hub。
3.Kconfig和Makefile
Kconfig和Makefile是浏览内核代码时最常用的两个文件之一。几乎Linux内核中的每一个目录下都有一个Kconfig文件和一个Makefile文件。通过Kconfig和Makefile,可以让用户了解一个内核目录下面的结构。在研究内核的某个子系统、某个驱动或其他某个部分之前,需要仔细阅读目录下对应的Kconfig和Makefile文件。
(1)Kconfig结构
每种平台对应的目录下面都有一个Kconfig文件,如arch/i386/Kconfig。Kconfig文件通过source语句可以构建一个Kconfig树。文件“arch/i386/Kconfig”的代码片段如下。
Kconfig的详细语法规则可以参看内核文档Documentation/kbuild/kconfig-language.txt,下面对其简单介绍。
①菜单项:关键字config可以定义一个新的菜单项,如下面的代码。
后面的代码定义了该菜单项的属性,包括类型、依赖关系、选择提示、帮助信息和默认值等。
常用的类型有bool、tristate、string、hex和int。类型bool的只能被选中或不选中,类型tristate的菜单项多了编译成内核模块的选项。
依赖关系是通过depends on或requires定义的,指出此菜单项是否依赖于另外一个菜单项。
帮助信息需要使用help或---help---来指出。
②菜单组织结构:菜单选项通过两种方式来组成树状结构,具体说明如下。
第一种方式:使用关键字menu显式声明为菜单,如下面的代码。
第二种方式:也可以使用依赖关系确定菜单结构,如下面的代码。
其中菜单项MODVERSIONS依赖于MODULES,所以它就是一个子菜单项。这要求菜单项和它的子菜单项同步显示或不显示。
③关键字Kconfig:Kconfig文件描述了一系列的菜单选项,除帮助信息外,文件中的每一行都以一个关键字开始,主要有config、menuconfig、choice/endchoice、comments、menu/endmenu、if/endif、source等,只有前5个可以用在菜单项定义的开始,它们都可以结束一个菜单项。
(2)Makefile
Linux内核的Makefile分为以下5个组成部分。
● Makefile:最顶层的Makefile。
● .config:内核的当前配置文档,编译时成为顶层Makefile的一部分。
● arch/$(ARCH)/Makefile:和体系结构相关的Makefile。
● Makefile.*:一些特定Makefile的规则。
● kbuild级别Makefile:各级目录下的大约500个文档,编译时根据上层Makefile传下来的宏定义和其他编译规则,将源代码编译成模块或编入内核。顶层的Makefile文档读取.config文档的内容,并总体上负责build内核和模块。Arch Makefile则提供补充体系结构相关的信息。其中.config的内容是在make menuconfig时通过Kconfig文档配置的结果。
假如想把自己写的一个Flash的驱动程式加载到工程中,并且能够通过menuconfig配置内核时会选择该驱动,此时该怎么办呢?其实借助Makefile就可以实现,解决流程如下。
将编写的flashtest.c文档添加到/driver/mtd/maps/目录下。
修改/driver/mtd/maps目录下的kconfig文档,修改代码如下。
当运行make menuconfig时会出现ap71 flash选项。
修改该目录下makefile文档,添加下面的代码内容。
此时当运行make menucofnig时会发现ap71 flash选项,假如选择了此选项,该选择就会保存在.config文档中。当编译内核时会读取.config文档,当发现ap71 flash选项为yes时,系统在调用/driver/mtd/maps/下的makefile时,会把flashtest.o加入内核中。