zl程序教程

您现在的位置是:首页 >  其它

当前栏目

module_init机制

机制 module init
2023-09-11 14:19:53 时间

模块代码有两种运行方式,一是静态编译连接进内核,在系统启动过程中进行初始化;一是编译成可动态加载的module,通过insmod动态加载重定位到内核。这两种方式可以在Makefile中通过obj-y或obj-m选项进行选择。

而一旦可动态加载的模块目标代码(.ko)被加载重定位到内核,其作用域和静态链接的代码是完全等价的。所以这种运行方式的优点显而易见:

  1. 可根据系统需要运行动态加载模块,以扩充内核功能,不需要时将其卸载,以释放内存空间;
  2. 当需要修改内核功能时,只需编译相应模块,而不必重新编译整个内核。

因为这样的优点,在进行设备驱动开发时,基本上都是将其编译成可动态加载的模块。但是需要注意,有些模块必须要编译到内核,随内核一起运行,从不卸载,如 vfs、platform_bus等。

那么同样一份C代码如何实现这两种方式的呢?答案就在于module_init宏!

#ifndef MODULE

#define module_init(x)    __initcall(x);

#else /* MODULE */

/* Each module must use one module_init(). */
#define module_init(initfn)                    \
    static inline initcall_t __maybe_unused __inittest(void)        \
    { return initfn; }                    \
    int init_module(void) __copy(initfn)            \
        __attribute__((alias(#initfn)));        \
    __CFI_ADDRESSABLE(init_module, __initdata);
#endif

 

显然,MODULE 是由Makefile控制的。上面部分用于将模块静态编译连接进内核,下面部分用于编译可动态加载的模块。接下来我们对这两种情况进行分析。

 

方式一:#ifndef MODULE

module_intit(fn) 展开来就是:

  static initcall_t 变量名 __used __attribute__((__section__(.initcall6.init))) = fn;

定义一个函数指针类型的变量,并把该变量放在代码段 .initcall6.init

接下来看链接脚本vmlinux.lds

 .init.data : AT(ADDR(.init.data) - 0) 
{
  KEEP(*(SORT(___kentry+*)))
  ...
  __initcall_start = .;
  KEEP(*(.initcallearly.init))
  __initcall0_start = .;
  KEEP(*(.initcall0.init))
  KEEP(*(.initcall0s.init))
  ...
  __initcall6_start = .;
  KEEP(*(.initcall6.init))
  KEEP(*(.initcall6s.init))
  __initcall7_start = .;
  KEEP(*(.initcall7.init))
  KEEP(*(.initcall7s.init))
  __initcall_end = .;
  ...
  . = ALIGN(4);
  __initramfs_start = .;
  KEEP(*(.init.ramfs))
  . = ALIGN(8);
  KEEP(*(.init.ramfs.info)) }

 

上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到:
Freeing unused kernel memory: 124K (80312000 - 80331000)

那么存放于 .initcall6.init 段中的 __initcall_hello_init6 是怎么样被调用的呢?我们看文件 init/main.c,代码梳理如下:

start_kernel
|
--> arch_call_rest_init
  |
  --> rest_init
    |
      --> kernel_thread
        |
        --> kernel_init
            |
            --> kernel_init_freeable
                |
                --> do_basic_setup
                    |
                    --> do_initcalls
                        |
                        --> do_initcall_level(level)
                            |
                            --> do_one_initcall(initcall_t fn)
static initcall_entry_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

 

 do_initcalls()会遍历数组initcall_levels[],数组的每个成员是一个代码段,do_initcall_level()会遍历代码段的所有函数指针,do_one_initcall()则执行每个函数指针。因为编译器根据链接脚本的要求将各个函数指针链接到了指定的位置,所以可以放心地用 do_one_initcall(*fn) 来执行相关初始化函数。

我们例子中的 module_init(hello_init) 是 level6 的 initcalls 段,比较靠后调用,很多外设驱动都调用 module_init 宏,如果是静态编译连接进内核,则这些函数指针会按照编译先后顺序插入到 initcall6.init 段中,然后等待 do_initcalls 函数调用。

方式二:#else

 

#define module_init(initfn)                    \
    static inline initcall_t __maybe_unused __inittest(void)        \
    { return initfn; }                    \
    int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));        \

__inittest() 仅仅是为了检测定义的函数是否符合 initcall_t 类型,如果不是 __inittest 类型在编译时将会报错。

 

alias 属性是 gcc 的特有属性,将 init_module 定义为 initfn 的别名。所以 module_init(hello_init) 的作用就是定义一个变量 init_module,其地址和 hello_init 是一样的。

// filename: HelloWorld.c
 
#include <linux/module.h>
#include <linux/init.h>
 
static int hello_init(void)
{
    printk(KERN_ALERT "Hello World\n");
    return 0;
}
 
static void hello_exit(void)
{
    printk(KERN_ALERT "Bye Bye World\n");
}
 
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");

上述例子编译可动态加载模块过程中,会自动产生 HelloWorld.mod.c 文件,内容如下:

#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
 
MODULE_INFO(vermagic, VERMAGIC_STRING);
 
struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,
#endif
    .arch = MODULE_ARCH_INIT,
};
 
