|
其他内核功能一样,每个网络功能都是内核成员中的一个。因此,它必须合理且公平的使用内存, CPU和其他共享资源。绝大多数功能并非内核中一段独立的程序,而是根据该功能而或多或少的与内核中其他部分相互影响。因此它们总是试图,尽可能的,使用类似的体系结构来实现类似的功能。
对许多内核组件来说有些需求是通用的,比如为同一数据结构分配好几个实例,或者跟踪一个数据结构的参考以避免不安全的内存重分配,等等。下面我们来看linux解决这些需求的一些通用的方法。我们也会谈到在查看内核编码时可能遇到的通用的编码技巧。
1.缓存
内核使用kmalloc和kfree来分配和释放内存。这两个函数的使用方法和用户空间的函数 malloc 和free的使用方法类似.
一个内核组件通常需要分配一个数据结构的多个实例。如果分配和释放频繁发生,相关内核组件的初始化函数(比如路由子系统中的fib_hash_init函数)通常会分配一个特殊的内存缓存以加速内存分配。当一个内存块释放时,它会被返回给与分配时相同的内存缓存。
以下是一些需要内核来维护内存缓存的网络数据结构:
Socket buffer descriptors
这个缓存,由net/core/sk_buff.c中的skb_init分配,它用于分配sk_buff结构。sk_buff结构可能是网络子系统中分配和释放频率最高的数据结构。
Neighboring protocol mappings
邻居协议使用内存缓存来分配neighbour结构,这个结构保存L3到L2的地址映射关系。
Routing tables
路由代码使用两个内存缓存来分配两种数据结构,这两种数据结构定义了路由表。
以下是使用内存缓存时会用到的一些函数:
kmem_cache_create
kmem_cache_destroy
建立或销毁缓存 .
kmem_cache_alloc
kmem_cache_free
从内存缓存中分配或释放一个对象。它们通常会在一个包装函数中被调用,这个包装函数在更高的层次上处理分配和释放的请求。比如:kfree_skb函数处理释放sk_buff的请求,但是只有在所有对此结构的引用释放之后并且相关的子系统(比如,防火墙)已执行了清除操作之后,才调用kmem_cache_free释放这个sk_buff。
从一个给定的内存缓存中能够分配多少个实例的数量限制通常在kmem_cache_alloc的包装函数中指定,但是有时也可以通过/proc文件系统中的参数来调整。
2. 缓存和哈希表
使用缓存来提升性能的技巧很常见。在网络代码中,有L3到L2映射的缓存(比如IPV4中的ARP缓存),路由表缓存,等等。
缓存的查找函数通常都有一个输入值来说明在缓存查找没有命中的情况下,是否需要分配一个新的元素并把它加入到缓存中。而其他类型的查找函数都只会把没有命中的元素添加进去。
缓存通常使用哈希表来实现。内核提供了许多数据结构,比如单向和双向的链表,这些数据结构可以直接用来实现简单的哈希表。
处理相同哈希值的标准方法是把这些元素放入一个链表。但是,遍历这些链表所花的时间通常要比通过哈希值来查找元素所用的时间长。因此,要尽量采用冲突几率小的哈希函数。
如果哈希表(不管它是否用作缓存)的查找时间是一个子系统的关键参数,那么就应该实现一个机制,通过增加哈希表的大小来减小平均的冲突几率,这样可以减小平均的查找时间
你也可以在其他子系统,比如neighboring layer,看到通过给键值增加一个随机变量使得哈希值在缓存bucket中可以均匀的分布。这样可以减小DoS(Denial of Service)的危害,因为这类DoS 都是通过特定参数使得哈希表的表项都集中在同一个哈希值上。
3. 引用计数器
如果一段代码访问一个已经被释放的数据结构,内核是不会感到高兴的,用户也不会为内核的反映而高兴的。为了避免这些烦人的问题,同时也让垃圾收集机制运行得更简便和高效,许多数据结构都会保持一个引用计数。好的内核程序员每次访问一个数据结构时,都会相应的增加或减小这个数据的引用计数。对于那些需要引用计数的数据结构,相应的拥有这个数据结构的内核模块通常会导出两个增加和减小引用计数的函数。这些函数通常被命名为xxx_hold(增加引用计数)和xxx_release(减小引用计数)。有时,减小引用计数的函数也可能被命名为xxx_put(例如
dev_put用于减小net_device结构的引用计数)。
虽然我们假设内核程序员都非常认真,但是,内核程序员也是人,所以他们不可能总是写出没有bug的代码。使用引用计数是一个简单但是有效的方法来防止释放那些还在使用的数据结构。但是,这个方法也不可能解决所有的问题。以下就是一些忘记增加或减小引用计数而引起的后果:
如果你释放了一个数据结构,但是忘了调用xxx_release函数,内核就永远都不会允许释放这个数据结构(除非另一个有bug的代码恰好错误地调用了两次减小引用计数的函数)。这将会导致内存被逐步耗尽.
如果你引用了一个数据结构,但是忘了调用xxx_hold函数,在某一时刻你恰好是这个数据结构的唯一引用者,这时,这个数据结构会被提前释放,原因就是你没有增加它的引用计数。这种情况的危害要比前一个更大。如果你后续的操作试图引用这个结构,将导致其他数据被破坏,或者导致内核立即崩溃。
如果需要释放一个数据结构,就要先通知这个结构的引用者,让它们先减小这个结构的引用计数。这可以通过notification chain来实现。
在以下情况下,需要增加数据结构的引用计数:
两个数据结构有紧密的关系。在这种情况下,一个结构会包含一个初始化指向另一个结构的指针。
一个定时器函数需要访问一个数据结构。在定时器运行时,会增加这个数据结构的引用计数,这样做的原因是:在定时器执行完成前,你不希望这个数据结构被提前释放了。
成功地在一个链表或哈希表中找到一个匹配的表项。在大多数情况下,这个查找到的表项会被查找者使用。因此,在查找函数里面,成功匹配的数据结构需要增加它的引用计数,而这个引用计数会被查找者减小。
当数据结构的最后一个引用被释放后,这个数据结构就可以被释放了,因为它现在已没 有什么作用了。当然,这并不是一个必须的操作。
4. 垃圾收集
内存是一种共享并且有限的资源,所以不应该浪费,尤其是在内核中。因为内核没有使用虚拟内存。大多数的内核子系统都实现了某种类型的垃圾收集机制去收集那些无用的或者过期的数据结构所占用的内存。从已有的实现来看,主要有以下两种垃圾收集机制:
异步
这种类型的垃圾收集机制与具体的事件无关。它使用一个定时器函数去定时检查一组数据结构并把可以释放的数据结构释放掉。判断一个数据结构是否可以被释放的条件依赖于子系统的功能和内部逻辑,但是一个基本的条件就是这个数据结构的引用计数为0。
同步
当内存短缺时,如果不能等到异步的垃圾收集函数定时运行,内核可以激活一个立即执行的垃圾收集函数。在这个函数里,判断一个数据结构是否可以被释放的条件可以和异步的垃圾收集函数不一样(例如,它可以释放一些引用计数不为0的数据结构)。
5. 函数指针和虚拟函数表 (VFTs)
使用函数指针是一个可以得到清晰的C语言代码的同时又能利用面向对象语言的某些优点的好方法。在定义数据结构时(对象),你可以包含一组函数指针(方法)。一部分或全部的数据结构操作可以通过这些函数来完成。在C语言中,数据结构中的函数指针如下所示:
struct sock {
...
void (*sk_state_change)(struct sock *sk);
void (*sk_data_ready)(struct sock *sk, int bytes);
...
};
使用函数指针最大的好处是在初始化对象时,可以根据对象的不同或对象作用的不同而把函数指针赋为不同的值。这样,同样是调用sk_state_change函数,可以就会激活不同sock对象的不同函数。
在网络代码中,函数指针被大量的使用。以下就是一些例子:
在路由子系统中,处理进入或发出包时,它会初始化sk_buff中的两个函数指针。
当一个包准备好往网络设备发送时,它会被传递给net_device结构中的hard_start_xmit函数指针。这个指针在网络设备驱动和网络设备绑定时被赋值。
当L3的协议发送一个包时,它会调用一组函数指针中的一个。这些指针在L3的地址解析协议处理中被初始化。具体调用哪个函数要看它被初始化成哪个函数,L3到L2的地址解析过程是透明的(例如,IPv4使用arp协议)。如果地址解析过程没有必要,这些函数指针会被赋为其他值。
在上面的例子中,我们可以看到,函数指针可以被用作不同内核组件之间的接口;或者是一个子系统在不同的条件下调用不同函数的机制;或者是被用做允许不同的协议,驱动程序或功能可以使用各自不同的方法的机制。
我们来看一个例子。当一个设备驱动向内核注册一个网络设备后,内核会执行一些与设备类型无关的函数。在某些点上,它会调用net_device结构中的一些函数指针来让设备驱动做一些事情。设备驱动可以把这些函数指针初始化为自己的函数,也可以把它置为NULL,这表示内核执行缺省函数就可以了。
在调用函数指针前,要先检查一下函数指针的值,以避免引用空指针。下面是一个从register_netdevice取得的快照例子:
if (dev->init && dev->init(dev) != 0)
{
...
}
函数指针有一个主要的缺点:它使得阅读源代码变得困难一些。当阅读一个给定的代码路径时,你可能会关注其中的函数指针调用。在这种情况下,你需要先了解这个函数指针是如何被初始化的之后,才能阅读后续的代码。函数指针初始化与很多不同的因素有关:
如果函数指针的赋值与特定的数据有关,比如协议标识或者收到包的是某个特定的驱动程序。这种情况下,很容易找到真正的函数。比如,如果drivers/net/3c59x.c驱动收到一个包,你可以在设备初始化函数中找到net_device结构中的函数指针被赋值为那些值。.
如果函数指针的赋值与更复杂的逻辑相关,比如L3到L2地址映射中的状态值。这种情况下,函数指针被赋为何值与外部的事件相关,因而难以预测。
一组函数指针放到一个数据结构中,通常叫做一个虚函数表(VFT)。当虚函数表被用做两个子系统间的接口,比如L3和L4间的接口;或者被导出做为某个内核组件的接口(一组对象)时,它里面可能包含很多不同的指针,这些指针在不同的协议或功能中使用。每一个功能可能只用到一小部分函数指针。当然,如果虚函数表用得过度,就会变得非常庞大,这种情况下,可能需要重新设计你的数据结构。
6. goto 语句
没有哪个c程序员会喜欢goto语句的。如果不看goto语句的历史(计算机编程历史上最长也是最有名的争论之一),我的总结是,goto语句是个过时的东西,但是为什么linux内核还在使用它?
任何一段使用goto语句的代码都可以用无goto语句的代码重写。使用goto语句会降低代码的可读性,同时会增加调试的难度。因为你不能完全确定执行goto之后的语句所需的条件。
让我们来做这样的类比:给定树中的任意节点,你可以明确知道从根到节点的路径。但是如果是随机缠绕的葡萄藤,你就不能总是得到一条从根到节点的唯一路径。
但是,由于c语言没有提供异常捕获机制(其他语言里。异常捕获通常也是被禁止的, 因为使用异常捕获会导致性能下降,并且增加代码的复杂性),小心地使用goto语句可以很容易地将代码跳转到异常处理代码中。在内核编程中,特别是网络代码,异常事件很常见,所以,goto语句就成了一个方便的工具。虽然内核中使用了goto语句,但是我并不主张开发人员滥用它。尽管内核中有超过30,000条goto语句,但是它们主要用于在同一个函数里面返回不同的值,或者跳出超过一层的嵌套。
7. 容器Vector定义
某些情况下,一个数据结构会在末尾包含一个可选的数据块。如下面的例子所示
struct abc {
int age;
char *name[20];
...
char placeholder[0];
}
可选块从placeholder开始。请注意,placeholder被定义成长度为0的Vector。这就意味着,在给abc分配空间时,同时也分配了一个可选块,placeholder就是块的指针。如果不需要可选块,placeholder就只是一个指向结构末尾的指针,它不占用任何空间。
因此,如果abc在不同的代码中被使用,每一个代码都使用相同的基本定义(避免做同样的事情而采用不同的方法),同时可以根据自己的需求来扩展abc。
1.2.8. 条件编译 (#ifdef and family)
编译器的条件编译有时很有必要。过度地使用条件编译会降低代码的可读性,但是我可以声明,linux内核没有滥用它们。条件编译可以在多种情况下使用,但是我们感兴趣的是它用于检查内核是否支持某一特性。make xconfig这样配置工具会决定某一特性是编译到内核,或者完全不支持,或者编译成内核模块。
使用#ifdef或#if defined检查内核是否支持某一特性的例子如下:
包含或者不包含数据结构的某个成员
struct sk_buff {
...
#ifdef CONFIG_NETFILTER_DEBUG
unsigned int nf_debug; #endif [1] [2] [3] 下一页
 |
频道声明:本频道的文章除部分特别声明禁止转载的专稿外,可以自由转载.但请务必注明出出处和原始作者 文章版权归本频道与文章作者所有.对于被频道转载文章的个人和网站,我们表示深深的谢意。
| 原始作者:佚名 |
录入时间:2007-7-14 12:36:00 |
| 信息来源:不详 |
投稿信箱:itqoo@126.com |
|
|
 |
|