|
...
}
在这个例子中,netfilter的调试功能需要sk_buff结构中的nf_debug项。如果内核不支持netfilter
调试功能(只有一小部分开发人员需要这项功能),就没必要包含这一项, 否则它只会使得每个网络包占用更多的内存。
函数中,包含或不包含某段代码
int ip_route_input(...)
{
...
if (rth->fl.fl4_dst == daddr &&
rth->fl.fl4_src == saddr &&
rth->fl.iif == iif &&
rth->fl.oif == 0 &&
#ifndef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark == skb->nfmark &&
#endif
rth->fl.fl4_tos == tos) {
...
}
}
为一个函数选择正确的原型
#ifdef CONFIG_IP_MULTIPLE_TABLES
struct fib_table * fib_hash_init(int id)
#else
struct fib_table * _ _init fib_hash_init(int id)
{
...
}
为函数选择正确的定义
#ifndef CONFIG_IP_MULTIPLE_TABLES ... static inline struct fib_table *fib_get_table(int id)
{
if (id != RT_TABLE_LOCAL)
return ip_fib_main_table;
return ip_fib_local_table
} ...
#else
...
static inline struct fib_table *fib_get_table(int id)
{
if (id == 0)
id = RT_TABLE_MAIN;
return fib_tables[id]; }
...
#endif
请注意这个例子和前一个例子的区别。在前一个例子中,函数体在 #ifdef/#endif 块之外, 而这个例子中,每一块都包含一个完整的函数定义。
变量定义或初始化,还有宏,都可以使用条件编译。
知道某个函数或宏有多个定义非常重要,具体使用哪个函数或者宏与条件编译有关,前面就有这样的例子。否则,你看的函数,变量或者宏的定义可能不是你所希望看到的那个。
9. 条件检查的编译优化
大多数时候,内核会把一个变量与一个外部值比较来看某一条件是否已满足,这种比较的结果很大程度上是可以预测的。这样的例子很常见,例如,检查合法性的代码。内核使用likely和
unlikely两个宏来分别表示返回的结果是true(1)或false(0)。这两个宏使用gcc可根据它的返回值来优化编译结果的特性来提升代码的性能。
这里有个例子。我们假设调用do_something函数,在出错的情况下,我们调用handle_error来处理错误:
err = do_something(x,y,z);
if (err)
handle_error(err);
如果do_something很少出错,我们可以将代码重写为:
err = do_something(x,y,z);
if (unlikely(err))
handle_error(err);
一个能够使用likely和unlikely宏来优化代码的例子是处理ip option。由于ip option只在特定的情况下出现,内核可以假设大多数的ip包都没有ip option.当内核转发一个ip包时,它不需要关心
Ip option选项。在ip_forward_finish里面,处理转发包的最后阶段,这个函数使用unlikely宏来检查是否有ip选项需要处理。
10. 互斥
锁在网络代码中大量使用,你会发现本书的每个话题都会涉及到它。互斥,锁机制和同步是编程领域中的一个很有意思,但也很复杂的话题,尤其是在内核编程领域。经过多年的发展和优化,linux内核中已经包含了多种途径来完成代码的互斥。这里我们重点描述网络代码中用的锁。
每种互斥机制都有它适用的环境。下面描述的是网络代码中常见的互斥机制:
自旋锁
这个锁,同一时刻只能由一个线程占有。其他试图得到锁的线程会重复尝试得到锁直到这个锁被释放。由于循环会有一定的消耗,所以,这个锁一般用在多处理器系统里面,而且开发者都希望线程占有锁的时间不会太长。由于其他尝试得到锁的线程也有一定的消耗,所以,占有锁的线程,其执行过程不能睡眠。
读写自旋锁
如果一个锁的用户可以明确分为只读和读写两类,这种情况下,推荐使用读写自旋锁。自旋锁和读写自旋锁的区别在于后者可以由多个读者同时占有,但是,如果写者获得了锁,读者就不能获取这个锁。由于读者的优先级比写者高,所以这个锁适用于读者多(或者只读的锁定请求多),而写者少(或者写的锁定请求少)的情况。
如果锁被读者获得,写者就不能获得这个锁;只有读者释放了这个锁,写者才能获得这个锁。
读-拷贝-更新Read-Read-Update(RCU)
RCU是linux最新提供的一种互斥机制。它在以下情况下表现良好:
读请求多而读写请求很少
获得锁的代码自动执行,而且不会睡眠.
被锁保护的数据结构通过指针访问
第一条规则与性能有关,后面两条规则是使用RCU时的限制条件。
值得注意的是,按照第一条规则的条件,一般都使用读写自旋锁来做为RCU的替代。为了理解为什么在某些情况下RCU的性能要高于读写自旋锁,你需要考虑其他一些因素,比如SMP系统中的处理器缓存的影响等。
网络代码中使用RCU的例子是路由子系统。路由查找的次数比路由更新的次数多,而且路由查找代码不会在中途被阻塞。
内核提供了信号量,但是本书描述的网络代码中很少用到它。
11.主机字节顺序和网络字节顺序序的转换
多于一个字节的数据结构在内存中的存储有两种不同的格式:Little Endian和 Big Endian。Little Endian格式把最低字节存储在最低的地址中,而Big Endian刚好与此相反。linux操作系统使用的存储格式与具体的处理器有关。例如,intel处理器使用Little Endian模型,而Motorola处理器使用Big Endian模型。
假设我们的linux主机从远端的主机上收到了一个IP包。由于不知道这个包在远端的主机上是以何种方式发送的,我们就不知道如何去读取这个包的包头。因为这个原因,每个协议簇都必须定义它的网络包的存储格式。比如,TCP/IP协议栈就使用Big Endian模型。
但是内核开发人员依然面临一个问题:必须编写可以在不同处理器,不同存储模式下使用的代码。有些处理器的存储模式可能与网络包的存储模式一致,这种情况下,就不需要转换网络包的存储格式。
但是,每次内核读写ip头中超过一个字节的变量时,它都必须首先将网络字节序转换成主机字节序或是相反。这个原则同样适用于TCP/IP协议栈中的其他协议。如果网络字节序和本机字节序一致,转换函数就执行一个空操作,因为它们之间不需要转换。这样做可以提高代码的可移植性,因为这种情况下,只有转换函数是与平台相关的。
表1列出了转换两字节和四字节变量时用到的函数:
Table 1. Byte-ordering conversion routines Macro
Meaning (short is 2 bytes, long is 4 bytes)
htons
Host-to-network byte order (short)
htonl
Host-to-network byte order (long)
ntohs
Network-to-host byte order (short)
ntohl
Network-to-host byte order (long)
这些宏的定义放在include/linux/byteorder/generic.h头文件中。下面是每个平台如何把本平台的存储格式与这些宏的定义关联起来的:
每个平台相关的目录下include/asm-XXX/,都有一个文件byteorder.h。
这个文件包含include/linux/byteorder/big_endian.h和include/linux/byteorder/little_endian.h两个文件中的一个,具体包含哪个,与处理器的存储格式有关
little_endian.h和big_endian.h两个文件都包含通用文件include/linux/byteorder/generic.h。表1中的宏的定义依赖于little_endian.h和big_endian.h中定义的宏,这样,不同平台的存储格式就会影响到表1中定义的宏。
表1中定义的每个宏xxx都有一个与之对应的宏__constant_xxx,用于转换常量的存储格式,例如,一个枚举类型的元素。值得注意的是,表1中的宏是通用的宏,不管它的输入值是常量还是变量。
我们前面说过,存储格式对超过一个字节的数据项非常重要。存储格式对超过一个字节的位域定义同样非常重要。例如,IPV4的头部定义。内核使用_ LITTLE_ENDIAN_BITFIELD和_ _BIG_ENDIAN_BITFIELD两个条件编译参数来控制数据结构的定义,这两个条件在前面所述的
little_endian.h和big_endian.h两个文件中定义。
12. Catching Bugs
追踪有些函数只能在某种条件下调用,或者不能在某种条件下调用。内核使用BUG_ON和BUG_TRAP宏来捕获那些未满足条件的函数调用。如果BUG_TRAP的输入值是false
, 内核打印一段告警信息。而BUG_ON会打印一段出错信息并且使内核崩溃。
13. Statistics
在实现某项功能时,统计某个特定条件出现的次数是个好习惯。比如统计缓存命中和失败的次数,内存分配成功和失败的次数等。本书会列出并描述每一个在网络代码中出现的统计变量。
14. 计时
内核经常需要测量从某个给定时刻开始经过了多长时间。例如,某个cpu使用量大的任务通常会在给定的时间段后释放cpu。如果被重新调度后,它会继续运行。这对内核程序非常重要,虽然linux内核支持内核抢占。网络代码中,一个常见的例子就是实现垃圾收集的例程。
内核空间中时间用时钟嘀哒来计量。一个嘀哒是两个连续的时钟中断之间的时间隔。时钟处理不同的任务(在这里,我们不关注它们),并且每秒发生HZ次。HZ是一个体系结构相关的变量。例如,如果在i386机器上把它初始化为1000,就意味着每秒发生1000次时钟中断,并且两个连续中断之间的时间隔是1毫秒。
每一次时钟中断都会把全局变量jiffies加一。这就意味着,在任何时刻,jiffies代表从开机到现在所发生的嘀哒的数量,并且n*HZ值一般都表示n秒。
如果一个函数需要测量时间隔,它可以把当前的jiffies值保存到一个局部变量中,然后把这个值与后续时刻的jiffies相比较以求得它们之间的差值。通过这个差值(两个时刻间的嘀哒数量)就可以计算出从计时开始经过了多长时间。
下面的例子展示了一个函数,它需要执行一些任务,但是它占用cpu的时间不能超过一个嘀哒数。当d 上一页 [1] [2] [3] 下一页
 |
频道声明:本频道的文章除部分特别声明禁止转载的专稿外,可以自由转载.但请务必注明出出处和原始作者 文章版权归本频道与文章作者所有.对于被频道转载文章的个人和网站,我们表示深深的谢意。
| 原始作者:佚名 |
录入时间:2007-7-14 12:36:00 |
| 信息来源:不详 |
投稿信箱:itqoo@126.com |
|
|
 |
|