在 Linux 内核中加载驱动程序(一)
大家好!我是大聪明-PLUS!
几乎每个人都知道如何为 Linux 编写一个简单的驱动程序。网上有很多关于这个主题的资料。但关于驱动程序加载过程“底层”的信息却很少。实际上很少有人需要它。但作者觉得这很有趣,这促使我写了这篇文章。这个主题相当广泛,所以本文只涵盖了一小部分。
本文并非学术文章,可能存在错误。作者秉持以下原则:与其写一篇绝对准确但永远无法帮助任何人的文章,不如写一篇有错误但能帮助到别人的文章。欢迎提出实质性意见。任何发现的错误都将予以纠正。
假设本文提供的所有信息都可以在开源文档或类似的在线文章中找到。实际上,我只能通过深入研究源代码才能弄清楚,而且也只是部分弄清楚。
关于内核文档的抒情题外话
研究设置
我们使用了 大聪明-PLUS 开发板和 Buildroot 2021.02。这个配置没什么特别的;这只是我手头上现有的,而且或多或少比较熟悉的东西。
在本例中,Buildroot 使用的内核版本是 4.19.79。现代内核版本可能存在一些细微的差异,但总体方法保持不变。
构建和下载过程很简单,但为了以防万一,我们会记录下来:
将 Buildroot 解压/下载到合适的目录;
在控制台中转到此目录;
我们组装图像:
make dacongming_defconfig
make linux-rebuild
make转到 output/images 目录,将 SD 卡插入读卡器,然后刷新:
sudo dd if=sdcard.img of=/dev/sda bs=4K
sync我们连接到板子的UART口,插入SD卡,给板子供电;
我们查看内核日志,发现信息不够;
向内核代码添加日志;
我们重复一遍,从重建图像开始。
加载过程
系统启动后,我们来看一下内核日志(dmesg)。很明显,驱动程序的加载开始得相当早,大概在以下这行代码附近:
[ 0.268291] OMAP GPIO hardware version 0.1
现在是时候深入研究源代码了。以下路径是相对于包含内核源代码的根目录的。
我们对init/main.c文件感兴趣:
asmlinkage __visible void __init start_kernel(void)
事实上,这是最重要的函数。但它包含的内容远不止这些。如果你搜索一下,就会发现我们感兴趣的地方就在这里:
/** Ok, the machine is now initialized. None of the devices* have been touched yet, but the CPU subsystem is up and* running, and memory and process management works.** Now we can finally start doing some real work..*/
static void __init do_basic_setup(void)
{cpuset_init_smp();shmem_init();driver_init();init_irq_proc();do_ctors();usermodehelper_enable();do_initcalls();
}
如果你在代码中添加一些额外的日志记录(或者只是稍微聪明一点,但这里不是这样),就会清楚地看到,驱动程序的加载是在调用 do_initcalls() 之后开始的。那么,让我们来看看:
static void __init do_initcalls(void)
{int level;for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)do_initcall_level(level);
}static void __init do_initcall_level(int level)
{initcall_entry_t *fn;strcpy(initcall_command_line, saved_command_line);parse_args(initcall_level_names[level],initcall_command_line, __start___param,__stop___param - __start___param,level, level,NULL, &repair_env_string);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));
}
因此,我们有一个 initcalls 数组,我们用特定的方式对其进行迭代。它在同一个文件中声明:
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];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,
};/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {"pure","core","postcore","arch","subsys","fs","device","late",
};
它看起来像一个指向函数的指针数组,我们可以通过某种复杂的方式调用这些函数。但是这些指针从何而来?如果你尝试在源代码中搜索类似 __initcall0_start 的名称,你只会找到这个:
arch/arm/kernel/vmlinux.lds:
.init.data : AT(ADDR(.init.data) - 0) { KEEP(*(SORT(___kentry+*))) *(.init.data init.data.*) *(.meminit.data*) *(.init.rodata .init.rodata.*) . = ALIGN(8); __start_ftrace_events = .; KEEP(*(_ftrace_events)) __stop_ftrace_events = .; __start_ftrace_eval_maps = .; KEEP(*(_ftrace_eval_map)) __stop_ftrace_eval_maps = .; . = ALIGN(8); __start_kprobe_blacklist = .; KEEP(*(_kprobe_blacklist)) __stop_kprobe_blacklist = .; *(.meminit.rodata) . = ALIGN(8); __clk_of_table = .; KEEP(*(__clk_of_table)) KEEP(*(__clk_of_table_end)) . = ALIGN(8); __reservedmem_of_table = .; KEEP(*(__reservedmem_of_table)) KEEP(*(__reservedmem_of_table_end)) . = ALIGN(8); __timer_of_table = .; KEEP(*(__timer_of_table)) KEEP(*(__timer_of_table_end)) . = ALIGN(8); __cpu_method_of_table = .; KEEP(*(__cpu_method_of_table)) KEEP(*(__cpu_method_of_table_end)) . = ALIGN(8); __cpuidle_method_of_table = .; KEEP(*(__cpuidle_method_of_table)) KEEP(*(__cpuidle_method_of_table_end)) . = ALIGN(32); __dtb_start = .; KEEP(*(.dtb.init.rodata)) __dtb_end = .; . = ALIGN(8); __irqchip_of_table = .; KEEP(*(__irqchip_of_table)) KEEP(*(__irqchip_of_table_end)) . = ALIGN(8); __earlycon_table = .; KEEP(*(__earlycon_table)) __earlycon_table_end = .; . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .; __initcall_start = .; KEEP(*(.initcallearly.init)) __initcall0_start = .; KEEP(*(.initcall0.init)) KEEP(*(.initcall0s.init)) __initcall1_start = .; KEEP(*(.initcall1.init)) KEEP(*(.initcall1s.init)) __initcall2_start = .; KEEP(*(.initcall2.init)) KEEP(*(.initcall2s.init)) __initcall3_start = .; KEEP(*(.initcall3.init)) KEEP(*(.initcall3s.init)) __initcall4_start = .; KEEP(*(.initcall4.init)) KEEP(*(.initcall4s.init)) __initcall5_start = .; KEEP(*(.initcall5.init)) KEEP(*(.initcall5s.init)) __initcallrootfs_start = .; KEEP(*(.initcallrootfs.init)) KEEP(*(.initcallrootfss.init)) __initcall6_start = .; KEEP(*(.initcall6.init)) KEEP(*(.initcall6s.init)) __initcall7_start = .; KEEP(*(.initcall7.init)) KEEP(*(.initcall7s.init)) __initcall_end = .; __con_initcall_start = .; KEEP(*(.con_initcall.init)) __con_initcall_end = .; __security_initcall_start = .; KEEP(*(.security_initcall.init)) __security_initcall_end = .; . = ALIGN(4); __initramfs_start = .; KEEP(*(.init.ramfs)) . = ALIGN(8); KEEP(*(.init.ramfs.info)) }
也就是说,我们在链接脚本中为这个数组分配了空间,但是如何填充这个数组却不清楚。
好的,那么让我们回到引用include/linux/init.h 的注释:
#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)
在同一文件中,稍微高一点的内容是:
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \__ADDRESSABLE(fn) \asm(".section \"" #__sec ".init\", \"a\" \n" \"__initcall_" #fn #id ": \n" \".long " #fn " - . \n" \".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(#__sec ".init"))) = fn;
#endif#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
这一点非常重要。如果你在源代码中搜索类似 device_initcall 的内容,你会发现大多数驱动程序都将它们隐藏在诸如 builtin_driver 之类的定义之下。事实上,这个定义本身就在许多驱动程序中使用。在某些地方,也会使用较旧的 __initcall。
换句话说,系统将每个驱动程序的初始化函数添加到 initcall 列表中,并在启动时调用它。该函数负责将驱动程序注册到系统中。
本文对 initcalls 机制的讨论到此结束。因为它在驱动程序加载方面的作用到此为止。同时,该机制本身也相当有趣。
module_init 的定义值得单独看一下。它在kernel/module.h中的定义如下:
#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)
#define security_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)));/* 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)));#endif
也就是说,如果我们将此文件构建为外部模块(.ko 文件),我们定义 MODULE,并且我们的函数不会包含在 initcalls 数组中。init_module() 函数会在模块加载时被调用(详见下文)。但是,如果我们将此文件构建为内核模块,则 MODULE 的定义不会被定义,并且它的 init 函数会包含在 initcalls 数组中。
关于模块的证明
*.ko模块加载机制
内核模块(编译为单独文件)的加载通常在用户空间进行。通常使用以下机制之一:
用户在控制台或初始化脚本中调用 insmod/modprobe 实用程序。
当检测到匹配的设备时,内核会生成一个 uevent 事件,该事件会被 udev 守护进程(对于嵌入式系统,则为 mdev)接收。在这种情况下,守护进程会执行与 modprobe 实用程序相同的操作。
如果我们查看 insmod 代码(为了方便起见,我们将使用 busybox 包中的版本),我们将看到主要操作:
busybox/modutils/insmod.c:
rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));
busybox/modutils/modutils.c:
int FAST_FUNC bb_init_module(const char *filename, const char *options)
{
…/** First we try finit_module if available. Some kernels are configured* to only allow loading of modules off of secure storage (like a read-* only rootfs) which needs the finit_module call. If it fails, we fall* back to normal module loading to support compressed modules.*/
# ifdef __NR_finit_module{int fd = open(filename, O_RDONLY | O_CLOEXEC);if (fd >= 0) {rc = finit_module(fd, options, 0) != 0;close(fd);if (rc == 0)return rc;}}
# endifimage_size = INT_MAX - 4095;mmaped = 0;image = try_to_mmap_module(filename, &image_size);if (image) {mmaped = 1;} else {errno = ENOMEM; /* may be changed by e.g. open errors below */image = xmalloc_open_zipped_read_close(filename, &image_size);if (!image)return -errno;}errno = 0;init_module(image, image_size, options);
简而言之,代码可以归结为:如果内核支持 finit_module 系统调用,我们只需打开文件并调用此调用,并将文件描述符传递给它。如果内核不支持,我们将模块加载到内存中,并调用 init_module 系统调用,并将指向内存映像开头的指针传递给它。
无论如何,我们都会转到内核文件kernel/module.c。根据上述代码的分支,我们最终会到达这里:
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{struct load_info info = { };loff_t size;void *hdr;int err;err = may_init_module();if (err)return err;pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags);if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS|MODULE_INIT_IGNORE_VERMAGIC))return -EINVAL;err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX,READING_MODULE);if (err)return err;info.hdr = hdr;info.len = size;return load_module(&info, uargs, flags);
}
或者在这里:
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);
}
有趣的是,在这两种情况下,我们最终都会进入 load_module() 函数。这个函数会执行大量的检查和准备工作,但最终,如果一切顺利,我们会得到这个调用:
return do_init_module(mod);
反过来,do_init_module() 函数可以归结为这些操作的包装器:
/* Start the module */if (mod->init != NULL)ret = do_one_initcall(mod->init);if (ret < 0) {goto fail_free_freeinit;
换句话说,我们以完全相同的方式提取 initcall,只是在这种情况下它没有内置到系统初始化期间执行的 initcalls 链中。