内核组件初始化体系结构

Understanding Linux Network Internals 第七章 翻译稿

【翻译】内核组件初始化体系结构

为了全面了解内核组件,你不仅需要了解特定的程序做了什么,也要知道这些程序什么时候被谁调用。内核子系统的初始化是一项基本任务,这些任务由内核根据它自己的模式来处理。这个体系结构值得我们学习并有助于理解网络堆栈的核心组件,包括网络设备驱动程序是如何初始化的。

本章的目的在于展示内核怎样处理用于初始化内核组件的函数,既包含静态嵌入内核的组件,也包括作为内核模块加载的组件,特别是网络设备。我们将会弄明白如下几点:
l初始化函数是如何被特殊的宏来命名与标识
l这些基于内核配置的宏如何被定义,以优化内存使用,确保各种初始化以正确的顺序被执行
l函数什么时候、怎样执行

我们并不讨论初始化体系结构的所有细节,不过你可以快速浏览一遍并舒适的阅读源代码了

7.1、启动时内核选项

Linux 允许用户传递内核配置选项给启动程序,启动程序再把这些选项传递给内核。有经验的用户可以利用这个机制在系统启动时调整内核。在启动阶段,内核有两次调用 parse_args函数(译者注:本书是基于2.6内核,在2.4内核中采用parse_options函数)处理启动时的配置输入。接下来我们在“两 次调用解析中解释”parse_args为何被调用两次。

你可以在Linux BootPrompt HOWTO中找到一些使用启动选项的文档或例子

parse_args是解析具有形如“变量名=值”的输入字符串的函数,它查找关键字并调用相应的处理函数。在加载模块、解析模块命令行参数时,parse_args也会被调用。

我们不必知道parse_args如何实现解析功能的细节,但是我们对内核组件如何为关键字注册处理函数以及处理函数如何被调用感兴趣。为了有一个清晰的认识,我们需要了解:
l在启动字符串中含有关键字时,内核组件如何注册关键字及相对应的执行函数
l内核如何解析关键字和处理函数之间的关联关系,我们将提供一个内核如何解析输入字符串的高级用法。
l网络设备子系统如何使用这个特性

所有的解析代码都在kernel/params.c中,我们在接下来的部分逐步讲述。

7.1.1、注册关键字
内核组件用__setup宏来注册关键字及相关联的处理函数,__setup宏在include/linux/init.h中定义,其原型如下:
__setup(string, function_handler)
其 中:string是关键字,function_handler是关联处理函数。__setup只是告诉内核在启动时输入串中含有string时,内核要去 执行function_handler。String必须以“=”符结束以使parse_args更方便解析。紧随“=”后的任何文本都会作为输入传给 function_handler。
下面的例子来自于net/core/dev.c,其中netdev_boot_setup作为处理程序被注册给“netdev=”关键字:
__setup("netdev=", netdev_boot_setup);
不 同的关键字可以注册相同的处理函数,例如在net/ethernet/eth.c中为“ether =”关键字注册了同样的处理函数 netdev_boot_setup。当代码作为模块被编译时,__setup宏被忽视,你可以在include/linux/init.h中看到 __setup宏是怎样变化的,不管后续包含它的文件是否是模块,include/linux/init.h都是独立的。
start_kernel两次调用parse_args解析启动配置字符串的原因是启动选项事实上分为两类,且每次调用值能够兼顾到其中一类:

缺省选项:
绝大多数选项归于此类,这些选项由__setup宏定义并在第二次调用parse_args时处理。
先期(处理)选项:
在 内核启动阶段,有些选项要在其它选项之前被处理,内核提供了early_param宏以代替__setup宏申明此类选项。这些选项由 parse_early_params函数解析。early_param宏和__setup宏仅有的不同就是前者设置了一个特殊标志让内核能够区分两种不 同的状况。这个标志是我们将在“.init.setup内存区”小节中看到的obs_kernel_param结构的一部分。
启动时选项在内核 2.6中的处理方式已经改变,但并非所有的内核代码都因此而更新。在最近一次改变之前,还仅用__setup宏。因此,遗留下来将被更新的代码现在使用 __obsolete_setup宏。但用户用__obsolete_setup宏定义的选项给内核时,内核打印一条警告消息说明它已是废弃状态,并提供 一个文件指针和随后被公告的源代码行信息。
图7-1概述了几个宏之间的关系:它们都包裹了普通的__setup_param函数。

图7-1 setup_param宏及其包裹物
  

注意:传给__setup宏的程序被放到.init.setup内存节,这样,在“启动时初始化代码”一节中可以很清晰的看出这样做的效果。

7.1.2、两次解析
因 为早期的内核版本(译者注:所谓的早期是相对于2.6内核而言的)中,启动时选项用于做不同的处理,并且这些选项没有全部移植到新的内核版本,所以新内核 要处理这两者情况。当新的体系结构不能够识别关键字,它会采用废弃的体系结构来处理。当废弃的体系结构也处理失败时,系统将把关键字和值一起由 run_init_process函数传递给init进程处理,而init进程是在init内核线程处理后期被调用。然后关键字和值要麽被加入到arg参 数列表,要麽被加到envp环境变量列表。
前面解释了,为了支持先期选项以特定的顺序提前处理,启动串解析和处理调用被做了两次,如图7-2所示(此图是第五章介绍的start_kernel的快照):
1、第一次处理看上去似乎是有特殊标记标识的高优先权的选项先处理(early标志)
2、第二次处理其它的选项,绝大多数选项归于此类,废弃的所有选项都在这次被处理。
第 二次首先检测在新体系结构下是否有匹配的选项处理,这些选项存储在kernel_param结构中,由第五章介绍的module_param宏填充。 module_param宏还确保所有的这些结构数据被放到特殊的内存节(__param),并由 __start___param 和 __stop___param指针分隔。
当识别了这些选项时,相关参数被初始化给选项的值,即启动选项串的function_handler;而当没有选项匹配时,unknown_bootoption函数试着看是否废弃的处理模型能够处理这些选项,如下图示:

图7-2 两次调用选项解析
 

废弃的和新模型选项放在两个不同的内存区:

__setup_start … __setup_end:
我们在后面的章节中会看到,这个区域在启动阶段结束时被释放:一旦内核启动,这些选项就不在需要,用户也不能够在运行时查看或修改它们。
__start___param … __stop___param:
这个区域不会被释放,它的内容被导入/sys文件系统,以暴露给用户。
第五章有更多关于模块参数的细节。

注意:所有作废模式选项,不管它是否有优先处理的特殊标志,都被放到__setup_start和__setup_end内存区。

7.1.3、.init.setup内存区

我们前面章节介绍的传给__setup宏的两个输入参数被放入obs_kernel_param类型的数据结构中,它在include/linux/init.h中定义:

struct obs_kernel_param {
    const char *str;
    int (*setup_func)(char*);
    int early;
};
其中,str是关键字,setup_func是处理函数,early是“两次解析”小节中介绍的两次调用中的特殊标志。
__setup_param宏把所有的obs_kernel_params实例放到专门的内存区,这样做主要有两个原因:
l当查找基于str的关键字时,通过所有的实例,系统更容易处理具体的实例。我们将明白在查找关键字时内核如何使用分别代表先前提及的内存区的开始和结束的__setup_start 和 __setup_end两个指针。
l当不再需要时,内核能够快速释放所有数据结构。我们可以在后面要讲的“内存最优化”小节看到这点。

7.1.4、用启动选项配置网络设备

按照前面章节所述,我们接下来看看网络代码怎样使用启动选项的。

我 们在“注册关键字”一节中注意到ether= 和 netdev=关键字都用同一函数netdev_boot_setup注册。当调用 netdev_boot_setup函数处理输入参数(紧随匹配的关键字后的字符串)时,函数将把处理结果存储在include/linux /netdevice.h中定义的netdev_boot_setup结构中,处理函数和结构碰巧是同名的,因此你要注意不要混淆两者:
struct netdev_boot_setup {
    char name[IFNAMSIZ];
    struct ifmap map;
};
其中,name是设备名,ifmap在in include/linux/if.h中定义,是存储输入配置的数据结构:
struct ifmap
{
    unsigned long mem_start;
    unsigned long mem_end;
    unsigned short base_addr;
    unsigned char irq;
    unsigned char dma;
    unsigned char port;
    /* 3 bytes spare */
};

同一关键字可以在启动时字符串选项中多次出现(对不同设备),如下例所示:
LILO: linux ether=5,0×260,eth0 ether=15,0×300,eth1
但是,这种机制下,能够在启动时配置的设备的最大数是NETDEV_BOOT_SETUP_MAX常量,它也是用于存储配置的静态数组dev_boot_setup的大小:
static struct netdev_boot_setup dev_boot_setup[NETDEV_BOOT_SETUP_MAX];

