位置:海鸟网 > IT > linux/Unix >

Linux TCP/IP协议栈的通用编码模式解析

和其他内核功能一样,每个网络功能都是内核成员中的一个。因此,它必须合理且公平的使用内存, 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来实现。