什么是initcall

initcallLinux 内核中的一种机制,用于在系统启动过程中自动执行初始化函数。这些函数在内核启动阶段被内核自动调用,用于初始化各种子系统、总线、驱动、文件系统等。

也就是说,如果我们的驱动想要被linux调用,就要使用initcall来“通知”linux: 我需要被你调用。

在驱动中我们可以看到xxx_init这样的形式,比如module_init

initcall的种类

linux6.15.0-rc1include/linux/init.h中可以看到如下代码:

#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ____define_initcall(fn, __stub, __name, __sec) \
__define_initcall_stub(__stub, fn) \
asm(".section \"" __sec "\", \"a\" \n" \
__stringify(__name) ": \n" \
".long " __stringify(__stub) " - . \n" \
".previous \n"); \
static_assert(__same_type(initcall_t, &fn));
#else
#define ____define_initcall(fn, __unused, __name, __sec) \
static initcall_t __name __used \
__attribute__((__section__(__sec))) = fn;
#endif

#define __unique_initcall(fn, id, __sec, __iid) \
____define_initcall(fn, \
__initcall_stub(fn, __iid, id), \
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))

#define ___define_initcall(fn, id, __sec) \
__unique_initcall(fn, id, __sec, __initcall_id(fn))

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)

/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)

#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn

#define console_initcall(fn) ___define_initcall(fn, con, .con_initcall)

通过注释我们可以看到,这些initcall都是被用于built-in的驱动的,作为ko模块编译的驱动的initcall我们将稍后讨论。

这里注意到有些initcall后面带了sync的后缀,优先级方便也是有了s的后缀,带s的要比不带s的优先级要低

这里sync的意思是,某些模块必须等另一个模块初始化完才能工作,那就需要用*_sync(),让它们顺序执行,防止 race 条件或访问未初始化的数据

built-in initcall

这些initcall只能在内核构建的时候一起编译进入内核,不能作为单独模块编译加载。

优先级和使用场景

作为built-ininitcall,下面是他们的优先级和使用场景:

等级宏名 优先级 用于初始化什么
early_initcall() -1 非常早的初始化,在 SMP 启动前,只用于核心代码
pure_initcall() 0 仅初始化一些全局变量,用于非常“纯”的代码
core_initcall() 1 内核核心子系统,比如内存管理、调度器等
postcore_initcall() 2 核心子系统之后,比如 console、boot param 等
arch_initcall() 3 特定架构(x86、arm 等)相关初始化
subsys_initcall() 4 各种内核子系统,如 i2c、spi、mmc 等
fs_initcall() 5 文件系统,如 ext4、vfat 等初始化
rootfs_initcall() - 特别用于挂载 rootfs 的钩子
device_initcall() 6 设备驱动,比如 platform_driver_register 等
late_initcall() 7 最后阶段的初始化,比如 deferred probe 等

其中需要特别说明的是rootfs_initcall,他的作用是初始化 rootfs 和挂载根文件系统,常用场景:内建initramfs 、解压 cpio、挂载 /init

这个阶段主要用于 内建的 RAMFS/Initramfs 文件系统的解压和挂载

有时候也用于设置默认 root device(比如挂载 root=/dev/mmcblk0p1

宏展开

我们来随机挑选一个宏进行展开,就用device_initcall

/* Format: <modname>__<counter>_<line>_<fn> */
#define __initcall_id(fn) \
__PASTE(__KBUILD_MODNAME, \
__PASTE(__, \
__PASTE(__COUNTER__, \
__PASTE(_, \
__PASTE(__LINE__, \
__PASTE(_, fn))))))

/* Format: __<prefix>__<iid><id> */
#define __initcall_name(prefix, __iid, id) \
__PASTE(__, \
__PASTE(prefix, \
__PASTE(__, \
__PASTE(__iid, id))))


#define __initcall_section(__sec, __iid) \
#__sec ".init"

#define ____define_initcall(fn, __unused, __name, __sec) \
static initcall_t __name __used \
__attribute__((__section__(__sec))) = fn;

#define __unique_initcall(fn, id, __sec, __iid) \
____define_initcall(fn, \
__initcall_stub(fn, __iid, id), \
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))

#define ___define_initcall(fn, id, __sec) \
__unique_initcall(fn, id, __sec, __initcall_id(fn))

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

#define device_initcall(fn) __define_initcall(fn, 6)

