yihect | 06 八月, 2010 13:11
我们在浏览Linux内核源代码时,经常会碰到一些非常奇怪的代码记号。按照我们之前对C语言的一般掌握,这些代码记号通常都算不上是符合C语言语法的。那这是怎么回事呢?其实,这基本上是GNU C对 C语言标准的扩充,有了这些扩充,人们就可以更方便的开发,开发出的程序经过GCC编译后,也会产生更有效的二进制代码。
通常情况下,我们写C代码都不大会用到这些扩充,这只是因为很简单的原因,那就是我们还不知道它们带来的好处,或者我们是在用GCC以外的编译器做开发。不过如果我们想探索研究Linux内核代码的话,可就一定要熟悉这些扩充,否则还真的就理解不了其中的代码。谁让GCC和Linux结合的如此紧密呢?
在这些扩充中,有的是为了提供某些原来C语言规范所不具备的语言能力,从而让我们能更方便的用扩充过的C(GNU C)来开发;也有的是为了让编译器在编译程序时,尽最大可能的优化我们写出来的程序,从而产生运行时更加有效率的二进制代码。在这里,我仅介绍几种浏览内核代码过程中最为常见的几种扩充,其他更多的知识可参见相关资料。
⒈ 复合语句作为表达式
表达式可以用园括号括起来使用。而将用分号分隔的多条语句看成一个整体,并用大括号将其括起来,则构成为一个复合语句。复合语句也可以放在园括号中变成一个表达式来使用。
这个表达式的类型为复合语句中以分号结尾的最后一个子语句表达式的类型,其值也为此一最后子表达式的值。
复合语句的应用如在内核中文件 printf.c 的 第37行:
#define do_div
(n,base
) ({ \
int __res; \
__res = ((unsigned long) n) % (unsigned) base; \
n = ((unsigned long) n) / (unsigned) base; \
__res; })
也许你想知道如何使用这个宏,在这个文件的第86行可以找到:
if (num ==
0)
tmp
[i++
] =
'0';
else
while (num != 0)
tmp
[i++
] =
(digits
[do_div
(num, base
)] | locase
);
⒉ 取得表达式的数据类型
不像其他更高阶的语言,比如C++,JAVA等在一定程度上能让您动态取得变量或表达式的数据类型,C语言是不允许这样做的。但是在GNU C里面却可以用 typeof() 这样的操作符来取得表达式的类型。其用法和 sizeof() 差不多,像这样:
typeof
(arr
[4][3])
typeof
(int *
)您甚至可以这样来定义一个新的变量:
typeof
(*p
) y;
在内核代码里面,这个扩展用的也很多。最多的地方就是用来定义宏,比如文件 kernel.h 的 第556行:
#define min
(x, y
) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ? _min1 : _min2;
})⒊ 零长的数组
标准的C语言里面,要求数组最少长度为1个字节。但是GCC允许定义零长数组。这在定义结构体时特别有用,将其放在结构体的最后面,用来表示长度变化的字段。比方内核文件 raw1394-private.h 中的第12行:
struct iso_block_store
{
atomic_t refcount;
size_t data_size;
quadlet_t data
[0];
};
⒋ 范围标记
GCC还扩充了范围标记,通过它可以表示一段数值范围。这可以用在C程序的很多地方。最常用的莫过于 switch/case 语句中,比方内核文件 sd.c 的第312行:
static int sd_major
(int major_idx
)
{
switch (major_idx)
{
case 0:
return SCSI_DISK0_MAJOR;
case 1 ... 7:
return SCSI_DISK1_MAJOR + major_idx - 1;
case 8 ... 15:
return SCSI_DISK8_MAJOR + major_idx - 8;
default:
BUG();
return 0; /* shut up gcc */
}
}范围标记还可以用来初始化数组里面的一些连续元素,如文件 numa.c 中的第41行:
/* maps to convert between proximity domain and logical node ID */ static int pxm_to_node_map[MAX_PXM_DOMAINS]
= { [0 ... MAX_PXM_DOMAINS - 1] = NID_INVAL };
static int node_to_pxm_map[MAX_NUMNODES]
=
{ [0 ...
MAX_NUMNODES -
1] = PXM_INVAL
};
⒌ 给函数、变量和数据类型指定属性
属性是我们程序员向编译器传送信息或命令的工具,我们通常用它们来命令编译器在编译程序的时候,帮我们完成某些特殊的处理。属性可以被指定到不同种类的对象上,包括函数、变量和类型等。
指定属性时,必须先用关键字“__attribute__”,然后再在后面跟上用两个圆括号括起来的属性列表,属性列表中的属性用逗号隔开。整个属性指示就像这样:
__attribute__
((attr_
1,attr_
2,attr_
3))在内核代码里,时常会用一个宏定义来简化它。比方你可以在文件 compiler-gcc.h 中找到:
# define __inline__ __inline__ __attribute__
((always_inline
)) # define __deprecated __attribute__((deprecated))
# define __attribute_used__ __attribute__((__used__))
# define __must_check __attribute__
((warn_unused_result
))用宏定义过之后,就可以使用较为简单的形式,如__inline__,__deprecated__,__attribute_used__ 之类的了。
⒌⒈ 给函数指定属性
上面宏中所列出的属性,都是指定给函数的常用属性。我们做个简单说明:
- always_inline 在C里面,我们通常使用关键字inline来向编译器建议内联我们的函数,而这些函数是否真正得到内联则取决于编译器。但是这里用此属性则命令编译器总是要内联我们的函数,而不是仅仅将其作为一个简单的来自程序员的建议。
- deprecated 我们用这个属性请求编译器告诉另外的程序员说这个函数接口已经过时,已被废弃而不应该再使用了。倘若其他程序员在他们的程序里用了这类的函数,则他们在编译的时候会得到警告。
- __used__ 通常如果你写了一个函数,但是并没有用它,那么编译器编译的时候会给出警告。我们用此属性告诉编译器一定不要发出这样的警告,即使我们真的在其他C程序里面没有调用这个函数。假如你只有在汇编代码里调用了此函数,那么这将会很有用。
- warn_unused_result 用此属性命令编译器去检查函数调用者是否已经检验函数返回值了。如果你写了一个函数,其本身要求调用者必须检查该函数的返回值,那就用上这个属性。因为倘若程序员忘记检查函数返回值了,编译器在编译的时候会给出警告。
那如何使用这些属性呢,你可以查找内核代码,比如 文件 dma_mapping.h 的 第113行:
/* for backwards compatibility, removed soon */
static inline void __deprecated dma_sync_single(struct device *dev,
dma_addr_t addr, size_t size,
enum dma_data_direction dir)
{
dma_sync_single_for_cpu(dev, addr, size, dir);
}再如 pci.h 的第 609 行:
#ifdef CONFIG_PCI_LEGACY
struct pci_dev __deprecated *pci_find_device(unsigned int vendor,
unsigned int device, struct pci_dev *from);
#endif
/* CONFIG_PCI_LEGACY */⒌⒉ 给变量指定属性
属性也可以指定到变量身上,举两个例子:
⒌⒊ 给数据类型指定属性
我们还可以把属性指定给结构体和联合体等数据类型上。也举个例子:
⒍ 条件跳转时依据可能性多少进行优化
在内核代码中最普遍的优化就是用__builtin_expect来优化。您在程序中处理条件逻辑操作时,作为程序的作者,您通常都知道哪种条件更可能发生,而哪种条件则不太可能发生。那如果编译器也知道这方面的信息后,它就可以对你的条件逻辑操作进行优化,得到最有效率的二进制代码。
在内核代码中,用__builtin_expect来优化,是基于文件 compiler.h 中定义的两个宏:
# define likely
(x
) __builtin_expect
(!!
(x
),
1)
# define unlikely
(x
) __builtin_expect
(!!
(x
),
0)当条件x很可能为true时,编译器就将条件操作的true分支放在条件判断后面紧接着的位置,而将false分支用一条跳转语句去实现。反之,如果条件x很不可能为true时,它就将false分支紧挨着放在条件判断的后面,而用跳转语句实现true分支。用这种方式实现的二进制代码更加有效?为什么?:)想想cache的原理。
至于如何使用,您可从 内核文件 datagram.c 的 第24行 看出端倪:
__sum16 __skb_checksum_complete_head
(struct sk_buff *skb,
int len
) {
__sum16 sum;
sum = csum_fold(skb_checksum(skb, 0, len, skb->csum));
if (likely(!sum))
{
if (unlikely(skb->ip_summed == CHECKSUM_COMPLETE))
netdev_rx_csum_fault(skb->dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
}
return sum;
}
在这个函数内,程序员预料到 sum 很可能为0,而skb->ip_summed则不太可能等于CHECKSUM_COMPLETE。编译器得知此信息后将做出优化。
⒍ 根据所计算的值是否为常量而进行优化
我们可以用GCC内嵌的函数__builtin_constant_p来判断一个值是否为常量。该函数原型如下:
int __builtin_constant_p
( exp
);
在Linux内核代码中用此函数来进行是否常量的侦测相当普遍,比如在文件 log2.h 中的第 19 行:
#define roundup_pow_of_two
(n
) \
( \
__builtin_constant_p(n) ? ( \
(n == 1) ? 1 : \
(1UL << (ilog2((n) - 1) + 1)) \
) : \
__roundup_pow_of_two(n) \
)该函数用来向上计算离开输入整数n最近的2的幂值。如果输入参数为常量,它就用移位的方式来计算,否则它调用另外一个函数__roundup_pow_of_two()去计算。
最后,需要说明的是,本文仅介绍了几种在阅读内核代码过程中经常会碰到的几种 GNU C语言扩充。读者如遇到其他在此文中未提到的扩充,还请参考相关资料或者在mail list中讨论。您可以参考这个链接: