首页 > 代码库 > [Linux] __init & __setup 等宏的代码追踪
[Linux] __init & __setup 等宏的代码追踪
Platform:Linux 3.0.35
模仿 fbmem.c 的代码添加了 __setup 却无法触发效果(代码如下),所以原本的打算是追一下这个 __setup 的流程,结果还牵扯到了 kernel 初始化的一些相关知识,在此作简单记录。
static int __init my_video_setup(char *options){ printk("%s-------------------------%s\n",__FUNCTION__, options); return 1;}__setup("video=", my_video_setup);
__init_ & __init_data & __exit
先讲一下这两个宏,我们在很多地方都可以看到这两个宏对变量和方法的修饰,看一下宏的定义:
init.h
#define __init __section(.init.text) __cold notrace#define __initdata __section(.init.data)#define __initconst __section(.init.rodata)#define __exitdata __section(.exit.data)#define __exit_call __used __section(.exitcall.exit)
compiler.h
#define __section(S) __attribute__ ((__section__(#S)))
我没有对 __attribute__ 和 __section__ 做深入调查,但是它的作用就是将描述的变量或函数放入指定的段。
我们知道Linux可执行文件是ELF格式的,通过链接器和链接脚本将一个个的对象文件链接到一个ELF文件中,一个对象文件有很多段组成,如Text、Data、Bss等。
看一下__init,__initdata 是如何使用的:
static int __initdata text_int2 = 100;static int __init ch7036_init(void){ return i2c_add_driver(&ch7036_driver);}static void __exit ch7036_exit(void){ i2c_del_driver(&ch7036_driver);}module_init(ch7036_init);module_exit(ch7036_exit);
可以看到,我们对一个驱动模块的初始化函数统称都会添加 __init,对一个驱动模块的退出函数都会添加一个 __exit 来修饰。
被 __init 修饰的函数被放在 .init.text 段,__initdata 修饰的变量被放在 .init.data 段,在模块加载的时候会被调用,在模块加载初始化完成后,变量和函数占用的内存会被释放,整个.init 段都会被释放。
__exit 的作用类似 __init,只不过是在驱动模块卸载时调用。
下面这个例子可以验证我们上面所说的结论:
static int text_int1 = 10;static int __initdata text_int2 = 100;static ssize_t user_test(struct device *dev, struct device_attribute *attr, const char *buf, size_t count){ printk("----------------------text_int1----%d\n",text_int1); printk("----------------------text_int2----%d\n",text_int2); return count;}static DEVICE_ATTR(test, 644, NULL, user_test);static int __devinit dev_probe(struct i2c_client *client, const struct i2c_device_id *id){ ...... ......
printk("-------------ch7036_probe---------text_int2----%d\n",text_int2); return 0; ...... ......}
结果是:
probe中可以输出 text_int2 的值 100; user_test中输出 text_int2 的值是0,因为 text_int2 在模块初始化结束后就已经释放了。
接下来看一下 module_init(), module_exit(),subsys_initcall() 这些函数是如何被调用的:
init.h
#define module_init(x) __initcall(x);#define module_exit(x) __exitcall(x);#define __initcall(fn) device_initcall(fn)#define __exitcall(fn) static exitcall_t __exitcall_##fn __exit_call = fn#define pure_initcall(fn) __define_initcall("0",fn,0)#define core_initcall(fn) __define_initcall("1",fn,1)#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)#define postcore_initcall(fn) __define_initcall("2",fn,2)#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)#define arch_initcall(fn) __define_initcall("3",fn,3)#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)#define subsys_initcall(fn) __define_initcall("4",fn,4)#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)#define fs_initcall(fn) __define_initcall("5",fn,5)#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)#define device_initcall(fn) __define_initcall("6",fn,6)#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)#define late_initcall(fn) __define_initcall("7",fn,7)#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)#define __define_initcall(level,fn,id) static initcall_t __initcall_##fn##id __used __attribute__((__section__(".initcall" level ".init"))) = fn
我们注意 __define_initcall 的定义,它就是将对应的 fn 放到了 ".initcall" level ".init" 段中,而这个 level 参数是传进来的,比如:
// level = 4,对应 .initcall4.init 段#define subsys_initcall(fn) __define_initcall("4",fn,4)// level = 6,对应 .initcall6.init 段#define device_initcall(fn) __define_initcall("6",fn,6)
根据前面的介绍,我们知道这些段必定会被链接器使用,而段的定义必定会在链接器脚本中:
vmlinux.lds.h:
#define INITCALLS *(.initcallearly.init) VMLINUX_SYMBOL(__early_initcall_end) = .; *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init) *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init) *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s.init) *(.initcall7.init) *(.initcall7s.init)#define INIT_CALLS \ VMLINUX_SYMBOL(__initcall_start) = .; INITCALLS VMLINUX_SYMBOL(__initcall_end) = .;
接下来的问题就是,kernel是如何定位到 init 段,以此执行每个 init 段中的函数呢,其实在kernel启动的时候会做这件事情:
kernel/init/main.c
start_kernel() -> rest_init() -> kernel_init() -> do_basic_setup() -> do_initcalls()
extern initcall_t __initcall_start[], __initcall_end[], __early_initcall_end[];static void __init do_initcalls(void){ initcall_t *fn; for (fn = __early_initcall_end; fn < __initcall_end; fn++) do_one_initcall(*fn);}
从 __early_initcall_end 开始遍历到 __initcall_end,从上面 vmlinux.lds.h 中的代码中可以知道,其实就是从 .initcall0.init 开始遍历到 .initcall7s.init,依次调用这些段中的函数。
static int __init_or_module do_one_initcall_debug(initcall_t fn){ int ret; ... ... ret = fn(); ... ... return ret;}
上述就是对 __init 段的追踪,我们看到了驱动模块如何添加变量和方法到不同的 __init 段,链接脚本如何定义这些 __init 段,以及kernel何如遍历这些 __init 段并调用里面的函数。
TODO:驱动模块加载完成后,如何销毁内存;对 __exit 等其它段的追踪。
__setup 的实现
__setup 宏的作用是根据传入的字符串参数,与bootloader传递的参数进行匹配,从而调用传入的函数,如:
static int __init video_setup(char *options){ printk("%s\n", options); return 1;}__setup("video=", video_setup);
假如bootloader传给kernel的参数是“ video=xxxxx”,则会调用 video_setup 函数,printk 输出“xxxxx”。
看一下 __setup 的定义:
init.h:
#define __setup_param(str, unique_id, fn, early) static const char __setup_str_##unique_id[] __initconst __aligned(1) = str; static struct obs_kernel_param __setup_##unique_id __used __section(.init.setup) __attribute__((aligned((sizeof(long))))) = { __setup_str_##unique_id, fn, early }#define __setup(str, fn) \ __setup_param(str, fn, fn, 0)
以 “__setup("video=", video_setup);” 为例,可以简化为:
static const char __setup_str_video_setup[] __initconst __aligned(1) = "video=";static struct obs_kernel_param __setup_video_setup __used __section(.init.setup) __attribute__((aligned((sizeof(long))))) = { __setup_str_video_setup, video_setup, 0};
简单来讲就是定义了一个 obs_kernel_param 结构题变量,它的名字叫做 __setup_video_setup,里面有三个成员,分别是 char数组__setup_str_video_setup="video=" 、函数video_setup 、 一个叫做early的变量,此处的值为0。重点是,这个结构体变量被存放进了.init.setup 段。
联想到之前的 init 段,这个 .init.setup 段必然也是在kernel启动的时候被遍历。
先看一下链接器脚本文件 vmlinux.lds.h:
#define INIT_SETUP(initsetup_align) \ . = ALIGN(initsetup_align); VMLINUX_SYMBOL(__setup_start) = .; *(.init.setup) VMLINUX_SYMBOL(__setup_end) = .;
main.c
start_kernel() -> parse_early_param() / parse_args(...)
两个函数的本质一样,都是调用了 parse_args 函数。
前者:
parse_args("early options", cmdline, NULL, 0, do_early_param);
后者:
parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption);
params.c
int parse_args(const char *name, char *args, const struct kernel_param *params, unsigned num, int (*unknown)(char *param, char *val)){ char *param, *val; ... ... while (*args) { int ret; int irq_was_disabled; args = next_arg(args, ¶m, &val); irq_was_disabled = irqs_disabled(); ret = parse_one(param, val, params, num, unknown); ... ... } /* All parsed OK. */ return 0;}
看起来就是解析bootloader传给kernel的参数,在while循环里将参数一个个的取出,丢给 parse_one 函数。
比如(以空格做分隔做解析):
console=ttymxc1,115200 androidboot.console=ttymxc1 androidboot.hardware=freescale init=/init vmalloc=400M video=mxcfb0:dev=hdmi,1920x1080M@60,bpp=32
parse_one 函数的本质就是调用unkonwn函数,传递的参数就是param和val。
所以,两个函数本质分别是:
1. 遍历调用 do_early_param 函数,传入的参数有2个,格式是"xx=" 和 "xx",如:"console=" 和 "ttymxc1,115200" ; "androidboot.console=" 和 “ttymxc1” ; ... ...
2. 遍历调用 unknown_bootoption 函数,传入的参数同上。
static int __init do_early_param(char *param, char *val){ const struct obs_kernel_param *p; for (p = __setup_start; p < __setup_end; p++) { if ((p->early && strcmp(param, p->str) == 0) || (strcmp(param, "console") == 0 && strcmp(p->str, "earlycon") == 0) ) { if (p->setup_func(val) != 0) printk(KERN_WARNING "Malformed early option ‘%s‘\n", param); } } /* We accept everything at this stage. */ return 0;}
首先定义了 struct obs_kernel_param *p, 还记得我们在分解 __setup 宏的时候提到过 名字叫做 __setup_video_setup 的 obs_kernel_param 变量,这个的指针p就是它。
然后从 __setup_start 开始遍历到 __setup_end,根据上面 vmlinux.lds.h 的代码知道,就是遍历整个 .init.setup 段。
看代码 “ (p->early && strcmp(param, p->str) == 0) ” 这边就体现了 early 的作用了,原来early 的值假如不为0,则会被更早得解析处理到。
随后,将param(从bootloader传过来的)和 p->str(__setup传进来的)进行匹配,若匹配成功,则调用 p->setup_func 方法,这个方法也是我们 __setup 自己定义传进来的。
unknown_bootoption 函数的功能类似,但它不会处理 p->early = 0 的数据。
基本逻辑就是这样,回到最初的问题,我在两个驱动模块中定义了 __setup("video=", my_video_setup),为什么模块1的函数能被触发,而模块2的函数无法被触发,原因很简单,因为当kernel拿着“video=”这个字串到 .init.setup 段中去遍历时,一旦遍历到匹配字串,就会调用对应函数,然后就终止遍历,所以始终只有一个匹配的 __setup 会被触发。
[Linux] __init & __setup 等宏的代码追踪