Linux 内核中的GCC特性了解用于C语言的GCC扩展.docx
《Linux 内核中的GCC特性了解用于C语言的GCC扩展.docx》由会员分享,可在线阅读,更多相关《Linux 内核中的GCC特性了解用于C语言的GCC扩展.docx(7页珍藏版)》请在冰豆网上搜索。
![Linux 内核中的GCC特性了解用于C语言的GCC扩展.docx](https://file1.bdocx.com/fileroot1/2023-4/2/1bc123c6-5c37-4d52-92c2-6b9f01cafb58/1bc123c6-5c37-4d52-92c2-6b9f01cafb581.gif)
Linux内核中的GCC特性了解用于C语言的GCC扩展
GCC和Linux是出色的组合。
尽管它们是独立的软件,但是Linux完全依靠GCC在新的体系结构上运行。
Linux还利用GCC中的特性(称为扩展)实现更多功能和优化。
本文讨论一些重要的扩展,讲解如何在Linux内核中使用它们。
GCC当前的稳定版本(版本4.3.2)支持C标准的三个版本:
InternationalOrganizationforStandardization(ISO)最初的C语言标准(ISOC89或C90)
带修正1的ISOC90
当前的ISOC99(这是GCC使用的默认标准,本文也假设采用这种标准)
注意:
本文假设使用ISOC99标准。
如果指定比ISOC99版本旧的标准,那么可能无法使用本文描述的一些扩展。
可以在命令行上使用-std选项指定GCC使用的实际标准。
可以通过GCC手册查看哪个标准版本支持哪些扩展。
可以以几种方式对可用的C扩展进行分类。
本文把它们分为两大类:
功能性扩展提供新功能。
优化扩展帮助生成更高效的代码。
功能性扩展
先讨论一些扩展标准C语言的GCC扩展。
类型发现
GCC允许通过变量的引用识别类型。
这种操作支持泛型编程。
在C++、Ada和Java™语言等许多现代编程语言中都可以找到相似的功能。
Linux使用typeof构建min和max等依赖于类型的操作。
清单1演示如何使用typeof构建一个泛型宏(见./linux/include/linux/kernel.h)。
清单1.使用typeof构建一个泛型宏
#define min(x, y) ({ \
typeof(x) _min1 = (x); \
typeof(y) _min2 = (y); \
(void) (&_min1 == &_min2); \
_min1 < _min2 ?
_min1 :
_min2; })
范围扩展
GCC支持范围,在C语言的许多方面都可以使用范围。
其中之一是switch/case块中的case语句。
在复杂的条件结构中,通常依靠嵌套的if语句实现与清单2(见./Linux/drivers/scsi/sd.c)相同的结果,但是清单2更简洁。
使用switch/case也可以通过使用跳转表实现进行编译器优化。
清单2.在case语句中使用范围
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 */
}
}
还可以使用范围进行初始化,如下所示(见./linux/arch/cris/arch-v32/kernel/smp.c)。
在这个示例中,spinlock_t创建一个大小为LOCK_COUNT的数组。
数组的每个元素初始化为SPIN_LOCK_UNLOCKED值。
/* Vector of locks used for various atomic operations */
spinlock_t cris_atomic_locks = { [0 ... LOCK_COUNT - 1] = SPIN_LOCK_UNLOCKED};
范围还支持更复杂的初始化。
例如,以下代码指定数组中几个子范围的初始值。
int widths = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
零长度的数组
在C标准中,必须定义至少一个数组元素。
这个需求往往会使代码设计复杂化。
但是,GCC支持零长度数组的概念,这对于结构定义尤其有用。
这个概念与ISOC99中灵活的数组成员相似,但是使用不同的语法。
下面的示例在结构的末尾声明一个没有成员的数组(见./Linux/drivers/IEee1394/raw1394-private.h)。
这允许结构中的元素引用结构实例后面紧接着的内存。
在需要数量可变的数组成员时,这个特性很有用。
struct iso_block_store {
atomic_t refcount;
size_t data_size;
quadlet_t data[0];
};
判断调用地址
在许多情况下,需要判断给定函数的调用者。
GCC提供用于此用途的内置函数__builtin_return_address。
这个函数通常用于调试,但是它在内核中还有许多其他用途。
如下面的代码所示,__builtin_return_address接收一个称为level的参数。
这个参数定义希望获取返回地址的调用堆栈级别。
例如,如果指定level为0,那么就是请求当前函数的返回地址。
如果指定level为1,那么就是请求进行调用的函数的返回地址,依此类推。
void * __builtin_return_address( unsigned int level );
在下面的示例中(见./linux/kernel/softirq.c),local_bh_disable函数在本地处理器上禁用软中断,从而禁止在当前处理器上运行softirqs、tasklets和bottomhalves。
使用__builtin_return_address捕捉返回地址,以便在以后进行跟踪时使用这个地址。
void local_bh_disable(void)
{
__local_bh_disable((unsigned long)__builtin_return_address(0));
}
常量检测
在编译时,可以使用GCC提供的一个内置函数判断一个值是否是常量。
这种信息非常有价值,因为可以构造出能够通过常量叠算(constantfolding)优化的表达式。
__builtin_constant_p函数用来检测常量。
__builtin_constant_p的原型如下所示。
注意,__builtin_constant_p并不能检测出所有常量,因为GCC不容易证明某些值是否是常量。
int __builtin_constant_p( exp )
Linux相当频繁地使用常量检测。
在清单3所示的示例中(见./linux/include/linux/log2.h),使用常量检测优化roundup_pow_of_two宏。
如果发现表达式是常量,那么就使用可以优化的常量表达式。
如果表达式不是常量,就调用另一个宏函数把值向上取整到2的幂。
清单3.使用常量检测优化宏函数
#define roundup_pow_of_two(n) \
( \
__builtin_constant_p(n) ?
( \
(n == 1) ?
1 :
\
(1UL << (ilog2((n) - 1) + 1)) \
) :
\
__roundup_pow_of_two(n) \
)
函数属性
GCC提供许多函数级属性,可以通过它们向编译器提供更多数据,帮助编译器执行优化。
本节描述与功能相关联的一些属性。
下一节描述影响优化的属性。
如清单4所示,属性通过其他符号定义指定了别名。
可以以此帮助阅读源代码参考,了解属性的使用方法(见./linux/include/linux/compiler-gcc3.h)。
清单4.函数属性定义
# define __inline__ __inline__ __attribute__((always_inline))
# define __deprecated __attribute__((deprecated))
# define __attribute_used__ __attribute__((__used__))
# define __attribute_const__ __attribute__((__const__))
# define __must_check __attribute__((warn_unused_result))
清单4所示的定义是GCC中可用的一些函数属性。
它们也是在Linux内核中最有用的函数属性。
下面解释如何使用这些属性:
always_inline让GCC以内联方式处理指定的函数,无论是否启用了优化。
deprecated指出函数已经被废弃,不应该再使用。
如果试图使用已经废弃的函数,就会收到警告。
还可以对类型和变量应用这个属性,促使开发人员尽可能少使用它们。
__used__告诉编译器无论GCC是否发现这个函数的调用实例,都要使用这个函数。
这对于从汇编代码中调用C函数有帮助。
__const__告诉编译器某个函数是无状态的(也就是说,它使用传递给它的参数生成要返回的结果)。
warn_unused_result让编译器检查所有调用者是否都检查函数的结果。
这确保调用者适当地检验函数结果,从而能够适当地处理错误。
下面是在Linux内核中使用这些属性的示例。
deprecated示例来自与体系结构无关的内核(./linux/kernel/resource.c),const示例来自IA64内核源代码(./linux/arch/ia64/kernel/unwind.c)。
int __deprecated __check_region(struct resource
*parent, unsigned long start, unsigned long n)
static enum unw_register_index __attribute_const__
decode_abreg(unsigned char abreg, int memory)
优化扩展
现在,讨论有助于生成更好的机器码的一些GCC特性。
分支预测提示
在Linux内核中最常用的优化技术之一是__builtin_expect。
在开发人员使用有条件代码时,常常知道最可能执行哪个分支,而哪个分支很少执行。
如果编译器知道这种预测信息,就可以围绕最可能执行的分支生成最优的代码。
如下所示,__builtin_expect的使用方法基于两个宏likely和unlikely(见./Linux/include/linux/compiler.h)。
#define likely(x) __builtin_expect(!
!
(x), 1)
#define unlikely(x) __builtin_expect(!
!
(x), 0)
通过使用__builtin_expect,编译器可以做出符合提供的预测信息的指令选择决策。
这使执行的代码尽可能接近实际情况。
它还可以改进缓存和指令流水线。
例如,如果一个条件标上了“likely”,那么编译器可以把代码的True部分直接放在分支指令后面(这样就不需要执行分支指令)。
通过分支指令访问条件结构的False部分,这不是最优的方式,但是访问它的可能性不大。
按照这种方式,代码对于最可能出现的情况是最优的。
清单5给出一个使用likely和unlikely宏的函数(见./linux/net/core/datagram.c)。
这个函数预测sum变量将是零(数据包的checksum是有效的),而且ip_summed变量不等于CHECKSUM_HW。
清单5.likely和unlikely宏的使用示例
unsigned int __skb_checksum_complete(struct sk_buff *skb)
{
unsigned int sum;
sum = (u16)csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));
if (likely(!
sum)) {
if (unlikely(skb->ip_summed == CHECKSUM_HW))
netdev_rx_csum_fault(skb->dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
}
return sum;
}
预抓取
另一种重要的性能改进方法是把必需的数据缓存在接近处理器的地方。
缓存可以显著减少访问数据花费的时间。
大多数现代处理器都有三类内存:
一级缓存通常支持单周期访问
二级缓存支持两周期访问
系统内存支持更长的访问时间
为了尽可能减少访问延时并由此提高性能,最好把数据放在最近的内存中。
手工执行这个任务称为预抓取。
GCC通过内置函数__builtin_prefetch支持数据的手工预抓取。
在需要数据之前,使用这个函数把数据放到缓存中。
如下所示,__builtin_prefetch函数接收三个参数:
数据的地址
rw参数,使用它指明预抓取数据是为了执行读操作,还是执行写操作
locality参数,使用它指定在使用数据之后数据应该留在缓存中,还是应该清除
void __builtin_prefetch( const void *addr, int rw, int locality );
Linux内核经常使用预抓取。
通常是通过宏和包装器函数使用预抓取。
清单6是一个辅助函数示例,它使用内置函数的包装器(见./linux/include/linux/prefetch.h)。
这个函数为流操作实现预抓取机制。
使用这个函数通常可以减少缓存缺失和停顿,从而提高性能。
清单6.范围预抓取的包装器函数
#ifndef ARCH_HAS_PREFETCH
#define prefetch(x) __builtin_prefetch(x)
#endif
static inline void prefetch_range(void *addr, size_t len)
{
#ifdef ARCH_HAS_PREFETCH
char *cp;
char *end = addr + len;
for (cp = addr; cp < end; cp += PREFETCH_STRIDE)
prefetch(cp);
#endif
}
变量属性
除了本文前面讨论的函数属性之外,GCC还为变量和类型定义提供了属性。
最重要的属性之一是aligned属性,它用于在内存中实现对象对齐。
除了对于性能很重要之外,某些设备或硬件配置也需要对象对齐。
aligned属性有一个参数,它指定所需的对齐类型。
下面的示例用于软件暂停(见./Linux/arch/i386/mm/init.c)。
在需要页面对齐时,定义PAGE_SIZE对象。
char __nosavedata swsusp_pg_dir[PAGE_SIZE]
__attribute__ ((aligned (PAGE_SIZE)));
清单7中的示例说明关于优化的两点:
packed属性打包一个结构的元素,从而尽可能减少它们占用的空间。
这意味着,如果定义一个char变量,它占用的空间不会超过一字节(8位)。
位字段压缩为一位,而不会占用更多存储空间。
这段源代码使用一个__attribute__声明进行优化,它用逗号分隔的列表定义多个属性。
清单7.结构打包和设置多个属性
static struct swsusp_header {
char reserved[PAGE_SIZE - 20 - sizeof(swp_entry_t)];
swp_entry_t image;
char orig_sig[10];
char sig[10];
} __attribute__((packed, aligned(PAGE_SIZE))) swsusp_header;
结束语
本文只讨论了在Linux内核中可以使用的几个GCC特性。
可以通过GNUGCC手册进一步了解针对C和C++语言的所有扩展。
另外,尽管Linux内核经常使用这些扩展,但是也可以在用户自己的应用程序中使用它们。
随着GCC的发展,肯定会出现新的扩展,它们会进一步改进性能和增加Linux内核的功能。