3.Linux内核及内核驱动编程
3.1.Linux 内核的发展与演变
Linux 操作系统诞生于 1991 年 10 月 5 日(第一次正式向外公布的时间)。 Linux 操作系统的诞生、发展和成长过程依赖于 UNIX 操作系统、 MINIX 操作系统、 GNU 计划、 POSIX 标准和 Internet。
3.1.1.UNIX 操作系统
UNIX 操作系统是美国贝尔实验室的 Ken. Thompson和 Dennis Ritchie于 1969 年在 DEC PDP-7 小型计算机上开发的一个分时操作系统。 Linux 操作系统可看作 UNIX 操作系统的一个克隆版本。
3.1.2.Minix 操作系统
Minix 操作系统也是 UNIX 操作系统的一种克隆系统,它于 1987 年由著名计算机教授 Andrew S.Tanenbaum 开发完成。开放源代码 Minix 操作系统的出现在全世界的大学中刮起了学习 UNIX 操作系统的旋风。 Linux 操作系统刚开始就是参照 Minix 操作系统开发进行。
3.1.3.GNU 计划
GNU 计划和*软件基金会(FSF)是由 Richard M. Stallman 于 1984 年创办的, GNU 是“GNU’s NotUNIX”的缩写。到 20 世纪 90 年代初, GNU 项目已经开发出许多高质量的免费软件,其中包括 Emacs 编辑系统、 Bash Shell 程序、 GCC 系列编译程序、 GDB 调试程序等。这些软件为 Linux 操作系统的开发创造了一个合适的环境,是 Linux 操作系统诞生的基础之一。没有 GNU 软件环境, Linux 操作系统将寸步难行。因此,严格而言, Linux 应该被称为 GNU/Linux 操作系统。
3.1.4. POSIX 标准
POSIX(Portable Operating System Interface for Computing Systems,可移植的操作系统接口)是由 IEEE和 ISO/IEC 开发的一组标准。该标准基于现有的 UNIX 实践和经验完成, 描述了操作系统的调用服务接口,用于保证编制的应用程序可以在源代码一级上在多种操作系统上移植。该标准在推动 Linux 操作系统朝着正规化方向发展起着重要的作用,是 Linux 前进的灯塔。
3.1.5. Internet
如果没有 Internet,没有遍布全世界的 Linux 爱好者的无私奉献,那么 Linux 操作系统就不可能发展到现在的水平。
3.2.Linux 后的内核特点
尽管 Linux 2.4 目前仍然被广泛应用,但是本书是基于 Linux 2.6 内核的。从 2003 年 12 月 Linux 2.6.0发布至今,一直还处于开发之中,并还将稳定较长一段时间。 Linux 2.6 相对于 Linux 2.4 有相当大的改进,主要体现在如下几个方面。
3.2.1.新的调度器
2.6 版本的 Linux 内核使用了新的进程调度算法,它在高负载的情况下执行得极其出色,并且当有很多处理器时也可以很好地扩展。
3.2.2.内核抢占
在 2.6 版本的 Linux 内核中,内核任务可以被抢占,从而提高系统的实时性。这样做最主要的优势在于可以极大地增强系统的用户交互性。
3.2.3.改进的线程模型
2.6 版本的 Linux 内核中线程操作速度得以提高,可以处理任意数目的线程, PID 最大可以到2000000000。
3.2.4.虚拟内存的变化
从虚拟内存的角度来看,新内核融合了 r-map(反向映射)技术,显著改善虚拟内存在一定程度负载下的性能。
3.2.5.文件系统
2.6 版本的 Linux 内核增加了对日志文件系统功能的支持,解决了 2.4 版本的 Linux 内核在这方面的不足。 2.6 版本的 Linux 内核在文件系统上的关键变化还包括对扩展属性及 POSIX 标准访问控制的支持。Ext2/Ext3 作为大多数 Linux 系统缺省安装的文件系统,在 2.6 版本的 Linux 内核中增加了对扩展属性的支持,可以给指定的文件在文件系统中嵌入元数据。
3.2.6.音频
新的 Linux 音频体系结构 ALSA(Advanced Linux Sound Architecture)取代了缺陷很多的旧的 OSS(Open SoundSystem)。新的声音体系结构支持 USB 音频和 MIDI 设备,并支持全双工重放等功能。
3.2.7.总线
SCSI/IDE 子系统经过大幅度的重写,解决和改善了以前的一些问题。比如 2.6 版本的 Linux 内核可以直接通过 IDE 驱动程序来支持 IDE CD/RW 设备,而不必像以前那样使用一个特别的 SCSI 模拟驱动程序。
3.2.8.电源管理
支持 ACPI(高级电源配置管理界面, Advanced Configuration and Power Interface),用于调整 CPU 在不同的负载下工作于不同的时钟频率以降低功耗。
3.2.9.联网和 IPSec
2.6 版本的 Linux 内核中加入了对 IPSec 的支持,删除了原来内核内置的 HTTP 服务器 kttpd,加入了对新的 NFSv4(网络文件系统)客户机/服务器的支持,并改进了对 IPv6 的支持。
3.2.10.用户界面层
2.6 版本的 Linux 内核重写了帧缓冲/控制台层,人机界面层还加入了对大多数接口设备的支持(从触到盲人用的设备和各种各样的鼠标)。
在设备驱动程序的方面, Linux 2.6 相对于 Linux 2.4 也有较大的改动,这主要表现在内核 API 中增加了不少新功能(例如内存池)、 sysfs 文件系统、内核模块从.o 变为.ko、驱动模块编译方式、模块使用计数、模块加载和卸载函数的定义等方面。
3.3.Linux 内核的组成
3.3.1.Linux 内核源代码目录结构
Linux 内核源代码包含如下目录
arch:包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如 i386、 arm、arm64、powerpc 等。
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 等。
include:内核API级别头文件。
内核一般要做到drivers与arch的软件架构分离,驱动中不包含板级信息,让驱动跨平台。同时内核的通用部分(如kernel、fs、jpc、net等)则与具体的硬件(arm和drivers)剥离。
3.3.2.Linux 内核的组成部分
如图 3.1 所示, Linux 内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)等 5 个子系统组成。
1.进程调度
进程调度控制系统中的多个进程对 CPU 的访问,使得多个进程能在 CPU 中微观串行,宏观并行地执行。进程调度处于系统的中心位置,内核中其他的 子 系 统 都依赖它,因为每个子系统都需要挂起或恢复进程。
如图 3.2 所示,Linux 的进程在几个状态间进行切换。在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使驱动对应的进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪状态。睡眠分为可被打断的睡眠和不可被打断的睡眠,两者的区别在于可被打断的睡眠在收到信号的时候会醒来。
在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,其对应的进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪状态。
设备驱动中,如果需要几个并发执行的任务,可以启动内核线程,启动内核线程的函数为:pid_t kernel_thread(int (*fn)(void *), void * arg, unsigned long flags);
2.内存管理
内存管理的主要作用是控制多个进程安全地共享主内存区域。当 CPU 提供内存管理单元(MMU)时, Linux 内存管理完成为每个进程进行虚拟内存到物理内存的转换。Linux 2.6 引入了对无 MMU CPU 的支持。
如图 3.3 所示,一般而言, Linux 的每个进程享有 4GB 的内存空间, 0~3GB 属于用户空间, 3~4GB 属于内核空间,内核空间对常规内存、 I/O 设备内存以及高端内存存在不同的处理方式。
3.虚拟文件系统
如图 3.4 所示, Linux 虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象,它使用超级块 super block 存放文件系统相关信息,使用索引节点 inode 存放文件的物理信息,使用目录项 dentry 存放文件的逻辑信息。
4.网络接口
网络接口提供了对各种网络的标准的存取和各种网络硬件的支持。网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备进行通信,每一种可能的硬件设备都有相应的设备驱动程序。
5.进程通信
Linux 支持进程间的多种通信机制,包含信号量、共享内存、管道等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。
Linux 内核的5个组成部分之间的依赖关系如下。
进程调度与内存管理之间的关系:这两个子系统互相依赖。在多道程序环境下,必须为程序创建进程,而创建进程的第一件事情就是将程序和数据装入内存。
进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域。
虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持 RAMDISK 设备。
内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,这也是内存管理依赖于进程调度的惟一原因。当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。
除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的例程,如分配和释放内存空间的函数、打印警告或错误信息的函数及系统提供的调试例程等。
3.3.3.Linux 内核空间与用户空间
现代 CPU 内部往往实现了不同的操作模式(级别)。例如, ARM 处理器有以下 7 种工作模式。
用户模式(usr):大多数的应用程序运行在用户模式下,当处理器运行在用户模式下时,某些被保护的系统资源是不能被访问的。
快速中断模式(fiq):用于高速数据传输或通道处理。
外部中断模式(irq):用于通用的中断处理。
管理模式(svc):操作系统使用的保护模式。
数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护。
系统模式(sys):运行具有特权的操作系统任务。
未定义指令中止模式(und):当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。
又如, X86 处理器包含 4 个不同的特权级,称为 Ring0~Ring3。 Ring0 下,可以执行特权级指令,对任何 I/O 设备都有访问权等,而 Ring3 则有很多操作限制。
Linux 系统充分利用 CPU 的这一硬件特性,但它只使用了两级。在 Linux 系统中,内核可进行任何操作,而应用程序则被禁止对硬件的直接访问和对内存的未授权访问。例如,若使用 X86 处理器,则用户代码运行在特权级 3,而系统内核代码则运行在特权级 0。
内核空间和用户空间这两个名词被用来区分程序执行的这两种不同状态,它们使用不同的地址空间。Linux 系统只能通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。
3.4.Linux 内核的编译及加载
3.4.1.Linux 内核的编译
在编译内核时,需要配置内核,可以使用 make menuconfig 命令(其他命令参考书本P64)对/home/baohua/devlop/linux中的Linux 4.0-rc1内核运行 make ARCH=arm menuconfig(我们公司用的是make kernel-menuconfig) 即可以进行Linux 内核编译配置:界面如图3.1所示
图3.1 Linux内核编译配置
内核配置包含的条目很多,在/home/ baohua/ devlop/ linux/ arch/ arm/ configs/ xxx_ defconfig文件包含了许多电路的默认配置。只需要运行 make ARCH=arm xxx _def config就可以为xxx开发板配置内核。
编译内核和模块的方法是: make ARCH=arm zImage
Make ARCH=arm modules
上述命令中,如果ARCH=arm已经作为环境变量导出,则不需要在make命令后书写该选项。执行完上述命令后,在源代码的根目录下会得到未压缩的内核镜像vmlinux和内核符号表文件System.map,在arch/arm/boot/目录下会得到压缩的内核映像zImage,在内核各对应目录内得到选中的内核模块。
Linux 内核的配置系统由以下3个部分组成。
. Makefile: 分布在Linux 内核源代码中,定义Linux内核的编译规则。
. 配置文件(Kconfig):给用户提供配置选择的功能。
. 配置工具:包括配置命令解析器(对配置脚本中使用配置命令进行解析)和配置用户界面(提供字符界面和图形界面)。这些界面使用的都是脚本语言,如Tcl/TK、Perl等。
使用make config、menuconfig 等命令后,会生成一个.config配置文件,记录哪些部分被编译入内核、哪些被编译成内核模块。
运行make menuconfig等时,配置工具首先分析与体系结构对应/arch/xxx/Kconfig文件(xxx即为传入的ARCH参数),/arch/xxx/Kconfig文件中除了本身包含一些与体系结构相关的配置项和配置菜单以外,还通过source语句引入了一些列Kconfig文件,而这些Kconfig又可能再次通过source引入下一层的Kconfig.配置工具依据Kconfig包含的菜单和条目即可描绘出如图3.9所示的分层结构。
3.4.2.Kconfig和Makefile
在Linux内核中添加程序需要完成以下3项工作。
. 将编写的源码复制到Linux内核源代码的相应目录中。
. 在目录的Kconfig文件中增加关于新源代码对应项目的编译配置选项。
. 在目录的Makefile文件中增加对新源码的编译条目。
1.实例引导:TTY_PRINTK字符设备
一般而言,驱动开发者会在内核源代码的devices目录内的相应子目录中增加新设备的源代码或者在arch/arm/mach-xxx下新增加版级支持的代码,同时增加或修改Kconfig配置脚本和Makefile脚本。(然后再内核配置中选择是否编入内核。
2.Makefile
这里主要对内核源代码各级子目录中的kbuild(内核的编译系统)Makefile进行简单的介绍。目标定义:
目标定义就是用来定义哪些内容要作为模块编译,哪些要编译并链接进内核。
例如:
obj-y += foo.o
表示要由foo.c或者foo.s文件链接得到foo.o并链接进内核(无条件编译,所以不需要Kconfig配置选项),而obj-n形式的目标不会被编译。
更常见的做法是根据make menuconfig 后生成的config文件的CONFIG_变量来决定文件的编译方式,如:
obj-
(
C
O
N
F
I
G
I
S
D
N
)
+
=
i
s
d
n
.
o
o
b
j
−
(CONFIG_ISDN) += isdn.o obj-
(CONFIGISDN)+=isdn.oobj−(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o
除了具有obj-形式的目标以外,还有lib-y library库、hostprogs-y主机程序等目标,但是这两类都应用在特定的目录和场景下。
多文件模块的定义
最简单的Makefile仅需要一行代码就够了。如果一个模块由多个文件组成,会稍微复杂一些,这个时候应采用模块名加-y或者-objs后缀的形式来定义模块的组成文件(具体请参考Linux设备驱动开发详解)例如:obj-y += test.o, obj-y表示把test.o文件编入内核。
目录层次迭代
Obj-$(CONFIG_EXT2_FS) += ext2/
当CONFIG_EXT2_FS的值为y或者m时,kbuild将会把ext2目录列入迭代的目标中。
3.Kconfig
内核配置脚本文件的语法比较简单,主要包括如下几个方面。
(1)配置选项
大多数内核配置选项都对应Kconfig中的一个配置选项(config):
config MODVERSIONS
bool “Module versioning support”
help
Usually,you have to use modules compiled with your kernel.
Saying Y here make it …
“config” 关键字定义新的配置选项,之后的几行代码定义了该配置选项的属性。配置选项的属性包括类型、数据范围、输入提示、依赖关系、选择关系及帮助信息、默认配置等。
.每个配置选项都必须指定类型,类型包括bool、tristate、string、hex和int,其中tristate和string是两种基本类型,其他类型都基于这两种基本类型。类型定义后可以紧跟着输入提示,下面两段脚本是等价的:
bool “Networking support”
和
bool
prompt “Networking support”
.输入提示的一般格式为:
prompt [if ]
其中,可选的if用来表示该提示的依赖关系。
默认值的格式为:
defalut [if ]
如果用户不设置对应选项,配置选项的值就是默认值。
.依赖关系的格式为:
Depend on (或者 requires)
如果定义了多重依赖关系,它们之间用 “&&”间隔。依赖关系也可以应用到该菜单中所有的其他选项(同样接受if表达式)内,下面两段脚本是等价的:
bool “foo” if BAR
default y if BAR
和
depends on BAR
Bool “foo”
default y
.选择关系(也称为反向依赖关系)的格式为:
Select [if ]
A如果选择了B,则在A被选中的情况下,B被自动选中。
.数据范围格式为:
Range [if ]
.Kconfig 中的expr(表达式)定义为:
::=
‘=’
‘!=’
(2)菜单结构
配置选项在菜单树结构中的位置可由两种方法决定。第一种方法为:
Menu “Network device support”
depends on NET
config NETDEVICES
…
endmenu
所有处于 “menu”,和“endmenu”之间的配置选项都会成为 “Network device support”的子菜单,而且,所有的子菜单(config)选项都会继承父菜单(menu)的依赖关系,比如,“Network device support”对“Net”的依赖会被加到配置选项NETDEVICES的依赖列表中。
注意:menu后面跟的“Network device support”项仅仅是一项菜单,没有对应真实的配置选项,也不具备3种不同的状态。这就是它和config的区别。
另一种方式是通过分析依赖关系生成的菜单结构。如果菜单项在一定程度上依赖前面的选项,它就能成为该选项的子菜单。如果父选项为“n”,子选项不可见;如果父选项可见,子选项才可见。例如:
Config MODULES
bool “Enable loadable module support”
Config MODVERSIONS
bool “Set version information on all module symbol”
depends on MODULES
comment “module support disabled”
depends on !MODULES
MODVERSIONS直接依赖MODULES,只有MODULES不为“n”时,该选项才可见。
除此之外,Kconfig中还可以使用“choices … endchoice”、“comment”、“if…endif”这样的语法结构。其中“choices…endchoice的结构为
choice
endchoice”
它定义了一个选择群,其接受的选项(choice options)可以是前面描述的任何属性,例如,LDD6410的VGA输出分辨率可以是1024768或者800600。
(2)应用实例:在内核中新增驱动代码和目录(参考Linux设备驱动开发详解P72-P73)
3.4.3.Linux内核的引导
3.5.Linux下的C编程特点
3.5.1.Linux编码风格
例子:
#define PI 3.1415926
int min_value, max_value;
void send_data(void)
Linux 使用下划线来区分单词,且缩进使用Tab键,它使用代码括号的风格如下:
对与结构体、if/for/while/switch语句,“{”不另起一行。
struct var_data {
int len;
Char data[0];
};
如果if、for循环后只有一行,不要加“{”和“}”,例如:
for (i = 0; i < 10; i++) {
a = c;
} 应该改成
for (i = 0; i < 10; i++)
a = c;
if和else混用的情况下,else语句不另起一行,例如:
if (x == y) {
…
} else if (x > y){
…
} else {
…
}
对于函数,“{”另起一行,例如:
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;
default:
break;
}
3.5.2.GNU 和 ANSI 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)。
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;
}
代码中的caase ‘0’…‘9’等价于标准C中的:
case ‘0’: case‘1’: case ‘2’: case‘3’…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);
因为重新定义了_x和_y这两个局部变量,所以用上述方式定义的宏将不会有副作用。在标准C中,对应的如下宏会产生副作用:
#define min(x,y) (x,y) ((x) < (y) ? (x) : (y))
代码min(++ia, ++ib) 会展开为((++ia)<(++ib)?(++ia):(++ib),传入宏的“参数”增加两次。
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;})
可变参数宏
标准C就支持可变参数函数,意味着函数的参数是不固定的,例如printf()函数的原型为:
int printf(const char *format [, argument]…);
而在GNU中,宏也可以接受可变数目的参数,例如:
#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”,);
标号元素
标准C要求数组或结构体的初始值必须以固定的顺序出现,在GNU C中,通过指定索引或结构体成员名,允许初始化值以任意顺序出现。
指定数组索引的方法是在初始化值前添加 “[INDX]=”,当然也可以用“[FIRST…LAST]=”的形式来指定一个范围。例如,下面的代码定义了一个数组,并把所有的元素赋值为0:(代码参考Linux设备驱动开发详解P80)
当前函数名(参考Linux设备驱动开发详解P81)
GNU C预定义两个标志符保存当前函数的名字,__FUNCTION__保存函数在代码源中的名字,__PRETT__FUNCTION__保存带语言特色的名字。在C函数中,这两个名字是相同的。
特殊属性声明(参考Linux设备驱动开发详解P81)
内建函数
GNU C 提供了大量的内建函数,其中大部分是标准C库函数的GNU C编译器内建版本,例如memcpy()等,它们与对应的标准C函数功能相同。
不属于库函数的其他内建函数的命名通常以__builtin开始,如下所示。
内建函数__builtin_return_address(LEVEL)返回当前函数或其他调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数调用者的返回地址。
内建函数__buitin_constant_p(EXP)用于判段一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。
例如,下面代码可检测第一个参数是否为编译时常数以确定采用参数版本还是非参数版本:
#define test_bit(nr,addr)
(__builtin_constant_p(nr) ?
constant_test_bit(nr),(addr)) :
variable_test_bit(nr),(addr)))
内建函数__builtin_expect(EXP,C)用于编译器提供分支预测信息、其返回值是整数表达式EXP的值,C的值必须是编译时常数。
Linux内核编程时常用的likely()和unlikely()底层调用的likely_notrace()、unlikely_notrace()就是基于__builtin_expect(EXP,C)实现的。(具体参考Linux设备驱动开发详解P83)
do{} while(0)语句
在Linux内核中,经常会看到do{}while(0)这样的语句,do{}while(0)的用法主要用于宏定义中。(具体例子请看P83-P85)
goto语句
Linux内核源代码中对goto的应用非常广泛,但是一般只限于错误处理中,其结构(参考Linux设备驱动开发详解P85)
3.6.工具链
在Linux的编程中,通常使用GNU工具链编译Bootloader、内核和应用程序。(具体参考Linux设备驱动开发详解P85-P88)
3.7. 总结
本文主要讲解了Linux内核和内核编程的基础知识,为进行Linux驱动开发打下软件基础。在Linux内核方面,本文主要介绍了Linux内核的发展史、组成、特点、源代码结构、内核编译方法及内核引导过程。
由于Linux驱动编程本质属于内核编程,因此掌握内核编程的基础知识显得尤为重要。本文在这方面主要讲解了内核中新增程序及目录和编写Kconfig和Makefile的方法,并分析了Linux下C编程习惯以及Linux所使用的GNU C针对C的扩展语法。