netdev_boot_setup相当简单:它从字符串中提取输入参数中,填充到ifmap结构中,并通过netdev_boot_setup_add函数将ifmap信息加入到dev_boot_setup数组中。
启动阶段结束时,网络代码会调用netdev_boot_setup_check函数检查给定的接口是否与启动时配置有关联,在dev_boot_setup数组中查找时基于设备名dev->name:

int netdev_boot_setup_check(struct net_device *dev)
{
    struct netdev_boot_setup *s = dev_boot_setup;
    int i;

    for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
        if (s[i].name[0] != ‘’ && s[i].name[0] != ‘ ‘ &&
            !strncmp(dev->name, s[i].name, strlen(s[i].name))) {
            dev->irq        = s[i].map.irq;
            dev->base_addr  = s[i].map.base_addr;
            dev->mem_start  = s[i].map.mem_start;
            dev->mem_end    = s[i].map.mem_end;
            return 1;
        }
    }
    return 0;
}

有些设备具有特殊容量、特征与限制,在需要额外的参数时,可以定义它自己的关键字和处理函数,这些关键字和函数紧接着ether= 和 netdev=提供的基本数据之后(PLIP设备驱动程序就是这样做的)

7.2、模块初始化代码

由于下面的章节的例子经常提及模块,所以有必要弄清楚一对初始化概念:
内 核代码要麽静态连接到主映象文件,要麽在需要时作为模块动态加载。并不是所有的内核组件都适合编译成模块,设备驱动程序和基本功能扩展是内核组件被编译为 模块的一个好的例子。你可以参考Linux Device Drivers一书了解模块利弊以及内核需要时动态加载模块而不再需要时卸载模块的原理。

每个模块都必须提供两个函数:init_module 和 cleanup_module,前者在模块加载时初始化模块,后者在内核卸载模块时被调用以释放被模块使用时分配的资源(包括内存)。

内核提供了两个宏:module_init 和 module_exit,它允许开发人员随意命名初始化和卸载函数,下面是3COM公司的3c59x网卡驱动(drivers/net/3c59x.c)中的一个例子:
module_init(vortex_init);
module_exit(vortex_cleanup);
在 “内存最优化”一节,我们将看到这两个宏如何定义以及其定义如何被内核配置项所改变。绝大多数内核使用这两个宏,但极少数模块仍然使用旧的缺省名 init_module 和 cleanup_module。在本章后续内容中,我们用module_init 和 module_exit来指代初始化 和卸载清除函数。

首先我们来看看用旧的内核模型是如何编写模块初始化代码的,然后看看基于新的宏集的内核如何工作的。

7.2.1、旧模式:条件编译代码

不管内核组件是编译成模块还是静态编译到内核中,它都需要初始化。因此,内核组件初始化代码必须依靠条件指示符告诉编译器区分这两种情况。在旧模式下,这将强迫开发者在所有这些地方使用类似#ifdef的条件编译指示符。

下面是2.2.14内核3c59x网卡驱动(drivers/net/3c59x.c)中的一段:注意#ifdef MODULE 和 #if defined (MODULE)使用了多次。


#if defined(MODULE) && LINUX_VERSION_CODE > 0x20115
MODULE_AUTHOR("Donald Becker <becker@cesdis.gsfc.nasa.gov>");
MODULE_DESCRIPTION("3Com 3c590/3c900 series Vortex/Boomerang driver");
MODULE_PARM(debug, "i");

#endif

#ifdef MODULE

int init_module(void)
{
    …
}
#else
int tc59x_probe(struct device *dev)
{
    …
}
#endif  /* not MODULE */

static int vortex_scan(struct device *dev, struct pci_id_info pci_tbl[])
{
    …
#if defined(CONFIG_PCI) || (defined(MODULE) && !defined(NO_PCI))
    …
#ifdef MODULE
    if (compaq_ioaddr) {
        vortex_probe1(0, 0, dev, compaq_ioaddr, compaq_irq,
                compaq_device_id, cards_found++);
        dev = 0;
    }
#endif

    return cards_found ? 0 : -ENODEV;
}

#ifdef MODULE
void cleanup_module(void)
{
    … … …
}
#endif

上面代码表明旧模式如何让程序员根据代码是编译成模块还是静态连接到内核映象来指定做不同的事情:
初始化代码被区别执行
代码显示cleanup_module函数仅当驱动程序被编译成模块时被定义(因此被使用)
代码块被包含或排斥在模块之外
例如:仅当驱动程序被编译成模块时vortex_scan 调用 vortex_probe1

这种模型使代码很难扩展与调试,而且在每个模块中都重复相同的逻辑。

7.2.2、新模式:基于宏的标记

现在我们来对比一下上节代码与2.6内核中相匹配的同样的文件

static char version[] _ _devinitdata = DRV_NAME " … ";

