让我们从一个最简单的驱动程序开始,下面是来自《LINUX设备驱动开发详解》中的一个内核模块程序例子:
可以看到例子程序开始包含了头文件linux/init.h,由基本常识可知该文件在linux源码中的绝对路径是linux/include/linux/init.h(linux初始化模块函数定义和defines)。现在看一下该文件的具体内容:
该头文件首先包含了另一个头文件linux/compiler.h,依旧由基本常识可知该文件在linux源码中的绝对路径是linux/include/linux/compiler.h。先看一下该文件的内容,回头再来看init.h。
所有的内核代码,基本都包含了linux/compile.h这个文件,所以它是基础。首先印入眼帘的是对__ASSEMBLY__这个宏的判断,谈到这个宏涉及到GNU编译器。先提出这样一个问题,记得有个汇编文件名字叫做head.S,假如其被更名为head.s,也就是仅仅把后缀改为小写的s,编译即会出错。先来解释下这是为什么。
众所周知,head.S的代码主要是汇编编写,理所当然的我们知道汇编代码应该有汇编编译器GNU AS来处理,而GNU AS是不能做宏处理和文件包含处理的。不巧的是head.S中有很多预处理,现在要处理这些宏和文件包含,只有交给CPP预处理器来做。CPP如何识别这些文件呢?答案就是通过文件的后缀名。在命令行下执行man gcc可以找到下面的描述:
这就是为什么用.S而不是.s的原因了。也就是说由于GNU AS的预处理器无法完成head.S中的宏替换和文件包含功能,只能借助C预处理器来实现,从而提高ARM汇编的程序设计环境,更加方便。
现在回过头来说宏__ASSEMBLY__,由于借助里C预处理器的功能,而因为ARM汇编和C在宏替换的细节上有所不同,为了区分,引入了__ASSEMBLY__这个变量,这是通过Makefile中的AFLAGS来引入的:
AFLAGS := -D__ASSEMBLY__$(CPPFLAGS)
在编译汇编文件时,加入AFLAGS选项,所以__ASSEMBLY__传入,也就是定义了__ASSEMBLY__;在编译C文件时,没有用AFLAGS选项,自如也就没有定义__ASSEMBLY__。
现在知道这个变量实际是在编译汇编代码的时候,由编译器使用-D这样的参数加进去的,gcc会把这个宏定义为1。用在这里,是因为汇编代码里,不会用到类似于__user这样的属性,因为这样的属性是在定义函数参数的时候加的,这样避免不必要的宏在编译汇编代码时候的引用。
接下来又一个宏__CHECKER__的判断。当 编译内核代码的时候,使用make C=1或C=2的时候,会调用一个叫Sparse的工具(可以参考文档linux/Documentation/sparse.txt),这个工具对内核代码进行检查,怎么检查呢,就是靠对那些声明过的Sparse这个工具所能识别的特性的内核函数或是变量进行检查。在调用Sparse这个工具的同时,在Sparse代码里,会加上#define __CHECKER__ 1的字样。换句话说,就是,如果使用Sparse对代码进行检查,那么内核代码就会定义__CHECKER__宏,否则就不定义。而这里定义的类似于__attribute__((noderef, address_space(1)))这样的属性就是Sparse这个工具所能识别的了。
那么这些个属性是干什么用的呢:
# define __user __attribute__((noderef, address_space(1)))
__user这个特性,即__attribute__((noderef, address_space(1))),是用来修饰一个变量的,这个变量必须是非解除参考(no dereference)的,即这个变量地址必须是有效的,而且变量所在的地址空间类型必须是1,即用户程序空间的。refer to http://lkml.org/lkml/2004/9/12/249
程序空间被分成了3个部分,0表示normal space,即普通地址空间,对内核代码来说,当然就是内核空间地址了。1表示用户地址空间,这个不用多讲,还有一个2,表示是设备地址映射空间,例如硬件设备的寄存器在内核里所映射的地址空间。所以在内核函数里,有一个copy_to_user的函数,函数的参数定义就使用了这种方式。当然,这种特性检查,只有当机器上安装了Sparse这个工具,而且进行了编译的时候调用,才能起作用的。
# define __kernel /* default address space */
根据定义,就是默认的地址空间,即0,我想定义成__attribute__((noderef, address_space(0)))也是没有问题的。
# define __safe __attribute__((safe))
这个定义在sparse里也有,内核代码是在2.6.6-rc1版本变到2.6.6-rc2的时候被Linus加入的,为什么Linus要加入这个定义呢?原因是这样的:
有人发现在代码编译的时候,编译器对变量的检查有些苛刻,导致代码在编译的时候老是出问题。比如说这样一个例子:
这个编译的时候会有问题,因为没有检查参数是否为空,就直接进行调用。但是呢,在内核里,有好多函数,当它们被调用的时候,这些个参数必定不为空,所以根本用不着去对这些个参数进行非空的检查,所以呢,就增加了一个__safe的属性,如果修改一下:
也就是告诉编译器变量都是安全属性的,这样编译就没有问题了。不过我查找了linux2.6.34,arm的代码,里面已经没有使用这个宏定义变量的例子了,不知道是不是编译器现在已经支持这种特殊的情况了,所以就不用再加这样的代码了。
# define __force __attribute__((force))
表示所定义的变量类型是可以做强制类型转换的,在进行Sparse分析的时候,使其不用产生告警信息。
# define __nocast __attribute__((nocast))
这里表示这个变量的参数类型与实际参数类型一定得对得上才行,要不就在Sparse的时候生产告警信息。
# define __iomem __attribute__((noderef, address_space(2)))
这个定义与__user, __user是一样的,只不过这里的变量地址是需要在设备地址映射空间的。
# define __acquires(x) __attribute__((context(x,0,1)))
# define __releases(x) __attribute__((context(x,1,0)))
这是一对相互关联的函数定义,第一句表示参数x在执行之前,引用计数必须为0,执行后,引用计数必须为1,第二句则正好相反,这个宏定义用在修饰函数定义的变量。
# define __acquire(x) __context__(x,1)
# define __release(x) __context__(x,-1)
这是一对相互关联的函数定义,第一句表示要增加变量x的计数,增加量为1,第二句则正好相反,这个宏定义用在函数执行的过程中。
以上四句如果在代码中出现了不平衡的状况,那么在Sparse的检测中就会报警。当然,Sparse的检测只是一个手段,而且是静态检查代码的手段,所以它的帮助有限,有可能把正确的认为是错误的而发出告警。要是对以上四句的意思还是不太了解的话,请在源代码里搜一下相关符号的用法就能知道了。这两组,在本质上没什么区别,只是在使用的位置上有所区别罢了。
# define __cond_lock(x,c) ((c) ? ({ __acquire(x); 1; }) : 0)
这句话的意思就是条件锁。当c这个值不为0时,则让计数值加1,并返回值为1。不过这里我有一个疑问,就是在这里,有一个__cond_lock定 义,但没有定义相应的__cond_unlock,那么在变量的释放上,就没办法做到一致。而且我查了一下关于spin_trylock()这个函数的定 义,它就用了__cond_lock,而且里面又用了_spin_trylock函数,在_spin_trylock函数里,再经过几次调用,就会使用到 __acquire函数,这样的话,相当于一个操作,就进行了两次计算,会导致Sparse的检测出现告警信息,经过我写代码进行实验,验证了我的判断, 确实是会出现告警信息,如果我写两遍unlock指令,就没有告警信息了,但这是与程序的运行是不一致的。
# define __percpu __attribute__((noderef, address_space(3)))
该定义用于多处理器(SMP)的情况。以下是来自http://lkml.org/lkml/2010/1/25/107的解释。
__percpu annotation teaches sparse that percpu variables live in a separate address space and can't be accessed directly without going through percpu accessors. This allows detection of most percpu access mistakes involving both static and dyanmic percpu variables.
到目前为止没有发现有解释说这个(3)空间是什么空间的。
extern void __chk_user_ptr(const volatile void __user *);
extern void __chk_io_ptr(const volatile void __iomem *);
这两句比较有意思。这里只是定义了函数,但是代码里没有函数的实现。这样做的目的,就是在进行Sparse的时候,让Sparse给代码做必要的参数类型检查,在实际的编译过程中,并不需要这两个函数的实现。
#ifdef __KERNEL__
这个符号用于选择使用头文件的哪一部分。由于libc包含了这些头文件,应用程序最终也会包含内核头文件,但应用程序不需要内核原型。于是就用__KERNEL__符号将那些额外的去掉。将内核符号和宏开放给用户空间的程序会造成那个程序的名字空间污染。该宏可以由打算使用要包含的头文件中该条件编译包含的代码定义的宏的模块文件定义,亦可编译时通过-D选项来定义( -D__KERNEL__ )并传递给编译器,当然亦可在Makefile中完成该动作。
#ifdef __GNUC__
#include <linux/compiler-gcc.h>
#endif
1.__GNUC__ 是gcc编译器编译代码时预定义的一个宏。需要针对gcc编写代码时, 可以使用该宏进行条件编译。
2.__GNUC__ 的值表示gcc的版本。需要针对gcc特定版本编写代码时,也可以使用该宏进行条件编译。
3.__GNUC__ 的类型是“int”. 该宏被扩展后, 得到的是整数字面值。
此处如果定义了该宏则继续把compiler-gcc.h包含进来,该头文件定义了gcc所有版本都支持的一般定义。
#define notrace __attribute__((no_instrument_function))
宏notrace的定义,这个宏用于修饰函数,说明该函数不被跟踪。这里所说的跟踪是gcc一个很重要的特性,只要在编译时打开相关的跟踪选择,编译器会加入一些特性,使得程序在执行完后,可以通过工具(如Graphviz)来查看函数的调用过程。而对于内核来说,内部采用了ftrace机制,而不采用trace的特性。
/* Intel compiler defines __GNUC__. So we will overwrite implementations
* coming from above header files here
*/
#ifdef __INTEL_COMPILER
# include <linux/compiler-intel.h>
#endif
此处的注释解释的已然相当明了,不再多说。真希望内核中多些注释啊。不过免费给你的代码,还能要求什么呢?
上面包含的文件compiler-gcc.h和compiler-intel.h里面定义了编译器相关的内容,下面开始定义通用的一些编译属性。
这里首先定义了结构体ftrace_branch_data,其用于记录ftrace branch的trace记录。什么是ftrace呢?(这里内容比较多哦...)
ftrace 是当前 Linux 内核中一种新的 trace 工具。ftrace 的作用是帮助开发人员了解 Linux 内核的运行时行为,以便进行故障调试或性能分析。最早 ftrace 是一个 function tracer,仅能够记录内核的函数调用流程。如今ftrace 已经成为一个 framework,采用 plugin 的方式支持开发人员添加更多种类的 trace 功能。Ftrace 由 RedHat 的 Steve Rostedt 负责维护。branch tracer就是ftrace其中一个功能,用于跟踪内核程序中的 likely/unlikely 分支预测命中率情况。 Branch tracer 能够记录这些分支语句有多少次预测成功。从而为优化程序提供线索。
ftrace 在内核态工作,用户通过 debugfs 接口来控制和使用 ftrace 。从 2.6.30 开始,ftrace 支持两大类 tracer:传统 tracer 和 Non-Tracer Tracer 。下面分别简单介绍他们的使用。
传统 Tracer 的使用
使用传统的 ftrace 需要如下几个步骤:
- 选择一种 tracer
- 使能 ftrace
- 执行需要 trace 的应用程序,比如需要跟踪 ls,就执行 ls
- 关闭 ftrace
- 查看 trace 文件
用户通过读写 debugfs 文件系统中的控制文件完成上述步骤。使用 debugfs,首先要挂载她。命令如下:
# mkdir /debug |
此时您将在 /debug 目录下看到 tracing 目录。 Ftrace 的控制接口就是该目录下的文件。
选择 tracer 的控制文件叫作 current_tracer 。选择 tracer 就是将 tracer 的名字写入这个文件,比如,用户打算使用 function tracer,可输入如下命令:
#echo ftrace > /debug/tracing/current_tracer |
文件 tracing_enabled 控制 ftrace 的开始和结束。
#echo 1 >/debug/tracing/tracing_enable |
上面的命令使能 ftrace 。同样,将 0 写入 tracing_enable 文件便可以停止 ftrace 。
ftrace 的输出信息主要保存在 3 个文件中。
- Trace,该文件保存 ftrace 的输出信息,其内容可以直接阅读。
- latency_trace,保存与 trace 相同的信息,不过组织方式略有不同。主要为了用户能方便地分析系统中有关延迟的信息。
- trace_pipe 是一个管道文件,主要为了方便应用程序读取 trace 内容。算是扩展接口吧。
Non-Tracer Tracer 的使用
从 2.6.30 开始,ftrace 还支持几种 Non-tracer tracer,所谓 Non-tracer tracer 主要包括以下几种:
- Max Stack Tracer
- Profiling (branches / unlikely / likely / Functions)
- Event tracing
和传统的 tracer 不同,Non-Tracer Tracer 并不对每个内核函数进行跟踪,而是一种类似逻辑分析仪的模式,即对系统进行采样,但似乎也不完全如此。无论怎样,这些 tracer 的使用方法和传统tracer 的使用稍有不同。
下面还是来介绍下此处代码涉及的branch tracer。
Branch tracer 比较特殊,她有两种模式,即是传统 tracer,又实现了 profiling tracer 模式。
作为传统 tracer 。其输出文件为 trace,格式如下:
# tracer: branch |
在 FUNCTION 列中,显示了 4 类信息:
函数名,文件和行号,用中括号引起来的部分,显示了分支的信息,假如该字符串为 ok,表明 likely/unlikely 返回为真,否则字符串为 MISS 。举例来说,在文件 file.h 的第 29 行,函数 fput_light 中,有一个 likely 分支在运行时解析为真。我们看看 file.h 的第 29 行:
static inline void fput_light(struct file *file, int fput_needed) |
Branch tracer 作为 profiling tracer 时,其输出文件为 profile_annotated_branch,其中记录了 likely/unlikely 语句完整的统计结果。
#cat trace_stat/branch_ annotated |
if (unlikely(rt_task(prev)) && rq->rt.highest_prio.curr > prev->prio) |
ftrace 的实现
研究 tracer 的实现是非常有乐趣的。理解 ftrace 的实现能够启发我们在自己的系统中设计更好的 trace 功能。
ftrace 的整体构架
Ftrace 的整体构架:
图 1. ftrace 组成
Ftrace 有两大组成部分,一是 framework,另外就是一系列的 tracer 。每个 tracer 完成不同的功能,它们统一由 framework 管理。 ftrace 的 trace 信息保存在 ring buffer 中,由 framework 负责管理。 Framework 利用 debugfs 系统在 /debugfs 下建立 tracing 目录,并提供了一系列的控制文件。
branch tracer 的实现
内核代码中常使用 likely 和 unlikely 提高编译器生成的代码质量。 Gcc 可以通过合理安排汇编代码最大限度的利用处理器的流水线。合理的预测是 likely 能够提高性能的关键,ftrace 为此定义了 branch tracer,跟踪程序中 likely 预测的正确率。为了实现 branch tracer,重新定义了 likely 和 unlikely 。也就是接下来代码做的事情。
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
这两句是一对对应关系。__builtin_expect(expr, c)这个函数是新版gcc支持的,它是用来作代码优化的,用来告诉编译器,expr的期,非常有可能是c,这样在gcc生成对应的汇编代码的时候,会把相应的可能执行的代码都放在一起,这样能少执行代码的跳转。为什么这样能提高CPU的执行效率呢?因为CPU在执行的时候,都是有预先取指令的机制的,把将要执行的指令取出一部分出来准备执行。CPU不知道程序的逻辑,所以都是从可程序程序里挨着取的,如果这个时候,能不做跳转,则CPU预先取出的指令都可以接着使用,反之,则预先取出来的指令都是没有用的。还有个问题是需要注意的,在__builtin_expect的定义中,以前的版本是没有!!这个符号的,这个符号的作用其实就是负负得正,为什么要这样做呢?就是为了保证非零的x的值,后来都为1,如果为零的0值,后来都为0,仅此而已。
likely_notrace和unlikely_notrace宏使用__builtin_expect函数,__builtin_expect告诉编译器程序设计者期望的比较结果,以便编译器对代码进行优化,改变汇编代码中的判断跳转语句。
__branch_check__宏,记录当前trace点,并利用ftrace_likely_update记录likely判断的正确性,并将结果保存在ring buffer中,之后用户可以通过ftrace的debugfs接口读取分支预测的相关信息。从而优调整代码,优化性能。
重定义likely和unlikely宏里面用到了GCC的build-in函数__builtin_constant_p判断一个表达式在编译时是否为常量。当是常量时,直接返回likely和unlikely表达式的值,没必要做预测的记录。当表达式为非常数时,使用宏__branch_check__检查分支并记录likely判断的预测信息。
如果设置了CONFIG_PROFILE_ALL_BRANCHES宏,将重定义if()为__trace_if。__trace_if检查if的所有分支,并记录分支的跟踪信息。
#ifndef barrier
# define barrier() __memory_barrier()
#endif
这里表示如果没有定义barrier函数,则定义barrier()函数为__memory_barrier()。但在内核代码里,是会包含compiler-gcc.h这个文件的,所以在这个文件里,定义barrier()为__asm__ __volatile__("": : :"memory")。barrier翻译成中文就是屏障的意思,在这里,为什么要一个屏障呢?这是因为CPU在执行的过程中,为了优化指令,可能会对部分指令以它自己认为最优的方式进行执行,这个执行的顺序并不一定是按照程序在源码内写的顺序。编译器也有可能在生成二进制指令的时候,也进行一些优化。这样就有可能在多CPU,多线程或是互斥锁的执行中遇到问题。那么这个内存屏障可以看作是一条线,内存屏障用在这里,就是为了保证屏障以上的操作,不会影响到屏障以下的操作。然后再看看这个屏障怎么实现的。__asm__表示后面的东西都是汇编指令,当然,这是一种在C语言中嵌入汇编的方法,语法有其特殊性,我在这里只讲跟这条指令有关的。__volatile__表示不对此处的汇编指令做优化,这样就会保证这里代码的正确性。""表示这里是个空指令,那么既然是空指令,则所对应的指令所需要的输入与输出都没有。在gcc中规定,如果以这种方式嵌入汇编,如果输出没有,则需要两个冒号来代替输出操作数的位置,所以需要加两个::,这时的指令就为"" : :。然后再加上为分隔输入而加入的冒号,再加上空的输入,即为"" : : :。后面的memory是gcc中的一个特殊的语法,加上它,gcc编译器则会产生一个动作,这个动作使gcc不保留在寄存器内内存的值,并且对相应的内存不会做存储与加载的优化处理,这个动作不产生额外的代码,这个行为是由gcc编译器来保证完成的。如果对这部分有更大的兴趣,可以考察gcc的帮助文档与内核中一篇名为memory-barriers.txt的文章。
#################################################################################
接下来好多定义都没有实现,可以看一看注释就知道了,所以这里就不多说了。唉,不过再插一句,__deprecated属性的实现是为deprecated。
#define noinline_for_stack noinline
#ifndef __always_inline
#define __always_inline inline
#endif
这里noinline与inline属性是两个对立的属性,从词面的意思就非常好理解了。
#ifndef __cold
#define __cold
#endif
从注释中就可以看出来,如果一个函数的属性为__cold,那么编译器就会认为这个函数几乎是不可能被调用的,在进行代码优化的时候,就会考虑到这一点。不过我没有看到在gcc里支持这个属性的说明。
#ifndef __section
# define __section(S) __attribute__ ((__section__(#S)))
#endif
这个比较容易理解了,用来修饰一个函数是放在哪个区域里的,不使用编译器默认的方式。这个区域的名字由定义者自己取,格式就是__section__加上用户输入的参数。
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
这个函数的定义很有意思,它就是访问这个x参数所对应的东西一次,它是这样做的:先取得这个x的地址,然后把这个地址进行变换,转换成一个指向这个地址类型的指针,然后再取得这个指针所指向的内容。这样就达到了访问一次的目的,哈哈。