static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";

 

 可知,其定义了一个类型为 module 的全局变量 __this_module,成员 init 为 init_module(即 hello_init),且该变量链接到 .gnu.linkonce.this_module 段中。

 

编译后所得的 HelloWorld.ko 需要通过 insmod 将其加载进内核,由于 insmod 是 busybox 提供的用户层命令,所以我们需要阅读 busybox 源码。

// modutils/insmod.c
int insmod_main(int argc UNUSED_PARAM, char **argv)
{
    char *filename;
...
    rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));
    if (rc)
        bb_error_msg("can't insert '%s': %s", filename, moderror(rc));

    return rc;
}

insmod_main
|
--> bb_init_module
    |
    --> init_module

 

 

// modutils/modutils.c
/* Return:
 * 0 on success,
 * -errno on open/read error,
 * errno on init_module() error
 */
int FAST_FUNC bb_init_module(const char *filename, const char *options)
{
    size_t image_size;
    char *image;
    int rc;
...

    errno = 0;
    init_module(image, image_size, options);
    rc = errno;
    if (mmaped)
        munmap(image, image_size);
    else
        free(image);
    return rc;
}

 

 而 init_module 定义如下: //此 init_module 不同于驱动代码编译出来的 init_module,此处是 busybox 内的宏。

// modutils/modutils.c
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)

//上面的宏逻辑上相当于
# define init_module(mod, len, opts) sys_init_module(mod, len, opts)

 

syscall()是C库提供的一个函数,它调用系统调用。函数原型如下:

long int syscall (long int sysno, ...);

 

sysno:系统调用号

... :为剩余可变长的参数,为系统调用所带的参数,根据系统调用的不同,可带0~5个不等的参数,如果超过特定系统调用能带的参数,多余的参数被忽略。

返回值:该函数返回值为特定系统调用的返回值,在系统调用成功之后你可以将该返回值转化为特定的类型,如果系统调用失败则返回 -1,错误代码存放在 errno 中。

 

sys_init_module()源码

SYSCALL_DEFINE3(init_module, void __user *, umod,
        unsigned long, len, const char __user *, uargs)
{
    int err;
    struct load_info info = { };

    err = may_init_module();
    if (err)
        return err;

    pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
           umod, len, uargs);

    err = copy_module_from_user(umod, len, &info);
    if (err)
        return err;

    return load_module(&info, uargs, 0);
}

调用关系:
SYSCALL_DEFINE3(init_module, ...)
|
-->load_module
    |
    --> do_init_module(mod)
        |
        --> do_one_initcall(mod->init);

 

 

 do_one_initcall(mod->init):这里就是执行驱动加载函数,mod->init是指向module_init宏指定的驱动加载函数的函数指针

 load_module():在函数内部会将描述驱动的struct module结构体注册到全局变量modules中