static struct vortex_chip_info {
    …
} vortex_info_tbl[] _ _devinitdata = {
    {"3c590 Vortex 10Mbps",
    … … …
}

static int _ _init vortex_init (void)
{
    …
}
static void _ _exit vortex_cleanup (void)
{
    …
}

module_init(vortex_init);
module_exit(vortex_cleanup);

你可以看到:#ifdef指示符不再需要。
为 了移除这些混乱的条件编译代码,以使代码有良好的可读性,内核开发者引入了一组模块开发者现在能够用于写更清晰初始化代码的宏(绝大多数驱动程序都是这些 宏的使用者),上述代码展示了其中的几个宏:__init, __exit, 和 __devinitdata的用法。

后面的章节将讲述这些宏如何被使用以及它们是如何工作的。

对每个模块来说,这些宏允许内核在后台决定那些代码被包含在内核映象中,那些代码因为不需要被排斥,那些代码仅仅在初始化时被执行,等等。这样就去除了每个程序员在每个模块都要复制相同的逻辑的麻烦。[*]
[*]注意,用宏并不是消除了使用条件编译指示符,内核仍然用条件指示符设置用户编译时可配置的开关选项

很明显,就像上面章节例子展示的,这些宏允许程序员替换条件编译指示符,他们必须提供如下两个服务:

l定义新的内核组件加载时需要执行的函数,要麽由于它是静态包含进内核,要麽由于它作为模块动态加载。
l在初始化函数间定义某种顺序,以便内核组件之间的相互依赖互不相关。

7.3、优化基于宏的标记

    Linux 内核使用各种各样的宏来标识函数和数据结构的特殊属性,例如:标识初始化函数。绝大多数宏都在include/linux/init.h文件中定义,这些 宏很多是用于告诉连接器把这些具有特殊属性的代码或数据结构放到特殊的、专用的内存区(或内存节section)。这样做,内核能够以一种简单的方式很容 易访问一类具有特殊属性的对象(程序或数据结构)。我们在“内存最优化”一节会看到这样的例子。
图7-3展示了一些内核内存节:

图7-3 初始化代码使用的一些内存节
 

图片展示了初始化代码所使用的部分内存区(section)示意图。左边是分隔每个区或节的开始与结束部分的指针名,右边部分是用于将数据或代码放到相关内存区的宏的名字,图片展示仅限于部分而非全部的内存区以及部分而非全部宏

表 7-1和表7-2列出了用于分别标记程序或数据的一些宏并给出了简单的描述。限于篇幅,我们不会全部说明他们,但在”xxx_initcall宏"一节中 会花一定的篇幅讲述xxx_initcall宏,在"__init and __exit 宏"一节中会花一定的篇幅讲述__init宏和 __exit 宏。
本节的目的不是描述如何创建内核映象,如何处理模块等等,而是给你一些关于模块为什么存在的一些原因,以及设备驱动程序通常如何使用它们的。

表7-1 修饰函数的宏
宏使用宏的函数说明
__init启动时初始化函数: 用于启动阶段后期不再需要的函数,这些信息在某种情况下用于移除函数
__exit和__init匹配. 相关内核组件卸载时调用,常用于module_exit所修饰的函数,这些信息在某种情况下用于移除函数
core_initcall  postcore_initcall  arch_initcall    subsys_initcall fs_initcall device_initcall late_initcall 宏的集合,用于标记启动时需要执行的初始化函数
__initcall废弃的宏,定义为device_initcall的别名
__exitcalla标识退出函数,相关内核组件卸载时调用,迄今为止,它仅用于标记module_exit程序
a __exitcall和__initcall在__exit_call和__init_call之前定义

表7-2 初始化数据结构的宏
宏使用宏的数据(结构)说明
__initdata仅在启动时用于已初始化的数据结构
__exitdata仅被由__exitcall修饰的函数使用的数据结构,也有一层意思是:如果被__exitcall修饰的函数即时不被使用,由__exitdata修饰的数据也是正确的。因此,各种优化也可以被用于__exitdata和__exitcall

在了解上面两张表中一些宏的细节之前,我们有必要强调几点:
l绝大多数宏都是相配对的:一个修饰初始化加载,则与其相配对的修饰卸载过程,例如:__exit和_ _init匹配; __exitcalls和__initcall匹配
l宏要兼顾两方面:一是当函数被执行时(如:__initcall, __exitcall);另一面就是函数或数据放置的内存区。
l同样的函数可以被多个宏标记。例如:下面的代码表明:pci_proc_init可以在启动时运行(__initcall),一旦运行后就可以被释放(__init)

static int __init pci_proc_init(void)
{

}

__initcall(pci_proc_init)

7.3.1、设备初始化函数宏

表 7-3列出了用于标记函数的一些普通的宏,它们被设备驱动程序用于初始化设备,并且在内核不支持热拔插时可以使内存最优化。在第六章“网卡驱动程序注册实例”一节中你可以看到这样使用的例子,在后面“其它优化”一节,你可以看到表7-3的宏使内核优化变得容易。

表 7-3 设备初始化函数宏
名称描述
__devinit用于标记初始化设备的函数,例如,对于PCI驱动程序,用于初始化的函数pci_driver->probe就是用此宏标识的。被其它由_devinit标记的函数调用的函数通常也由_devinit标记。
__devexit用于标记设备卸载时被调用的函数。
__devexit_p用于初始化由__devexit 标记的函数的指针。如果内核既支持模块也支持热拔插,则__devexit_p(fn)返回fn,否则返回NULL。可以参考“其它优化”一节
__devinitdata用于标记函数使用的已初始化的数据,而这些函数兼顾设备初始化(如被_devinit标记),因此共享其属性。
__devexitdata与__devinitdata类似但与__devexit关联匹配.

7.4、启动时初始化代码

绝大多数初始化代码有两个有趣的特点:
l启动时,当所有内核组件初始化后,它们必须被执行
l一旦执行之后就不需要它了。
下一小节“xxx_initcall宏”描述了启动时运行初始化函数的原理,并考虑到模块之间的属性与优先权。后面一节”内存最优化”展示了不再需要的函数和数据是如何在连接或运行时通过巧妙的标记而释放的。

7.4.1、xxx_initcall宏

内核启动阶段前期,要考虑两个主要的初始化块:
l各种关键的、必不可少的子系统的初始化需要以特殊的顺序执行,例如:内核在初始化PCI层之前不能够初始化PCI设备。后面“初始化程序相互依赖的例子”小节中有各例子说明。
l其它一些不必按照严格顺序的内核组件的初始化:相同优先级的函数可以以任意顺序执行。

第 一个初始化块可由来自第五章图5-1的do_initcalls函数代码验证,第二个初始化块可由同一章中调用do_initcalls的函数 do_basic_setup的结尾处来验证,第二部分的初始化函数是基于其角色以及优先权来分类,内核从放在高优先级节(core_initcall) 的函数开始逐个执行这些初始化函数,这些需要被调用的函数的地址放在图7-3中由xxx_initcall宏标记的.initcallN.init内存节 中。这个区域用于存储由xxx_initcall标记的函数地址,并由开始地址(__initcall_start)和结束地址 (__initcall_end)分隔。在下面摘录的do_initcalls函数代码中,你会看到,很容易从这个区域轻松取得函数地址并执行其指向的函 数:

static void _ _init do_initcalls(void)
{
        initcall_t *call;
        int count = preempt_count( );

        for (call = _ _initcall_start; call < _ _initcall_end; call++) {
            … … …
            (*call)( );
            … … …
        }
        flush_scheduled_work( );
}

由 do_initcalls调用的函数不应该改变其优先权状态和禁止IRQs。因此,每个函数执行后,do_initcalls会检查函数是否做了任何变 化,如果有必要,它会校正优先权和IRQ状态。对于xxx_initcall函数,确定其后发生的工作也是可能的,这意味着由这些函数处理的任务有可能在 未知时间异步中止,flush_scheduled_work函数调用用于确保do_initcalls在返回前等待这些异步任务结束。

注意:do_initcalls自己用__init标记:因为它仅在启动阶段被do_basic_setup调用一次,内核之后一旦调用就会丢弃它。
__exitcall与__initcall相对。它们并不常用,相当多的是由其它宏作为其别名,如:module_exit,它在“模块初始化代码”一节中介绍

7.4.1.1、__initcall和__exitcall程序例子:模块

我们曾说过,在“模块初始化代码”一节中,module_init和module_exit宏分别用于标记模块初始化(若编译进内核则在启动时,若是单独加载则是在运行时)或卸载时要被执行的函数。

这使得对于__initcall和__exitcall宏来说,模块是非常完美的选择。正如我们说得,下面的代码来自于include/linux/init.h文件,其中关于module_init 和 module_exit宏的定义就来得很自然了(不令人惊奇):
#ifndef MODULE
… … …
#define module_init(x)    __initcall(x);
#define module_exit(x)    __exitcall(x);

#else
… … …
#endif

对于静态连接进内核的代码来说,module_init是__initcall的别名,它的输入函数被归到启动时初始化函数一类。
module_exit与此一致:当代码被编译进内核,module_exit变成了卸载函数,同时,卸载函数在系统关闭时不会被调用,但是代码允许这样放置.[*]
[*]用户模式下的linux是实际使用卸载函数的仅有的体系结构。它并不用__exitcall宏,而是定义它自己的宏,__uml_exitcall,用户模式linux项目的主页是::URL::http://user-mode-linux.sourceforge.net

7.4.1.2、初始化程序相互依赖的例子

第 五章介绍了net_dev_init,设备驱动程序在内核中用module_init函数注册,正如第六章描述的,由网络代码注册设备。内置到驱动程序的 net_dev_init和各种module_init标记的函数启动时由do_initcalls调用。因此,内核必须确保在net_dev_init 执行之前没有设备注册发生,这显然是强制的,因为设备驱动程序初始化函数被device_initcall宏(或别名__initcall)标记,而 net_dev_init被subsys_initcall宏标记。在图7-3中,你可以看到subsys_initcall函数比 device_initcall更早执行(其内存节以优先的顺序分类)。

7.4.1.3、遗留下来的代码
在引入 xxx_initcall宏集前,仅有一个标记初始化函数的宏:__initcall。这个单个的宏的使用带来严重的局限性:宏修饰的函数不能强制限定执 行顺序,在很多情况下,这个局限性由于模块内部独立或其它一些考虑变得不可接收。所以使用__initcall对所有的初始化函数没有扩展性。
__initcall主要被设备驱动程序使用。为了使一些尚未升级到新模式的代码向后兼容,它仍然存在而且被简单的定义为device_initcall的别名。
还有一个局限性经常在当前模型中出现,就是没有参数提供给初始化函数,但是这似乎不是很严重的局限。

7.5、内存最优化
不像用户空间代码和数据,内核的代码和数据永远保留在主内存中。因此在各种可能的方面减少内存的浪费变的很重要了。初始化代码对于内存优化是个很好的选择。绝大多数初始化函数要麽只执行一次,要麽根本就不执行,这视内核配置而定。例如:
lmodule_init标记的函数仅当关联模块加载时被执行一次,若模块是静态包含到内核的,则内核启动过程中,在模块运行后,内核能够完全释放module_init程序。
l当模块是静态包含到内核的时,module_exit修饰的函数决不会执行。因此,在这种情况下,没有必要在内核映象中包含此模块函数(也就是说,这个函数在连接时就可以丢弃)。
第一种情况是运行时优化,第二种是连接时优化。
启动时使用随后不需要的代码和数据放在图7-3所示的某个内存节,一旦内核完成了初始化过程,它会丢弃整个内存区,这靠调用第五章图5-1所示的free_init_mem 函数完成。不同的宏用于把代码放到图7-3所示的不同的内存区。
如果你看了前面的“新模式:基于宏的标记”一节,你会明白这两个module_init 和 module_exit函数通常是被__init 和__exit分别标记:利用本章开始所提及的两个属性正是这样做的。

7.5.1、__init和__exit宏

在内核前期阶段执行的初始化函数由__init标记
象前面小节提及的,绝大多数module_init输入函数用这个宏标记,例如:第五章图5-1(在调用free_initmem之前)的绝大多数函数用__init标记,正如其定义的那样,__init宏把输入函数放进.text.init内存节:
#define __init    __attribute__ ((__section__ (".text.init")))

这个节是运行时由free_initmem函数释放的内存区之一。
__exit 与__init相对,用于卸载的函数放在.text.exit节。对于直接编译进内核的模块而言,这个节在连接时就被丢弃。但是,有少数体系结构在运行时 处理交叉引用时会丢弃它。注意,对于单独加载的模块,若内核不支持模块卸载,同样的内存节在加载时就可以被移除(有个内核选项阻止用户卸载模块)。

7.5.2、xxx_initcall和__exitcall节
内核存放地址给由xxx_initcall 和 __exitcall宏标记的函数的内存节可以被丢弃:
l图7-3所示的xxx_initcall节在运行时由free_initmem丢弃
l.text.exit节用于__exitcall标记的函数,在连接时被丢弃,因为内核在系统关机时不会马上调用__exitcall函数(也就是说,它不会采用类似do_initcalls的机制).

7.5.3、其它优化
表7-3包含了其它优化的例子
__devinit 
 当内核在编译时不支持热拔插,则由__devinit修饰的函数在启动阶段结束时不再需要了(所有设备已被初始化)。因此,当不支持热拔插时__devinit变成了__init的别名。
__devexit  
当PCI驱动程序被编译进内核且不支持热拔插时,pci_driver->remove所指的函数被初始化,且由__devexit标记的函数因为不需要而被丢弃。当模块被加载到不支持模块卸载的内核中时,函数也被丢弃。
__devinitdata  
当 不支持热拔插时,数据也只在启动时需要。通常,在设备初始化时,设备驱动程序也用这个宏标记pci_driver->probe函数搜索到的字符 串。例如:PCI设备驱动程序用__devinitdata标记pci_device_id表:一旦系统启动结束且不支持热拔插,内核将不在需要这个表。
本节仅给出了一些丢弃代码的例子,你也可以阅读源代码以了解更多。

7.5.4、动态宏定义
前 面一节介绍了几个宏,如__init和几个版本的xxx_initcall,我们也看到了module_init宏修饰的函数被__initcall宏标 记。由于绝大多数内核组件要麽作为模块编译,要麽静态连接到内核,所以很多前面章节介绍的方法可供选择,以改变这些宏定义并用于内存优化。

特别说明,我们在include/linux/init.h中看到的表7-1宏的定义,它根据下面的符号是否在包含include/linux/init.h的文件的作用域范围而变化。

CONFIG_MODULE
当内核支持可加载模块时定义(可加载模块配置选项)
MODULE
当文件所属的内核组件作为模块编译时
CONFIG_HOTPLUG
当内核支持热拔插选项编译时定义("General setup"设置中的选项)

MODULE对不同的文件中有不同的值,而另外两个宏则具有内核作用域属性,因此它们在内核全局范围内要麽一直被设置,要麽不设置。
表 7-1和表7-2所示的宏中,我们最感兴趣的是下面与网卡驱动初始化相关的几个:__init, __exit, __initcall和 __exitcall。概括一下迄今为止我们讨论的内容:基于符号MODULE和 CONFIG_HOTPLUG是否被定义(我们假设内核支持可加载模 块,也就是说,CONFIG_MODULE被定义),图7-4展示了在节约内存方面前面列出的几个宏的效果。如下图所看到的,内核不支持模块动态加载和热 拔插与内核支持所有选项相比较有很多情况:你会有更多的限制,你可以获得更好的优化。

图7-4   表 7-1中的宏的效果

 

我们逐个看看图7-4中的1~6点的含义,谨记前面“新模式:基于宏的标记”小节中看到的设备驱动程序通用结构和前面“内存最优化”一节中看到的__initcall和__exitcall定义。

下面是把模块编译为内核的一部分时可以采用的优化:
1、module_exit函数从不被使用。因此由__exit标记它们,程序员要确保它们在连接时不会在内核映象中包含它们。
2、module_init函数仅在系统启动是执行一次,因此由__init标记它们,一旦它们执行了程序员就可以丢弃它们。
3、module_init(fn)是__initcall(fn)的别名,它们能够确保fn被do_initcalls被执行,这在“xxx_initcall宏”一节中可以看到
4、module_exit(fn)是__exitcall(fn)的别名,它们把输入函数的地址放在.exitcall.exit内存节,这使得内核在卸载时可以方便的执行fn函数。实际上,在连接时这个节就被丢弃了。
我们用PCI驱动来验证一下,看看需要热拔插支持的哪些优化被引入。这涉及到pci_driver->remove函数,它在模块卸载时调用,每个模块注册的设备驱动都调用一次。
5、不管MODULE是否被定义,当内核不支持热拔插时,设备不能够在系统运行时被移除。因此,remove函数决不会被PCI层调用,并被初始化为空指针(NULL).这由__devexit_p宏标记。
6、当内核不支持热拔插和模块时,模块不需要初始化pci_driver->remove函数的驱动程序,这是由__devexit宏标记的。注意,当模块支持模块时这是不正确的,因为用户允许加载或卸载模块,所以内核必须要remove函数。

注意:第五点是第六点的结果:如果你在内核没有包含初始化函数,你就不能够引用它(也就是说,你不能够为函数初始化指针)。

7.6、用/proc文件系统配置参数
本章没有与/proc系统有关内容
7.7、本节涉及的函数和变量

表7-4概括了本章介绍的函数、宏、数据结构和变量

表7-4  本章介绍的函数、宏、数据结构和变量
名称描述
函数和宏 
__init,__exit,__initcall,__exitcall,__initdata,__exitdata,__devinit,__devexit,__devexit_p,_devinitdata,__devexitdata,xxx_initcall 用于标记具有特殊要求的函数,这些宏标记可以优化内核映象大小,例如,移除不需要的代码.
do_initcalls启动时执行所有由xxx_initcall 宏标记的函数
init_module, cleanup_module, module_init, module_exit前两个宏是每个模块都要提供的用于单独初始化或卸载模块的函数名.另外两个是允许设备驱动程序编写人随意使用命名初始化或卸载模块的函数名。
netdev_boot, setup_check,neTDev_boot_setup_add应用于特殊设备启动时配置
module_param定义加载模块时提供的可选模块参数
数据结构 
kernel_param存储module_param 宏的入参
obs_kernel_param存储__setup宏的入参
netdev_boot_setup, ifmapnetdev_boot_setup为ether=和netdev=存储启动时参数,ifmap 是结构netdev_boot_setup的一个域.
变量 
dev_boot_setupnetdev_boot_setup结构数组
NETDEV_BOOT_SETUP_MAXdev_boot_setup数组大小

7.8、本节涉及的文件和目录
图7-5列出了本章参考的文件和目录

图7-5 本章参考的文件和目录
 

顺序和屏障

当处理多处理器之间或硬件设备之间的同步问题时,有时需要在程序代码中以指定的顺序发出读内存(读入)和写内存(存储)指令。在和硬件交互时,时常 需要确保一个给定的读操作发生在其他读或写操作之前。另外,在多处理上,可能需要按写数据时的顺序读数据(通常确保后来以同样的顺序进行读取)。但是编译 器和处理器为了提高效率,可能对读和写重新排序。   

所有可能重新排序和写的处理器提供了及其指令来确保顺序要求。同样也可以指示编译器不要对给定的点周围的指令进行重新排序,这些确保顺序的指令称为屏障(barrier)。

编译器会在编译时按代码的顺序编译,这种顺序是静态的。但是处理器会重新动态排序,因为处理器在执行指令期间,会在取值和分派时,把表面上看似无关的指令按自认为最好的顺序排列。这种重排序的发生是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序。

  不管是编译器还是处理器都不知道其他上下文中的相关代码。偶然情况下,有必要让写操作被其他代码识别,也让我们所期望的指定顺序之外的代码识别。这种情况常常发生在硬件设备上,但那是在多处理器机器上也很常见。

 

rmb()方法提供了一个“读”内存屏障,它确保跨越rmb()的载入动作不会发生重排序。在rmb()之前的载入操作不会被重新排在该调用之后;在rmb()之后的载入操作不会被重新排列在该调用之前。

  1. 在<System.h(includeasm-i386)>中
  2. #define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2) 

 

  1. 在<Alternative.h(includeasm-i386)>中
  2. /*
  3.  * Alternative instructions for different CPU types or capabilities.
  4.  *
  5.  * This allows to use optimized instructions even on generic binary
  6.  * kernels.
  7.  *
  8.  * length of oldinstr must be longer or equal the length of newinstr
  9.  * It can be padded with nops as needed.
  10.  *
  11.  * For non barrier like inlines please define new variants
  12.  * without volatile and memory clobber.
  13.  */
  14. #define alternative(oldinstr, newinstr, feature)    
  15.     asm volatile ("661:nt" oldinstr "n662:n"             
  16.               ".section .altinstructions,"a"n"            
  17.               "  .align 8n"                       
  18.               "  .quad 661bn"            /* label */          
  19.               "  .quad 663fn"        /* new instruction */ 
  20.               "  .byte %c0n"             /* feature bit */    
  21.               "  .byte 662b-661bn"       /* sourcelen */      
  22.               "  .byte 664f-663fn"       /* replacementlen */ 
  23.               ".previousn"                 
  24.               ".section .altinstr_replacement,"ax"n"     
  25.               "663:nt" newinstr "n664:n"   /* replacement */ 
  26.               ".previous" :: "i" (feature) : "memory")

wmb()方法提供了一个“写”内存屏障。该函数与rmb()类型,区别仅仅是它是针对存储而非载入。它确保跨越屏障的存储不发生重新排序。如果一个体系结构不执行打乱存储(比如Intel x86芯片就不会),那么wmb()就什么也补做。

 

  1. /*
  2.  * Force strict CPU ordering.
  3.  * And yes, this is required on UP too when we’re talking
  4.  * to devices.
  5.  *
  6.  * For now, "wmb()" doesn’t actually do anything, as all
  7.  * Intel CPU’s follow what Intel calls a *Processor Order*,
  8.  * in which all writes are seen in the program order even
  9.  * outside the CPU.
  10.  *
  11.  * I expect future Intel CPU’s to have a weaker ordering,
  12.  * but I’d also expect them to finally get their act together
  13.  * and add some real memory barriers if so.
  14.  *
  15.  * Some non intel clones support out of order store. wmb() ceases to be a
  16.  * nop for these.
  17.  */

 

  1. #ifdef CONFIG_X86_OOSTORE
  2. /* Actually there are no OOO store capable CPUs for now that do SSE, 
  3.    but make it already an possibility. */
  4. #define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
  5. #else
  6. #define wmb()   __asm__ __volatile__ ("": : :"memory")
  7. #endif

rmb()和wmb()方法相当于指令,它们告诉处理器在继续执行前提交所有尚未处理的载入或存储指令。

 

mb()方法既提供了读屏障也提供了写屏障。载入和存储动作都不会跨越屏障重新排序。这是因为一条单独的指令(通常和rmb()使用同一个指令)既可以提供载入屏障,也可以提供存储屏障。

 

  1. /* 
  2.  * Actually only lfence would be needed for mb() because all stores done 
  3.  * by the kernel should be already ordered. But keep a full barrier for now. 
  4.  */
  5.  
  6. #define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)

read_barrier_depends()是rmb()的变种,它提供一个读屏障,但是仅仅是针对后续读操 作锁依赖的那些载入。因为屏障后的读操作依赖于屏障前的读操作,因此,该屏障确保屏障前的读操作在屏障后的读操作之前完成。基本上说,该函数设置一个读屏 障,如rmb(),但是只真对特定的读—也就是那些相互依赖的读操作。

 

  1. /**
  2.  * read_barrier_depends – Flush all pending reads that subsequents reads
  3.  * depend on.
  4.  *
  5.  * No data-dependent reads from memory-like regions are ever reordered
  6.  * over this barrier.  All reads preceding this primitive are guaranteed
  7.  * to access memory (but not necessarily other CPUs’ caches) before any
  8.  * reads following this primitive that depend on the data return by
  9.  * any of the preceding reads.  This primitive is much lighter weight than
  10.  * rmb() on most CPUs, and is never heavier weight than is
  11.  * rmb().
  12.  *
  13.  * These ordering constraints are respected by both the local CPU
  14.  * and the compiler.
  15.  *
  16.  * Ordering is not guaranteed by anything other than these primitives,
  17.  * not even by data dependencies.  See the documentation for
  18.  * memory_barrier() for examples and URLs to more information.
  19.  *
  20.  * For example, the following code would force ordering (the initial
  21.  * value of "a" is zero, "b" is one, and "p" is "&a"):
  22.  *
  23.  * <programlisting>
  24.  *  CPU 0               CPU 1
  25.  *
  26.  *  b = 2;
  27.  *  memory_barrier();
  28.  *  p = &b;             q = p;
  29.  *                  read_barrier_depends();
  30.  *                  d = *q;
  31.  * </programlisting>
  32.  *
  33.  * because the read of "*q" depends on the read of "p" and these
  34.  * two reads are separated by a read_barrier_depends().  However,
  35.  * the following code, with the same initial values for "a" and "b":
  36.  *
  37.  * <programlisting>
  38.  *  CPU 0               CPU 1
  39.  *
  40.  *  a = 2;
  41.  *  memory_barrier();
  42.  *  b = 3;              y = b;
  43.  *                  read_barrier_depends();
  44.  *                  x = a;
  45.  * </programlisting>
  46.  *
  47.  * does not enforce ordering, since there is no data dependency between
  48.  * the read of "a" and the read of "b".  Therefore, on some CPUs, such
  49.  * as Alpha, "y" could be set to 3 and "x" to 0.  Use rmb()
  50.  * in cases like this where there are no data dependencies.
  51.  **/
  52.  
  53. #define read_barrier_depends()  do { } while(0)

第2个<programlisting>主要是为了说明read_barrier_depends()用于数据依赖的读操作,由于y和 x不是数据依赖的,因为没有成功设置读屏障,导致x=a在y=b之前运行,于是a可能还是0的时候就被赋给x了。此时,对于没有数据依赖的读操作应该使用 rmb()来提供读屏障。

宏smp_rmb()、smp_wbm()、smp_mb()和smp_read_barrier_depends()提供了一个有用的优化。在SMP 内核中,它们被定义成常用的内存屏障,而在单处理器内核中,它们被定义成编译器的屏障

 

  1. #ifdef CONFIG_SMP    //在SMP 内核中,它们被定义成常用的内存屏障
  2. #define smp_mb()    mb()
  3. #define smp_rmb()   rmb()
  4. #define smp_wmb()   wmb()
  5. #define smp_read_barrier_depends()  read_barrier_depends()
  6. #define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
  7. #else    //在单处理器内核中,它们被定义成编译器的屏障
  8. #define smp_mb()    barrier()
  9. #define smp_rmb()   barrier()
  10. #define smp_wmb()   barrier()
  11. #define smp_read_barrier_depends()  do { } while(0)
  12. #define set_mb(var, value) do { var = value; barrier(); } while (0)
  13. #endif

barrier()方法可以防止编译器跨越屏障对载入或存储操作进行优化。编译器不会重新组织存储或载入操作而防止改变C代码的效果和现有数据的依赖关系。但是,它不知道当前上下文之外会发生什么事。前面讨论的内存屏障可以完成编译器屏障的功能,但是后者比前者轻量得多。实际上,编译器屏障机会是空闲的,因为它只是防止编译器可能重排指令。

  1. 在<Compiler.h(includelinux)>中
  2. /* Optimization barrier */
  3. #ifndef barrier
  4. # define barrier() __memory_barrier()
  5. #endif
  6.  

 

为最坏的情况(即排序能力最弱的处理器)使用恰当的内存屏蔽,这样代码才能在编译时执行针对体系结构的优化。

网页设计中的默认字体及其理由

打算将淘宝的默认字体调整为:

font: 12px/1.5 Tahoma, Helvetica, Arial, sans-serif;

测试页面:default_fonts_test.html

理由如下:

1)font-size采用px, 不采用em. 理由是em带来的可用性提升很有限,同时基于em很难保证视觉设计的精确性,并且现在主流浏览器都支持“全页面缩放(Full Page Zoom)”功能,是时候不用考虑基于字体大小(em)的设计了。

