[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, &param, &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 会被触发。

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。