|
发表于 2009-1-14 19:13:33
|
显示全部楼层
这是我很早以前写的Linux里gcc扩展使用情况的分析
GNU C和GCC
gcc编译器符合现已公布的C语言标准,比如C89, C99。可以通过命令行参数来指定所采用的标准,比如“-ansi”、“-std=c89”、“-std=iso9899:1990”等等。指定标准后,如果源代码中有不合标准之处,gcc就会产生警告,乃至报错。除此之外,gcc编译器还定义了很多C语言的扩展。比如:零长度数组,变长数组,长长型数据类型,以及众多的函数属性,变量属性,内建函数等等。详细信息可以通过gcc的info1来查看,键入以下命令:
$ info gcc "C Extensions"
这些扩展在极少数情况下可能和C语言标准产生冲突。使用上面提到命令行选项来指定某个标准,就可以禁用会与此标准产生冲突的GNU扩展。你也可以指定GNU的标准,比如“-std=gnu89”或者“-std=gnu99”。如果不用-std来指定标准,那么缺省使用“-std=gnu89”。这也是内核代码遵循的标准。内核代码充分地利用了gcc提供的C语言扩展。下面就将对内核代码中常见的GNU扩展的应用一一加以解释。
asmlinkage/FASTCALL()/fastcall
这三个宏指定了函数参数的传递方式。asmlinkage修饰的函数,其参数通过堆栈传递。FASTCALL()/fastcall,此二者实际上是一样的作用。在Intel i386架构中,它们所修饰的函数,其前三个参数分别通过通用寄存器EAX,ECX和EDX来传递。
它们定义于include/asm-i386/linkage.h:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
#define FASTCALL(x) x __attribute__((regparm(3)))
#define fastcall __attribute__((regparm(3)))
此处利用的是函数属性regparm。
static inline内联函数
内联函数有些类似于宏。内联函数的代码会被直接嵌入在它被调用的地方2,调用几次就嵌入几次,没有使用call指令。这样省去了函数调用时的一些额外开销,比如保存和恢复函数返回地址等,可以加快速度。不过调用次数多的话,会使可执行文件变大,这样会降低速度。相比起宏来说,内核开发者一般更喜欢使用内联函数。因为内联函数没有长度限制,格式限制。编译器还可以检查函数调用方式,以防止其被误用。
static inline的内联函数,一般情况下不会产生函数本身的代码,而是全部被嵌入在被调用的地方。如果不加static,则表示该函数有可能会被其他编译单元所调用,所以一定会产生函数本身的代码。所以加了static,一般可令可执行文件变小。内核里一般见不到只用inline的情况,而都是使用static inline。
有时候也可以见到static __inline__。gcc编译参数“-ansi”或者“-std=c89”等会禁用一些关键字,比如“asm”,“inline”等。为了在这种情况下使用内联函数,gcc提供了“__inline__”关键字。不过内核使用的标准是gnu89,所以“inline”和“__inline__”在内核中都可以使用。现在“inline”已经成了C99标准的关键字。gcc未来的缺省标准也将变成基于C99的gnu99。所以现在写内核代码时,推荐使用“inline”关键字,而不要用“__line__”。
likely()/unlikely()
likely()和unlikely()这两个宏可以向编译器提供分支预测信息。它们允许开发者通过编译器告诉CPU,某部分代码有更高的可能性被执行,那些可能很少被执行,CPU就可以根据这些信息来作出合理的指令预取的决定,从而加快指令读取过程。
likely()和unlikely()一般用于判断真假的地方,比如定义于kernel/time.c中的这个函数:
1asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)
2{
3 if (likely(tv != NULL)) {
4 struct timeval ktv;
5 do_gettimeofday(&ktv);
6 if (copy_to_user(tv, &ktv, sizeof(ktv)))
7 return -EFAULT;
8 }
9 if (unlikely(tz != NULL)) {
10 if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
11 return -EFAULT;
12 }
13 return 0;
14}
它们并不会改变原有条件的真假,只是向编译器传达信息-原有条件为真的几率是高还是低。上面这段代码中第3行的likely(tv != NULL)表示tv!=NULL时条件为真,而且tv绝大多数情况下不等于NULL。第9行的unlikely(tz != NULL)表示tz != NULL时条件为真,但是tz绝大多数情况下等于NULL。
likely()和unlikely()定义于include/linux/compiler.h:
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
这里应用了内建函数__builtin_expect()。
__init/__initdata
__init告诉编译器它所标记的函数只在初始化阶段会被用到。编译器就会把它所标记的函数的代码放到一个特殊的内存区域,这块区域在初始化阶段结束后会被释放掉。比如arch/i386/kernel/i8259.c里的这个函数:
void __init init_IRQ(void)
这个函数的作用是在整个系统启动时初始化中断。很多只有在系统启动时才执行的函数都会被标记成__init。此外设备驱动程序的初始化函数一般也都会被标记成__init。
只在初始化阶段会被用到的数据,一般会被标记成__initdata。比如定义于init/do_mounts.c里的加载根设备时用到的这个指向根设备文件名的指针:
char * __initdata root_device_name;
类似的,__exit/__exitdata宏被用来标记在退出阶段会被用到的函数和数据。比如在注销设备驱动时。
数组/结构体成员的指定初始化
C89标准要求数组或者结构体成员必须按照指定的顺序来初始化。C99和C89的gnu扩展可以允许通过指定数组索引或者结构体成员名来初始化数组或者结构体成员。要把数组里连着的几个元素用同一个值初始化,可以这样写“[FIRST ... LAST] = VALUE”。这是一个gnu扩展。kernel/irq/handel.c里数组irq_desct的初始化集中体现了这两点的应用:
irq_desc_t irq_desc[NR_IRQS] __cacheline_aligned = {
[0 ... NR_IRQS-1] = {
.status = IRQ_DISABLED,
.handler = &no_irq_type,
.lock = SPIN_LOCK_UNLOCKED
}
};
其中NR_IRQS是一个与架构相关的宏。在i386架构里,其定义于头文件 include/asm-i386/mach-default/irq_vectors_limits.h中,定义如下:
#define NR_IRQS 16
而irq_desc_t是一个结构体,定义于include/linux/irq.h,定义如下:
typedef struct irq_desc {
hw_irq_controller *handler;
void *handler_data;
struct irqaction *action;
unsigned int status;
unsigned int depth;
unsigned int irq_count;
unsigned int irqs_unhandled;
spinlock_t lock;
#if defined (CONFIG_GENERIC_PENDING_IRQ) || defined (CONFIG_IRQBALANCE)
unsigned int move_irq;
#endif
} ____cacheline_aligned irq_desc_t;
前面那段代码把数组里每个irq_desc结构体的status,handler和lock这三个成员初始化成指定的相同的值,每个结构体的其他成员此时均为0。 |
|