2)line-height采用1.5, 也就是18px. 这是淘宝视觉规范中定义的行高,对于12px字体,这个行高看起来很舒服。

3)font-family默认采用Tahoma. Tahoma是英文Windows操作系统的默认字体,这个字体比较均衡,显示中英文混排很不错,是经久耐看的一款字体。

4)Tahoma是Matthew Carter为微软公司设计的一款字体,从Mac OS X 10.5开始,默认也捆绑了这款字体。但在Mac OS X 10.4及其之前的系统中,没有Tahoma字体,但有一款看起来比Tahoma更典雅的系统默认字体:Helvetica. 不信你玩玩这款游戏:Helvetica vs. Arial. 个人情感上,我甚至想把Helvetica放在首位,但考虑到非Mac系统的Helvetica字体都是rip版,以及大部分淘宝用户用的是 Windows操作系统,因此只能委屈Helvetica, 放在第二位。

5)Arial是早期Windows英文系统的默认字体,XP和Vista上都是Tahoma了。Arial几乎在所有操作系统中都有,因此放在最后,作为一道屏障。

7)最后的sans-serif是针对强悍的Linux DIY族。Linux默认只有kernel, 字体完全由用户自定义,针对这部分用户,sans-serif可能能派上用场。

最后,无论在XP还是Vista下,不指定网页的中文字体时,默认就是宋体。因此font-family里的’宋体’是多余的,可以省 去。(Opera和Safari不认识SimSun, 用到宋体的地方,还是用’宋体’更保险)(待测试验证:不指定中文字体时,网页的默认中文字体是什么?Linux是自己配的,Mac OS X呢?还有Vista中文系统。这些环境下,哪位朋友帮忙测试下网页的默认中文字体?)

8)用Tahoma字体,在中英文混排时,也有个不好的地方:

两个数字30所占的宽度,大于一个宋体汉字,导致上图中对不齐。但这仅是设计上的小不妥。调整为以冒号为基线,都向右对齐就可以避免了。

长肥管道

带宽时延乘积 bandwidth*delay product

capacity(b) = bandwidth (b/s) x round-trip time (s)

带宽时延乘积用于表示通道的容量;

长肥网络 long fat network

具有大的带宽时延乘积的网络称为长肥网络(Long Fat Network,即LFN,发音为“elefan(t)s”);

长肥管道 long fat pipe

运行于LFN上的TCP连接称为长肥管道。

长肥管道中面临的问题:

1)LFN需要更大的窗口来提供最大的吞吐量。

TCP首部中窗口大小为16bit,即窗口大小只能在65535个字节以内。
>> RFC 1323 中使用窗口扩大选项(Windows Scale Option)来解决这个问题。

2)LFN内的分组丢失会使吞吐量急剧减少。

在一个窗口内发生如果只有一个报文丢失,使用快速重传和快速恢复算法可以使管道避免耗尽;
但若有多个分组丢失,管道同样会耗尽(why?)。
>> RFC 1072 中建议使用有选择的确认(SACK)来处理在一个窗口发生的多个分组丢失。但该功能在RFC 1323 中被忽略了,因为作者觉得吧它们纳入TCP之前要先解决一些技术上的问题。

