【翻译】内核组件初始化体系结构
为了全面了解内核组件,你不仅需要了解特定的程序做了什么,也要知道这些程序什么时候被谁调用。内核子系统的初始化是一项基本任务,这些任务由内核根据它自己的模式来处理。这个体系结构值得我们学习并有助于理解网络堆栈的核心组件,包括网络设备驱动程序是如何初始化的。
本章的目的在于展示内核怎样处理用于初始化内核组件的函数,既包含静态嵌入内核的组件,也包括作为内核模块加载的组件,特别是网络设备。我们将会弄明白如下几点:
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,0x260,eth0 ether=15,0x300,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] != '\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项目的主页是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、内存最优化
不像用户空间代码和数据,内核的代码和数据永远保留在主内存中。因此在各种可能的方面减少内存的浪费变的很重要了。初始化代码对于内存优化是个很好的选择。绝大多数初始化函数要麽只执行一次,要麽根本就不执行,这视内核配置而定。例如:
l module_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, ifmap netdev_boot_setup为ether=和netdev=存储启动时参数,ifmap 是结构netdev_boot_setup的一个域.
变量
dev_boot_setup netdev_boot_setup结构数组
NETDEV_BOOT_SETUP_MAX dev_boot_setup数组大小
7.8、本节涉及的文件和目录
图7-5列出了本章参考的文件和目录
图7-5 本章参考的文件和目录