其中,我们假设:

  • fn = my_init
  • __KBUILD_MODNAME = mydriver (模块名)
  • _COUNTER_ = 0
  • _LINE_ = 0
device_initcall(my_init)
→ __define_initcall(my_init, 6)
→ ___define_initcall(my_init, 6, .initcall6)
→ __unique_initcall(my_init, 6, .initcall6, __initcall_id(my_init))
→ ____define_initcall(
my_init,
__initcall_stub(my_init, mydriver__0_123_my_init, 6),
__initcall_name(initcall, mydriver__0_123_my_init, 6),
__initcall_section(.initcall6, mydriver__0_123_my_init)
)
static initcall_t __initcall_initcall__mydriver__0_123_my_init6 __used
__attribute__((__section__(".initcall6.init"))) = my_init;

其中,initcall_t的定义如下,也就是一个返回值为int,参数为void的函数指针:

typedef int (*initcall_t)(void);

__initcall_initcall__mydriver__0_123_my_init6则为这个变量的名字。

__used的定义如下:

#define __used __attribute__((__used__))

因为这个变量不会被直接使用(如何使用下面讨论),为了防止编译器优化则使用该关键字。

__attribute__((__section__(".initcall6.init")))则是将该变量放入.initcall6.init段中。

linux去哪里调用这些initcall

前面说到通过xxx_init宏的函数,会被放到某一个段中,那么linux如何调用这个函数呢?

这些函数会在do_initcalls函数中被调用,完整过程:

start_kernel()
└── rest_init()
└── kernel_init()
└── do_basic_setup()
└── do_initcalls() ←🔹 关键函数

do_initcalls的关键代码如下:

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,
};

static void __init do_initcalls(void)
{
int level;

...

for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
...
do_initcall_level(level, command_line);
}

...
}

其中,initcall_levels保存着每一个initcall段的开始地址。

do_initcall_level的关键代码如下:

static void __init do_initcall_level(int level, char *command_line)
{
initcall_entry_t *fn;

...

trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}

trace开头的函数是用于调试的,可以忽略。

这里就是遍历某一个initcall的段,开始地址是initcall_levels[level],结束地址是initcall_levels[level + 1]也就是下一个段的地址。

这样就可以拿到该段中的每一个init函数地址了,最后通过do_one_initcall函数执行。

do_one_initcall关键代码如下:

int __init_or_module do_one_initcall(initcall_t fn)
{
...

do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);

...
return ret;
}

作为模块加载的initcall

作为模块加载所使用的initcall略有不同。

定义如下:

#define __initcall(fn) device_initcall(fn)

#ifndef MODULE
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);

/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);

#else /* MODULE */

/*
* In most cases loadable modules do not need custom
* initcall levels. There are still some valid cases where
* a driver may be needed early if built in, and does not
* matter when built as a loadable module. Like bus
* snooping debug drivers.
*/
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define core_initcall_sync(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define postcore_initcall_sync(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define subsys_initcall_sync(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define fs_initcall_sync(fn) module_init(fn)
#define rootfs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define device_initcall_sync(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define late_initcall_sync(fn) module_init(fn)

#define console_initcall(fn) module_init(fn)

/* 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))); \
___ADDRESSABLE(init_module, __initdata);

/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) \
__attribute__((alias(#exitfn))); \
___ADDRESSABLE(cleanup_module, __exitdata);

#endif

可以看到,如果MODULE宏被定义了,则module_init被定义成__initcall__initcall最后又被定义为了device_initcall,走的还是built-in initcall那一套。如果MODULE没被定义,就将所有的initcall都换为module_init,以免驱动的错误编译,此时module_init就是代码重定位那一套了,这个后面有时间再说。

那么MODULE这个宏又是什么时候被设置的呢?肯定不能静态设置,如果静态设置了,那么built-inko只能选择一个,很显然太纯了。

其实这个宏的定义和你的驱动是否作为模块 .ko 编译有关,由内核的构建系统 Kbuild 决定。

如果你写的是模块:

obj-m += xxx.o

那么Kbuild 会在编译 .ko 模块时自动添加一个编译宏定义:

-DMODULE

可以在make模块的时候加一个V=1,看看是否会被附加一个-DMODULECFLAGS,大概是这样:

gcc -Wp,-MD,drivers/my_driver/.my_driver.o.d  -DMODULE ...