3)LFN上需要更好的RTT测量机制。

>>RFC 1323中引入时间戳选项。

4)LFN存在序号回绕问题。

在带宽增加的情况下,网络足够快,使得不到一个MSL的时候序号就发生了回绕。
>>RFC 1323中使用TCP的时间戳选项的PAWS(Protect Against Wrapped Sequence numbers)算法来解决该问题。
 

TCP窗口扩大选项(TCP Window Scale Option)

TCP窗口扩大选项 TCP Window Scale Option (WSopt)

窗口扩大选项用于扩大TCP通告窗口,使TCP的窗口定义从16bit增加为32bit。

1、RFC 1323中关于窗口扩大选项的图示如下:

      TCP Window Scale Option (WSopt):
         Kind: 3 Length: 3 bytes
                +———+———+———+
                | Kind=3  |Length=3 |shift.cnt|
                +———+———+———+

2、要启用窗口扩大选项,通讯双方必须在各自的SYN报文中发送这个选项。主动建立连接的一方在SYN报文中发送这个选项;而被动建立连接的一方只有在收到带窗口扩大选项的SYN报文之后才能发送这个选项。

3、这个选项只在一个SYN报文中有意义(<SYN>或<SYN,ACK>),包含窗口扩大选项的报文如果没有SYN位,则会被忽略掉。当连接建立起来后,在每个方向的扩大因子是固定的。注意:在SYN报文本身的窗口字段始终不做任何的扩大(The Window field in a SYN (i.e., a <SYN> or <SYN,ACK>) segment itself is never scaled.)。

4、在启用窗口扩大选项的情况下,若发送一个窗口通告,要将实际窗口大小右移shift.cnt位,然后赋给TCP首部中的16bit窗口值;而当接收到一个窗口通告时,则将TCP首部中的16bit窗口值左移shift.cnt位,以获得实际的通告窗口大小。

5、shift.cnt取值范围为0~14,即最大TCP序号限定为2^16 * 2^ 14  = 2^30 < 2^31。该限制用于防止字节序列号溢出。

Linux网络子系统sk_buffer详细分析

作者: 小马哥 rstevens (rstevens2008@hotmail.com)
欢迎转载,未经允许,请勿用于商业目的

1.   定义

Packet:通过网卡收发的报文,包括链路层、网络层、传输层的协议头和携带的数据
Data Buffer:用于存储 packet 的内存空间
SKB:struct sk_buffer 的简写

2.   概述

struct sk_buffer 是 Linux TCP/IP stack 中用于管理Data Buffer的结构。sk_buffer在数据包的发送和接收中起着重要的作用。

为了提高网络处理的性能,应尽量避免数据包的拷贝。Linux 内核开发者们在设计 sk_buffer 结构的时候,充分考虑到这一点。目前Linux 协议栈在接收数据的时候,需要拷贝两次:数据包进入网卡驱动后拷贝一次,从内核空间递交给用户空间的应用时再拷贝一次。

sk_buffer结构随着内核版本的升级,也一直在改进。

学习和理解 sk_buffer 结构,不仅有助于更好的理解内核代码,而且也可以从中学到一些设计技巧。

 3.   sk_buffer 定义

struct sk_buff {

                struct sk_buff                     *next;

                struct sk_buff                     *prev;

                struct sock                          *sk;

                struct skb_timeval             tstamp;

                struct net_device         *dev;

                struct net_device         *input_dev;

 

                union {

                                struct tcphdr       *th;

                                struct udphdr      *uh;

                                struct icmphdr    *icmph;

                                struct igmphdr    *igmph;

                                struct iphdr          *ipiph;

                                struct ipv6hdr      *ipv6h;

                                unsigned char     *raw;

                } h;

                union {

                                struct iphdr          *iph;

                                struct ipv6hdr      *ipv6h;

                                struct arphdr       *arph;

                                unsigned char     *raw;

                } nh;

                union {

                                unsigned char     *raw;

                } mac;

 

                struct  dst_entry                 *dst;

                struct     sec_path              *sp;

                char                                       cb[40];

 

                unsigned int                         len,

                                                                data_len,

                                                                mac_len,

                                                                csum;

                __u32                                    priority;

 

                __u8                                       local_df:1,

                                                                cloned:1,

                                                                ip_summed:2,

                                                                nohdr:1,

                                                                nfctinfo:3;

                __u8                                       pkt_type:3,

                                                                fclone:2;

                __be16                                  protocol;

                void                                       (*destructor)(struct sk_buff *skb);

 

                /* These elements must be at the end, see alloc_skb() for details.  */

                unsigned int                         truesize;

                atomic_t                               users;

                unsigned char                     *head,

                                                *data,

                                                *tail,

                                                *end;

};

 
4.   成员变量

 · struct skb_timeval    tstamp;

此变量用于记录 packet的到达时间或发送时间。由于计算时间有一定开销,因此只在必要时才使用此变量。需要记录时间时,调用net_enable_timestamp(),不需要时,调用net_disable_timestamp() 。

tstamp 主要用于包过滤,也用于实现一些特定的 socket 选项,一些 netfilter 的模块也要用到这个域。

· struct net_device      *dev;

· struct net_device      *input_dev; 

这几个变量都用于跟踪与 packet相关的 device。由于 packet 在接收的过程中,可能会经过多个 virtual driver 处理,因此需要几个变量。

接收数据包的时候, dev 和 input_dev 都指向最初的 interface,此后,如果需要被 virtual driver 处理,那么 dev 会发生变化,而 input_dev 始终不变。 

(These three members help keep track of the devices assosciated with a packet. The reason we have three different device pointers is that the main ‘skb->dev’ member can change as we encapsulate and decapsulate via a virtual device.

So if we are receiving a packet from a device which is part of a bonding device instance, initially ‘skb->dev’ will be set to point the real underlying bonding slave. When the packet enters the networking (via ‘netif_receive_skb()’) we save ‘skb->dev’ away in ‘skb->real_dev’ and update ‘skb->dev’ to point to the bonding device.

Likewise, the physical device receiving a packet always records itself in ‘skb->input_dev’. In this way, no matter how many layers of virtual devices end up being decapsulated, ‘skb->input_dev’ can always be used to find the top-level device that actually received this packet from the network. ) 

· char                               cb[40];

此数组作为 SKB 的控制块,具体的协议可用它来做一些私有用途,例如 TCP 用这个控制块保存序列号和重传状态。 

 · unsigned int               len,

·                                              data_len,

·                                              mac_len,

·                                                   csum;

 ‘len’ 表示此 SKB 管理的 Data Buffer 中数据的总长度;

通常,Data Buffer 只是一个简单的线性 buffer,这时候 len 就是线性 buffer 中的数据长度;

但在有 ‘paged data’ 情况下, Data Buffer 不仅包括第一个线性 buffer ,还包括多个 page buffer;这种情况下, ‘data_len’ 指的是 page buffer 中数据的长度,’len’ 指的是线性 buffer 加上 page buffer 的长度;len – data_len 就是线性 buffer 的长度。

 ‘mac_len’ 指 MAC 头的长度。目前,它只在 IPSec 解封装的时候被使用。将来可能从 SKB 结构中去掉。

‘csum’ 保存 packet 的校验和。

(Finally, ‘csum’ holds the checksum of the packet. When building send packets, we copy the data in from userspace and calculate the 16-bit two’s complement sum in parallel for performance. This sum is accumulated in ‘skb->csum’. This helps us compute the final checksum stored in the protocol packet header checksum field. This field can end up being ignored if, for example, the device will checksum the packet for us.

On input, the ‘csum’ field can be used to store a checksum calculated by the device. If the device indicates ‘CHECKSUM_HW’ in the SKB ‘ip_summed’ field, this means that ‘csum’ is the two’s complement checksum of the entire packet data area starting at ‘skb->data’. This is generic enough such that both IPV4 and IPV6 checksum offloading can be supported. )

 ·  __u32                            priority;

 “priority”用于实现 QoS,它的值可能取之于 IPv4 头中的 TOS 域。Traffic Control 模块需要根据这个域来对 packet 进行分类,以决定调度策略。

 · __u8                              local_df:1,

