在分析Linux网络栈的时候,分析网络子系统的初始化是一件很重要的事情。有一些子系统并不能以模块的形式出现,因为它们是必须存在于内核当中,随内核启动而加载。不过,与普通应用程序初始化不同的是,它们的初始化工作,并没有使用显示的函数调用,而是透过一些巧秒的宏来实现。例如:
- /* Initialize the DEV module. */static int __init net_dev_init(void){ ……}subsys_initcall(net_dev_init);
复制代码网络设备子系统使用了宏subsys_initcall向内核注册它的初始化函数。当然内核中除了subsys_initcall,还有很多类似的xxx_initcall。另一方面,内核在实始化的时候,提供了一个内核命令行参数,允许用户向内核启动时传递参数,一些网络子系统的初始参数配置,也是通过这样一种方式实现初始化。例如:
- __setup("netdev", netdev_boot_setup);
复制代码之所以这样做,最重要的出发点在于内核优化:一些只用于初始化工作的模块,只应该被执行一次,结束后,它们占用的内存应该被释放掉——因为内核一旦启动,在关机之间,都是长驻内存的,不释放掉,就是占着茅坑不拉屎。本文主要是分析这些宏背后的运行机制。透过一个仿真的小程序,来揭秘其机制原理——程序中使用的宏和函数名称,尽量地跟内核代码完全一样。首先对仿真程序逐行介绍,最后我会贴出程序来,如果感兴趣,可以先运行一下它,观察一下,再来看本贴。启动的内核命令行参数,以及其对应的处理函数。使用一个名为obs_kernel_param的结构,把启动关键字同其处理函数封装:
- /* 关键与其处理函数的封装 */struct obs_kernel_param { const char *str; /* 关键字 */ int (*setup_func)(char *); /* 处理函数 */ int early; /* 本例中未使用 */};
复制代码再定义一个宏__setup_param,其作用是,允许用户定义一个类型为obs_kernel_param的结构变量,并初始化其成员:
- #define __setup_param(str, unique_id, fn, early) / static char __setup_str_##unique_id[] __initdata = str; / static struct obs_kernel_param __setup_##unique_id / __attribute_used__ / __attribute__((__section__(".init.setup"))) / __attribute__((aligned((sizeof(long))))) / = { __setup_str_##unique_id, fn, early }
复制代码与普通的定义稍有不同,宏里面使用了gcc的扩展属性。1、__attribute__((__section__("xxx")))自定义“段(section)”,elf中,包括原来的a.out格式,在编译和连接阶段,都将二进制文件按一定格式都分为若干段,如数据段、代码段等等,在加载器加载运行程序的时候,它们又被相应地映射至内存(这一句话估计够一本书来描述了)。这里需要了解的是,gcc允许程序员自定义一个新的段。就像这里展示的一样,一个名为.init.setup的段被创建,所有obs_kernel_param变量都被放在了这里,而不是原来默认的其它地方。2、另一个重要的扩展是对齐:__attribute__((aligned((sizeof(long)))))内存对齐问题,我想用不着过多地讨论了。OK,主角登场,定义一个名为__setup的宏:
- #define __setup(str, fn) /__setup_param(str, fn, fn, 0)
复制代码这个包裹,主要是对于obs_kernel_param的early成员的操作,否则,它们就完全一样了,可惜,本文出发点不同,并未使用early。另一个主角是subsys_initcall:
- #define __define_initcall(level,fn) / static initcall_t __initcall_##fn __attribute_used__ / __attribute__((__section__(".initcall" level ".init"))) = fn #define subsys_initcall(fn) __define_initcall("4",fn)typedef int (*initcall_t)(void);
复制代码__define_initcall(这个宏定义了一个函数指针(使用typedef定义了这个新的函数指针类型),其名称与宏的参数有关,同样地,它也使用了gcc扩展,创建一个名为.initcall" level ".init"的段,level是参数,也就是说,名称可以由调用者控制,函数指针指向它的另一个参数fn。subsys_initcall宏是一个包裹定义,主要是指明了level,这里是4。接下来,使用__setup宏注册了三个关键字:
- __setup("netdev=", netdev_boot_setup);__setup("ether=", ether_boot_setup);__setup("ip=", ip_auto_config_setup);
复制代码也定义了一个函数指针:
- subsys_initcall(net_dev_init);
复制代码对应的函数,具体实现为:
- static int __init ip_auto_config_setup(char *addrs){ printf("cmdline = %s/n", addrs); return 1;}static int __init ether_boot_setup(char *ether){ printf("ether = %s/n", ether); return 1;}static int __init netdev_boot_setup(char *netdev){ printf("netdev = %s/n", netdev); return 1;}static int __init net_dev_init(void){ /* Initialize the DEV module. */ printf("call net_dev_init/n"); return 0;}
复制代码当然,仿真嘛,毕竟不是真的。函数其实没有任何具体任务,就是打印一个记号,冒个泡。现在深入到问题的核心了,如何使用这些自定义段,也就是说,得知道它们链接后重定位的具体地址,这是通过gcc提供的自定义链接控制脚本来实现的:
- [root@Kendo develop]# cat my.ldsSECTIONS{ .text : { *(.text) } . = 0x08100000; .init.text : { *(.init.text) } .init.data : { *(.init.data) } . = ALIGN(16); __setup_start = .; .init.setup : { *(.init.setup) } __setup_end = .; __initcall_start = .; .initcall.init : { *(.initcall1.init) *(.initcall2.init) *(.initcall3.init) *(.initcall4.init) *(.initcall5.init) *(.initcall6.init) *(.initcall7.init) } __initcall_end = .;}
复制代码脚本中,明确地指明了自定义段的开始地址:. = 0x08100000;一共有.init.text //自定义文本段.init.data //自定义数据段.init.setup //刚才用__setup定义的几个启动关键字的对应的变量就放在这里.initcallX.init //一共7个,不过我只用了第4个。另外要注意的是,同时定义了四个变量,__setup_start = .;它指向.init.setup的开始位置,同样__setup_end = .;就是结束位置。__initcall_start/end = .;同理。这样,要程序中使用这四个变量,应该申明它们是外部变量:
- extern struct obs_kernel_param __setup_start[], __setup_end[];extern initcall_t __initcall_start[], __initcall_end[];
复制代码
- int main(int argc, char **argv){ if(argc < 2) return -1; printf("%x, %x/n", __setup_start, __setup_end); start_kernel(argv[1]); do_initcalls(); free_initmem(); return 0;}
复制代码在我的主函数中,调用了两个函数,前者用来调用关键解析函数,后者用于调用xxx_initcalls:
- static void __init do_initcalls(void){ initcall_t *call; for (call = __initcall_start; call < __initcall_end; call++) { (*call)(); }}因为__initcall_start指明了自定义段(程序运行后,加载进内存,就不存在段了,通过内存映射机制,对应了Linux内存区域VM)的开始地址,end是结束地址,循环遍历之,调用每个函数,这样,使用subsys_initcall注册的每个函数,都将被调用。subsys_initcall(net_dev_init);static int __init start_kernel(char *line){ struct obs_kernel_param *p; p = __setup_start; do { p->setup_func(line); p++; } while (p < __setup_end); return 0;}同样的道理,遍历自定义段.ini.setup的每个obs_kernel_param结构,调用__setup宏注册的关键字的函数,内核具体实现时,肯定有一个字符串解析和匹备的过程,这里也没有必要去分析字符串了。
复制代码最后,调用free_initmem以释放掉这些不再会被使用的内存区域。
- static void *free_initmem(void){ /* 释放掉不用的内存 */ }
复制代码它又是一个空函数,因为用户态程序跟内核的内存管理机制相差太大。暂时无法仿真了。但是这个不影响对整个框架的分析。编译它:
- [root@Kendo develop]# gcc -o test.o -c test.c[root@Kendo develop]# gcc -o test test.o –with-lds my.lds
复制代码注意,链接的时候,使用了–with-lds,表示要使用自定义链接脚本,这个脚本,刚才已经展示过了。先来看运行结果:
- [root@Kendo develop]# ./test hello,world!81000e0, 8100104netdev = hello,world!ether = hello,world!cmdline = hello,world!call net_dev_init
复制代码看起来还像那么回事,每个冒的泡泡都出现了。最使,使用readelf工具,来看看程序中的自定义段:
- [root@Kendo develop]# readelf -S testThere are 32 section headers, starting at offset 0x1318:Section Headers:[Nr] Name Type Addr Off Size ES Flg Lk Inf Al[color=Red][25] .init.text PROGBITS 08100000 001000 0000cd 00AX0 01[26] .init.data PROGBITS 081000cd 0010cd 000013 00WA0 01[27] .init.setup PROGBITS 081000e0 0010e0 000024 00WA0 04[28] .initcall.init PROGBITS 08100104 001104 000004 00WA0 04[/color]
复制代码附件1,完整程序清单
- #include <stdio.h>#define __init __attribute__ ((__section__ (".init.text")))#define __initdata __attribute__ ((__section__ (".init.data")))#define __setup_param(str, unique_id, fn, early) / static char __setup_str_##unique_id[] __initdata = str; / static struct obs_kernel_param __setup_##unique_id / __attribute_used__ / __attribute__((__section__(".init.setup"))) / __attribute__((aligned((sizeof(long))))) / = { __setup_str_##unique_id, fn, early }#define __setup(str, fn) /__setup_param(str, fn, fn, 0)#define __define_initcall(level,fn) / static initcall_t __initcall_##fn __attribute_used__ / __attribute__((__section__(".initcall" level ".init"))) = fn #define subsys_initcall(fn) __define_initcall("4",fn)/* 关键与其处理函数的封装 */struct obs_kernel_param { const char *str; /* 关键字 */ int (*setup_func)(char *); /* 处理函数 */ int early; /* 本例中未使用 */};typedef int (*initcall_t)(void);extern struct obs_kernel_param __setup_start[], __setup_end[];extern initcall_t __initcall_start[], __initcall_end[];static int __init ip_auto_config_setup(char *addrs){ printf("cmdline = %s/n", addrs); return 1;}static int __init ether_boot_setup(char *ether){ printf("ether = %s/n", ether); return 1;}static int __init netdev_boot_setup(char *netdev){ printf("netdev = %s/n", netdev); return 1;} __setup("netdev=", netdev_boot_setup);__setup("ether=", ether_boot_setup);__setup("ip=", ip_auto_config_setup);static int __init net_dev_init(void){ /* Initialize the DEV module. */ printf("call net_dev_init/n"); return 0;}static void __init do_initcalls(void){ initcall_t *call; for (call = __initcall_start; call < __initcall_end; call++) { (*call)(); }}subsys_initcall(net_dev_init);static int __init start_kernel(char *line){ struct obs_kernel_param *p; p = __setup_start; do { p->setup_func(line); p++; } while (p < __setup_end); return 0;}static void *free_initmem(void){ /* 释放掉不用的内存 */ }int main(int argc, char **argv){ if(argc < 2) return -1; printf("%x, %x/n", __setup_start, __setup_end); start_kernel(argv[1]); do_initcalls(); free_initmem(); return 0;}
复制代码附件2:如果对gcc自定义段还不熟悉,有个官方文档说明:
不要因为生活琐事而烦恼,不要因为儿女情长而忧愁,