·                                           cloned:1,

·                                          ip_summed:2,

·                                           nohdr:1,

·                                           nfctinfo:3;

 为了能迅速的引用一个 SKB 的数据,

当 clone 一个已存在的 SKB 时,会产生一个新的 SKB,但是这个 SKB 会共享已有 SKB 的数据区。

当一个 SKB 被 clone 后,原来的 SKB 和新的 SKB 结构中,”cloned” 都要被设置为1。

 (The ‘local_df’ field is used by the IPV4 protocol, and when set allows us to locally fragment frames which have already been fragmented. This situation can arise, for example, with IPSEC.

The ‘nohdr’ field is used in the support of TCP Segmentation Offload (‘TSO’ for short). Most devices supporting this feature need to make some minor modifications to the TCP and IP headers of an outgoing packet to get it in the right form for the hardware to process. We do not want these modifications to be seen by packet sniffers and the like. So we use this ‘nohdr’ field and a special bit in the data area reference count to keep track of whether the device needs to replace the data area before making the packet header modifications.

The type of the packet (basically, who is it for), is stored in the ‘pkt_type’ field. It takes on one of the ‘PACKET_*’ values defined in the ‘linux/if_packet.h’ header file. For example, when an incoming ethernet frame is to a destination MAC address matching the MAC address of the ethernet device it arrived on, this field will be set to ‘PACKET_HOST’. When a broadcast frame is received, it will be set to ‘PACKET_BROADCAST’. And likewise when a multicast packet is received it will be set to ‘PACKET_MULTICAST’.

The ‘ip_summed’ field describes what kind of checksumming assistence the card has provided for a receive packet. It takes on one of three values: ‘CHECKSUM_NONE’ if the card provided no checksum assistence, ‘CHECKSUM_HW’ if the two’s complement checksum over the entire packet has been provides in ‘skb->csum’, and ‘CHECKSUM_UNNECESSARY’ if it is not necessary to verify the checksum of this packet. The latter usually occurs when the packet is received over the loopback device. ‘CHECKSUM_UNNECESSARY’ can also be used when the device only provides a ‘checksum OK’ indication for receive packet checksum offload. )

 ·  void                               (*destructor)(struct sk_buff *skb);

·  unsigned int               truesize;

 一个 SKB 所消耗的内存包括 SKB 本身和 data buffer。

truesize 就是 data buffer 的空间加上 SKB 的大小。

struct sock 结构中,有两个域,用于统计用于发送的内存空间和用于接收的内存空间,它们是:

rmem_alloc

wmem_alloc

 另外两个域则统计接收到的数据包的总大小和发送的数据包的总大小。

rcvbuf

sndbuf

 rmem_alloc 和 rcvbuf,wmem_alloc 和sndbuf 用于不同的目的。

 当我们收到一个数据包后,需要统计这个 socket 总共消耗的内存,这是通过skb_set_owner_r() 来做的。 

static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)

{

        skb->sk = sk;

        skb->destructor = sock_rfree;

        atomic_add(skb->truesize, &sk->sk_rmem_alloc);

}

 最后,当释放一个 SKB 后,需要调用 skb->destruction() 来减少rmem_alloc 的值。

 同样,在发送一个 SKB 的时候,需要调用skb_set_owner_w() ,

 static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)

{

        sock_hold(sk);

        skb->sk = sk;

        skb->destructor = sock_wfree;

        atomic_add(skb->truesize, &sk->sk_wmem_alloc);

}

 在释放这样的一个 SKB 的时候,需要调用 sock_free()

 void sock_wfree(struct sk_buff *skb)

{

        struct sock *sk = skb->sk;

         /* In case it might be waiting for more memory. */

        atomic_sub(skb->truesize, &sk->sk_wmem_alloc);

        if (!sock_flag(sk, SOCK_USE_WRITE_QUEUE))

               sk->sk_write_space(sk);

        sock_put(sk);

}

 (Another subtle issue is worth pointing out here. For receive buffer accounting, we do not grab a reference to the socket (via ‘sock_hold()’), because the socket handling code will always make sure to free up any packets in it’s receive queue before allowing the socket to be destroyed. Whereas for send packets, we have to do proper accounting with ‘sock_hold()’ and ‘sock_put()’. Send packets can be freed asynchronously at any point in time. For example, a packet could sit in a devices transmit queue for a long time under certain conditions. If, meanwhile, the socket is closed, we have to keep the socket reference around until SKBs referencing that socket are liberated. )

 ·              unsigned char                        *head,

·                                                   *data,

·                                                   *tail,

·                                                   *end;

 SKB 对 Data Buffer 的巧妙管理,就是靠这四个指针实现的。

 

下图展示了这四个指针是如何管理数据 buffer 的:

Head 指向 buffer 的开始,end 指向 buffer 结束。 Data 指向实际数据的开始,tail 指向实际数据的结束。这四个指针将整个 buffer 分成三个区:

 

Packet data:这个空间保存的是真正的数据

Head room:处于 packet data 之上的空间,是一个空闲区域

Tail room:处于 packet data 之下的空间,也是空闲区域。

 

由于 TCP/IP 协议族是一种分层的协议,传输层、网络层、链路层,都有自己的协议头,因此 TCP/IP 协议栈对于数据包的处理是比较复杂的。为了提高处理效率,避免数据移动、拷贝,sk_buffer 在对数据 buffer 管理的时候,在 packet data 之上和之下,都预留了空间。如果需要增加协议头,只需要从 head room 中拿出一块空间即可,而如果需要增加数据,则可以从 tail room 中获得空间。这样,整个内存只分配一次空间,此后 协议的处理,只需要挪动指针。


5.   Sk_buffer 对内存的管理

我们以构造一个用于发送的数据包的过程,来理解 sk_buffer 是如何管理内存的。 

5.1. 构造Skb_buffer 

alloc_skb() 用于构造 skb_buffer,它需要一个参数,指定了存放 packet 的空间的大小。

构造时,不仅需要创建 skb_buffer 结构本身,还需要分配空间用于保存 packet。 

skb = alloc_skb(len, GFP_KERNEL);

  上图是在调用完 alloc_skb() 后的情况:

 head, data, tail 指向 buffer 开始,end 指向 buffer 结束,整个 buffer 都被当作 tail room。

Sk_buffer 当前的数据长度是0。 

5.2. 为 protocol header 留出空间

 通常,当构造一个用于发送的数据包时,需要留出足够的空间给协议头,包括 TCP/UDP header, IP header 和链路层头。

对 IPv4 数据包,可以从 sk->sk_prot->max_header 知道协议头的最大长度。

 skb_reserve(skb, header_len);

 上

  图是调用 skb_reserver() 后的情况

5.3. 将用户空间数据拷贝到 buffer 中

 

首先通过 skb_put(skb, user_data_len) ,从 tail room 中留出用于保存数据的空间

然后通过csum_and_copy_from_user() 将数据从用户空间拷贝到这个空间中。

 

5.4. 构造UDP协议头

 

通过 skb_push() ,向 head room 中要一块空间

然后在此空间中构造 UDP 头。

 

5.5. 构造 IP 头

 

通过 skb_push() ,向 head room 中要一块空间

然后在此空间中构造 IP 头。

 
6. sk_buffer 的秘密

当调用 alloc_skb() 构造 SKB 和 data buffer时,需要的 buffer 大小是这样计算的:

 data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);

 除了指定的 size 以外,还包括一个 struct skb_shared_info 结构的空间大小。也就是说,当调用 alloc_skb(size) 要求分配 size 大小的 buffer 的时候,同时还创建了一个 skb_shared_info 。

 这个结构定义如下:

 struct skb_shared_info {

            atomic_t            dataref;

            unsigned int       nr_frags;

            unsigned short   tso_size;

            unsigned short   tso_segs;

            struct sk_buff     *frag_list;

            skb_frag_t         frags[MAX_SKB_FRAGS];

};

  我们只要把 end 从 char* 转换成skb_shared_info* ,就能访问到这个结构

Linux 提供一个宏来做这种转换:

 #define skb_shinfo(SKB)             ((struct skb_shared_info *)((SKB)->end))

 那么,这个隐藏的结构用意何在?

它至少有两个目的:

1、  用于管理 paged data

2、  用于管理分片

 接下来分别研究 sk_buffer 对paged data 和分片的处理。

7.   对 paged data 的处理

 某些情况下,希望能将保存在文件中的数据,通过 socket 直接发送出去,这样,避免了把数据先从文件拷贝到缓冲区,从而提高了效率。

Linux采用一种 “paged data” 的技术,来提供这种支持。这种技术将文件中的数据直接被映射为多个page。

 Linux 用 struct skb_frag_strut 来管理这种 page:

 typedef struct skb_frag_struct skb_frag_t;

 struct skb_frag_struct {

        struct page *page;

        __u16 page_offset;

        __u16 size;

};

 并在shared info 中,用数组 frags[] 来管理这些结构。

 如此一来,sk_buffer 就不仅管理着一个 buffer 空间的数据了,它还可能通过 share info 结构管理一组保存在 page 中的数据。 

在采用 “paged data” 时,data_len 成员派上了用场,它表示有多少数据在 page 中。因此,

如果 data_len 非0,这个 sk_buffer 管理的数据就是“非线性”的。

skb->len – skb->data_len 就是非 paged 数据的长度。

 在有 “paged data” 情况下, skb_put()就无法使用了,必须使用 pskb_put() 。。。


8.   对分片的处理

 
9.   SKB 的管理函数

 9.1. Data Buffer 的基本管理函数

 ·  unsigned char *skb_put(struct sk_buff *skb, unsigned int len)

 “推”入数据

在 buffer 的结束位置,增加数据,len是要增加的长度。

这个函数有两个限制,需要调用者自己注意,否则后果由调用者负责

1)、不能用于 “paged data” 的情况

这要求调用者自己判断是否为 “paged data” 情况

2)、增加新数据后,长度不能超过 buffer 的实际大小。

这要求调用者自己计算能增加的数据大小

 · unsigned char *skb_push(struct sk_buff *skb, unsigned int len)

“压”入数据

从 buffer 起始位置,增加数据,len 是要增加的长度。

实际就是将新的数据“压”入到 head room 中

 · unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)

“拉”走数据

从 buffer 起始位置,去除数据, len 是要去除的长度。

如果 len 大于 skb->len,那么,什么也不做。

在处理接收到的 packet 过程中,通常要通过 skb_pull() 将最外层的协议头去掉;例如当网络层处理完毕后,就需要将网络层的 header 去掉,进一步交给传输层处理。

 ·  void skb_trim(struct sk_buff *skb, unsigned int len)

调整 buffer 的大小,len 是调整后的大小。

如果 len 比 buffer 小,则不做调整。

因此,实际是将 buffer 底部的数据去掉。

对于没有 paged data 的情况,很好处理;

但是有 paged data 情况下,则需要调用 __pskb_trim() 来进行处理。

  

9.2.  “Paged data” 和 分片的管理函数

 ·  char *pskb_pull(struct sk_buff *skb, unsigned int len)

 “拉“走数据

如果 len 大于线性 buffer 中的数据长度,则调用__pskb_pull_tail()  进行处理。

(Q:最后, return skb->data += len;  是否会导致 skb->data 超出了链头范围?)

 ·   int pskb_may_pull(struct sk_buff *skb, unsigned int len)

在调用 skb_pull() 去掉外层协议头之前,通常先调用此函数判断一下是否有足够的数据用于“pull”。

如果线性 buffer足够 pull,则返回1;

如果需要 pull 的数据超过 skb->len,则返回0;

最后,调用__pskb_pull_tail() 来检查 page buffer 有没有足够的数据用于 pull。

 ·   int pskb_trim(struct sk_buff *skb, unsigned int len)

将 Data Buffer 的数据长度调整为 len

在没有 page buffer 情况下,等同于 skb_trim();

在有 page buffer 情况下,需要调用___pskb_trim() 进一步处理。

 · int skb_linearize(struct sk_buff *skb, gfp_t gfp)

 · struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)

‘clone’ 一个新的 SKB。新的 SKB 和原有的 SKB 结构基本一样,区别在于:

1)、它们共享同一个 Data Buffer

2)、它们的 cloned 标志都设为1

3)、新的 SKB 的 sk 设置为空

(Q:在什么情况下用到克隆技术?)

 · struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)

 · struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask)

 · struct sk_buff *skb_pad(struct sk_buff *skb, int pad)

 · void skb_clone_fraglist(struct sk_buff *skb)

 · void skb_drop_fraglist(struct sk_buff *skb)

 · void copy_skb_header(struct sk_buff *new, const struct sk_buff *old)

 · pskb_expand_head(struct sk_buff *skb, int nhead, int ntail, gfp_t gfp_mask)

 · int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)

 · int skb_store_bits(const struct sk_buff *skb, int offset, void *from, int len)

 · struct sk_buff *skb_dequeue(struct sk_buff_head *list)

 · struct sk_buff *skb_dequeue(struct sk_buff_head *list)

 ·  void skb_queue_purge(struct sk_buff_head *list)

 ·  void skb_queue_purge(struct sk_buff_head *list)

 ·  void skb_queue_tail(struct sk_buff_head *list, struct sk_buff *newsk)

 ·  void skb_unlink(struct sk_buff *skb, struct sk_buff_head *list)

 ·  void skb_append(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)

 ·  void skb_insert(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)

 ·  int skb_add_data(struct sk_buff *skb, char __user *from, int copy)

 ·  struct sk_buff *skb_padto(struct sk_buff *skb, unsigned int len)

 ·  int skb_cow(struct sk_buff *skb, unsigned int headroom)

 这个函数要对 SKB 的 header room 调整,调整后的 header room 大小是 headroom.

如果 headroom 长度超过当前header room 的大小,或者 SKB 被 clone 过,那么需要调整,方法是:

分配一块新的 data buffer 空间,SKB 使用新的 data buffer 空间,而原有空间的引用计数减1。在没有其它使用者的情况下,原有空间被释放。

 · struct sk_buff *dev_alloc_skb(unsigned int length)

 · void skb_orphan(struct sk_buff *skb)

 · void skb_reserve(struct sk_buff *skb, unsigned int len)

 · int skb_tailroom(const struct sk_buff *skb)

 · int skb_headroom(const struct sk_buff *skb)

 · int skb_pagelen(const struct sk_buff *skb)

 · int skb_headlen(const struct sk_buff *skb)

 · int skb_is_nonlinear(const struct sk_buff *skb)

 · struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t pri)

 如果skb 只有一个引用者,直接返回 skb

否则 clone 一个 SKB,将原来的 skb->users 减1,返回新的 SKB

  需要特别留意 pskb_pull() 和 pskb_may_pull() 是如何被使用的:

 1)、在接收数据的时候,大量使用 pskb_may_pull(),其主要目的是判断 SKB 中有没有足够的数据,例如在 ip_rcv() 中:

 if (!pskb_may_pull(skb, sizeof(struct iphdr)))

                        goto inhdr_error;

 iph = skb->nh.iph;

 它的目的是拿到 IP header,但取之前,先通过 pskb_may_pull() 判断一下有没有足够一个 IP header 的数据。

 2)、当我们构造 IP 分组的时候,对于数据部分,通过 put向下扩展空间(如果一个sk_buffer 不够用怎么分片?);对于 传输层、网络层、链路层的头,通过 push 向上扩展空间;

 3)、当我们解析 IP 分组的时候,通过 pull(),从头开始,向下压缩空间。

 因此,put 和 push 主要用在发送数据包的时候;而pull 主要用在接收数据包的时候。

 
 10. 各种 header

 union {

                        struct tcphdr      *th;

                        struct udphdr     *uh;

                        struct icmphdr   *icmph;

                        struct igmphdr   *igmph;

                        struct iphdr        *ipiph;

                        struct ipv6hdr     *ipv6h;

                        unsigned char    *raw;

            } h;

 

            union {

                        struct iphdr        *iph;

                        struct ipv6hdr     *ipv6h;

                        struct arphdr      *arph;

                        unsigned char    *raw;

            } nh;

 

            union {

                        unsigned char    *raw;

            } mac;

11. 参考资料

1. http://vger.kernel.org/~davem/skb.html

2. Linux 2.4 内核源码

3. Linux 2.6 内核源码

4. <<The.Linux.Networking.Architecture_Design.and.Implementation.of.Network.Protocols.in.the.Linux.Kernel >>

5. <<Understanding Linux Network Internals>>

6. << The Linux TCPIP Stack- Networking for Embedded Systems>>

TCP学习笔记——TCP的超时与重传

参考书目:《TCP/IP详解 卷一:协议》

对于每个连接,TCP管理4个不同的定时器:

1、 重传定时器 —— 希望收到另一端的确认;
2、 坚持(persist)定时器 —— 使窗口大小信息保持不断流动;
3、保活(keepalive)定时器 —— 检测一个空闲连接的另一端何时崩溃或重启;
4、2MSL定时器 —— 测量一个连接处于TIME_WAIT状态的时间。

超时时间:第一次发送后所设置的超时时间为1.5秒,此后该时间在每次重传时增加1倍,到64s后维持不变(如下图所示)。这个倍乘关系称为指数退避(exponential backoff)。

连续重传的时间差
重传次序 1 2 3 4 5 6 7
时间差(s) 1.5 3 6 12 24 48 64 64

首次分组传输复位信号传输之间的时间差约为9分钟,这个时间在TCP实现中是不可变的。
也就是说第一次发送数据后9分钟内若没有收到ACK确认,则发送RST复位信号。

-UPDATING-