---- 整理自 王利涛老师 课程
实验环境:宅学部落 www.zhaixue.cc
文章目录
1. 可加载模块
1.1 Linux内核的模块机制
- LKM:Loadable Kernel Module
- 内核模块化、高度可定制化和可裁剪
- 适配不同的架构、硬件平台
- 支持运行时动态加载或卸载一个模块
- 不需要重新编译、重启内核
1.2 实验:hello 模块
- 目标:一个内核模块的编译和运行,动态加载和卸载
- 新建 hello.c 文件,内容如下:
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void) {
printk("Hello world\n");
return 0;
}
static void __exit hello_exit(void) {
printk("Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
- M=$(PWD):需要编译的模块源文件地址
-C
选项:此选项指定内核源码的位置,make 在编译时将会进入内核源码目录,执行编译,编译完成时返回。
1.3 内核模块的构成
- hello_init
- 模块加载入口函数,主要完成模块初始化工作
- 使用 __init 声明,使用 module_init 指定
- 模块被加载到内核时,入口函数自动被内核执行
- 返回值:errno
- 应用层可根据返回值,使用 perror 进行解析
- hello_exit
- 模块卸载函数,模块卸载时该函数自动被内核执行
- 使用 __exit 声明,使用 module_exit 指定
- 主要完成结束模块运行的相关工作、清理各种资源
- 返回类型:void
- insmod
- lsmod
- rmmod
2. 内核许可声明
- 用来描述内核的许可权限:内核以 GPL 发布
- 模块不声明 LICENSE,内核会有 (kernel tainted) 警告
- 内核状态此时是 受污染的(dirty)
- 内核受污染后,一些调试、打印功能可能会失效
2.1 协议分类
2.2 内核污染(kernel tainted)
2.2.1 cat /proc/sys/kernel/tainted
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void) {
printk("Hello world\n");
return 0;
}
static void __exit hello_exit(void) {
printk("Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
// MODULE_LICENSE("GPL"); // 模块不声明 LICENSE
MODULE_AUTHOR("uuxiang");
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
- 模块不声明 LICENSE,内核会有 (kernel tainted) 警告
2.2.2 内核被污染的原因
3. 模块签名机制
- 内核编译时配置打开如下选项:
make menuconfig
CONFIG_MODULE_SIG
CONFIG_MODULE_SIG_FORCE
CONFIG_MODULE_SIG_ALL
- 未签名的 hello.ko
- 查看签名需要的文件:
- certs/signing_key.x509 公钥
- certs/signing_key.pem 私钥
- scripts/sign-file 签名工具
- 手工给模块签名
- 补充:清除模块签名
4. 两种编译方式
4.1 将模块编译进内核
进入如下目录 drivers/char
,新建 hello.c
文件(内容同上),并修改 Kconfig
和 Makefile
// linux-5.10.4\drivers\char\hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk("Hello world\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
# linux-5.10.4\drivers\char\Kconfig
...
config HELLO
tristate "A hello module test"
default m
help
a simple kernel module test
...
# linux-5.10.4\drivers\char\Makefile
...
obj-${CONFIG_HELLO} += hello.o
...
运行新的内核,从开机 log 中打印我们 hello 模块中的信息:
4.2 out-of-tree 编译
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
4.3 模块的 Makefile 分析
- Kbuild 示例:
obj-y := hello.o # Static Build
obj-m := hello.o # Module Build
obj-{CONFIG_HELLO} := hello.o # Conditional Build
hello-objs := hello.c sub.c # Multi-File Object
- 示例:将 Kbuild 和 Makefile 分开:
# Kbuild
obj-m := hello.o
ifneq ($(KERNELRELEASE),)
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
5. 模块参数
5.1 如何给模块传参数?
#define module_param(name, type, perm) module_param_named(name, name, type, perm)
- name:要传递的参数,对应模块中的全局变量
- type:要传递的参数类型,要和全局变量类型一致
- perm:读写权限
/sys/module/hello/parameters/xx
参数节点0666
:读写权限0444
:只读权限,无法对这个文件节点执行写的操作- 4 - 读,2 - 写,1 - 执行
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
static int num = 10;
module_param(num, int, 0660);
static int __init hello_init(void)
{
printk("Hello world\n");
printk("num = %d\n", num);
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye world\n");
printk("num = %d\n", num);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
5.2 通过 uboot 给模块传参
- 需要使用编译进内核中的 hello 模块
// linux-5.10.4\drivers\char\hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
static int num = 10;
module_param(num, int, 0660);
static int __init hello_init(void)
{
printk("Hello world\n");
printk("num = %d\n", num);
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye world\n");
printk("num = %d\n", num);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
- 编译 uboot 时,make menuconfig,在
bootargs
中添加 hello.num = 123
- 完整开机 log 如下:
6. EXPORT_SYMBOL
6.1 用户空间的模块化编程
函数的实现:math.c/int add(int a, int b)
函数的声明:math.h/int add(int a, int b);
#ifndef __MATH_H
#define __MATH_H
int add(int a, int b);
int sub(int a, int b);
#endif
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
#include <stdio.h>
#include "math.h"
int main(void)
{
int sum = 0;
sum = add(3, 4);
printf("sum = %d\n", sum);
return 0;
}
6.2 内核空间的模块化编程
- 模块的封装:static、EXPORT_SYMBOL
- 函数的声明:头文件
- 按不同的协议导出符号
- EXPORT_SYMBOL
- EXPORT_SYMBOL_GPL
6.3 编程实验:hello 模块调用 math 模块
KBUILD_EXTRA_SYMBOLS
用来告诉内核当前 module 需要引用另外一个 module 导出的符号
# drivers\05export_symbol\hello\Makefile
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
EXTRA_CFLAGS += -DDEBUG
# KBUILD_EXTRA_SYMBOLS 用来告诉内核当前module需要引用另外一个module导出的符号
KBUILD_EXTRA_SYMBOLS += /home/code_folder/uboot_linux_rootfs/kernel/drivers/05export_symbol/math/Module.symvers
export KBUILD_EXTRA_SYMBOLS
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
#include <linux/init.h>
#include <linux/module.h>
extern int num;
static void hello_probe(void)
{
extern int add(int a, int b);
int sum = add(3, 4);
printk("sum = %d\n", sum);
}
static int __init hello_init(void)
{
printk("num = %d\n", num);
hello_probe();
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye hello_module\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
# drivers\05export_symbol\math\Makefile
ifneq ($(KERNELRELEASE),)
obj-m := math.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
#include <linux/init.h>
#include <linux/module.h>
int num = 10;
module_param(num, int, 0660);
EXPORT_SYMBOL(num);
int add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add);
static int __init math_init(void)
{
printk("Hello math_module\n");
return 0;
}
static void __exit math_exit(void)
{
printk("Goodbye math_module\n");
}
module_init(math_init);
module_exit(math_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
9. 模块的版本控制
- 解决内核模块和内核之间的接口一致性问题
- 根据函数参数、返回值类型等生成 CRC 校验码
- 当内核和模块双方的校验码相等,则为相同接口
- 内核启动版本控制功能:CONFIG_MODVERSIONS
9.1 实验:版本控制实验
- 使用打开 CONFIG_MODVERSIONS 的内核和不开 CONFIG_MODVERSIONS 编译内核后编译的 hello.ko,进行版本控制的实验。
9.2 分析
- 相关的几个文件
hello.mod.c
hello.ko:__versions section
内核中:Modules.symvers
模块中:Modules.symvers
9.2.1 未开 CONFIG_MODVERSIONS
// hello.mod.c
#include <linux/module.h>
#define INCLUDE_VERMAGIC
#include <linux/build-salt.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>
BUILD_SALT;
MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);
__visible struct module __this_module
__section(".gnu.linkonce.this_module") = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
#ifdef CONFIG_RETPOLINE
MODULE_INFO(retpoline, "Y");
#endif
MODULE_INFO(depends, "");
内核中 Modules.symvers:
模块中 Modules.symvers:
9.2.2 打开 CONFIG_MODVERSIONS
hello.ko 模块中调用的内核中的接口函数以及 CRC 校验码,当模块被加载到内核中运行的时候,内核就会 根据调用的比如 printk 的校验码和内核中保存的 printk 的校验码进行比对,比对成功,模块才可以加载到内核中运行。
- 内核中 Modules.symvers:
反汇编:
我们自己编译的模块 hello.ko,使用 EXPORT_SYMBOL 导出的时候,也会生成校验码,别的模块在调用时也会进行校验。
10. 模块的头文件
#include <linux/xx.h>
#include <asm/xx.h>
#include <plat/xx.h>
#include <mach/xx.h>
#include "usb.h"
-
头文件分类
- 内核专用头文件:include/linux
- 和CPU架构相关:arch/$(ARCH)/include
- 板级硬件相关:
arch/$(ARCH)/plat-xx/include
arch/$(ARCH)/mach-xx/include
-
通过 gcc -l 指定头文件路径
#include <module1/module1.h>
#include <module2/module2.h>
#include <module3/module3.h>
├── a.out
├── inc
│ ├── module1
│ │ └── module1.h
│ ├── module2
│ │ └── module2.h
│ └── module3
│ └── module3.h
├── main.c
├── module1
│ └── module1.c
├── module2
│ └── module2.c
└── module3
└── module3.c
- 内核中的头文件路径
11. 多文件构成的模块
- 编程实验:
- 一个复杂模块往往由多个 C 文件构成
- 模块内部接口的封装和引用
- 模块如何封装
- 模块间如何引用
- 头文件
- Makefile 的写法
ifneq ($(KERNELRELEASE),)
obj-m := files_in_hello.o
files_in_hello-objs := main.o add.o sub.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
12. 模块间的依赖
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif
#include <linux/init.h>
#include <linux/module.h>
int add(int a, int b) {
return a + b;
}
EXPORT_SYMBOL(add);
static int __init add_init(void) {
printk("add module init\n");
return 0;
}
static void __exit add_exit(void) {
printk("add module exit\n");
}
module_init(add_init);
module_exit(add_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
#include <linux/init.h>
#include <linux/module.h>
int sub(int a, int b) {
return a - b;
}
EXPORT_SYMBOL(sub);
static int __init sub_init(void) {
printk("sub module init");
return 0;
}
static void __exit sub_exit(void) {
printk("sub module exit\n");
}
module_init(sub_init);
module_exit(sub_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
#include <linux/init.h>
#include <linux/module.h>
#include "add.h"
#include "sub.h"
static int __init hello_init(void) {
int result;
int sum;
printk("hello module init\n");
sum = add(3, 4);
printk("sum = %d\n", sum);
result = sub(5, 3);
printk("result = %d\n", result);
return 0;
}
static void __exit hello_exit(void) {
printk("hello module exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuxiang");
ifneq ($(KERNELRELEASE),)
obj-m := dep_modules.o
obj-m += add.o
obj-m += sub.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
- 先加载依赖的 ko,再加载 dep_modules.ko ,卸载顺序相反。
- 生成模块间的依赖关系:
depmod -a
depmod 会解析/lib/modules/$(kernel_version)
下的所有内核模块,通过各个模块 EXPORT_SYMBOL 和引用的符号,生成一个 模块依赖关系表:/lib/modules/$(kernel_version)/modules.dep.bb
modprobe hello # 加载
modprobe –r hello # 卸载
13. 编写一个字符驱动
read | write |
---|---|
sys_read | sys_write |
普通文件 | 设备文件 |
文件系统 | 设备模型 |
inode | 设备号 |
块设备驱动 | 字符设备驱动 |
- 编程实验:实现一个最简单的字符驱动
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
static char hello_buf[512] = "0";
static int hello_open(struct inode *inode, struct file *fp)
{
return 0;
}
static int hello_release(struct inode *inode, struct file *fp)
{
return 0;
}
static ssize_t hello_read(struct file *fp, char __user *buf,
size_t size, loff_t *pos)
{
unsigned long p = *pos;
unsigned int count = size;
if (p >= 512) {
return -1;
}
if (count > 512) {
count = 512 - p;
}
if (copy_to_user(buf, hello_buf + p, count) != 0) {
printk("read error!\n");
return -1;
}
return count;
}
static ssize_t hello_write(struct file *fp, const char __user *buf,
size_t size, loff_t *pos)
{
unsigned long p = *pos;
unsigned int count = size;
if (p >= 512) {
return -1;
}
if(count > 512) {
count = 512 - p;
}
if(copy_from_user(hello_buf, buf + p, count) != 0) {
printk("write error!\n");
return -1;
}
return count;
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.read = hello_read,
.write = hello_write,
.open = hello_open,
.release = hello_release,
};
static int __init hello_init(void)
{
int ret;
ret = register_chrdev(222, "hello", &hello_fops);
if (ret < 0) {
printk("Register char module: hello_char failed..\n");
return 0;
} else {
printk("Register char module: hello_char success!\n");
}
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye char module: hello_char!\n");
unregister_chrdev(222, "hello_char");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuuuu");
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int fd;
int i, ret;
char write_buf[10] = "aaaabbbb";
char read_buf[10] = "0";
fd = open("/dev/hello", O_RDWR);
if(fd == -1) {
printf("cannot open file..\n");
exit(1);
}
if ((ret = write(fd, write_buf, 10)) < 0) {
printf("write failed\n");
return -1;
}
lseek(fd, 0, SEEK_SET);
if ((ret = read(fd, read_buf, 10)) < 10) {
printf("read error!\n");
exit(1);
}
printf("%s\n", read_buf);
close(fd);
return 0;
}
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
EXTRA_CFLAGS += -DDEBUG
KDIR := /home/code_folder/uboot_linux_rootfs/kernel/linux-5.10.4
ARCH_ARGS := ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
all:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules
clean:
make $(ARCH_ARGS) -C $(KDIR) M=$(PWD) modules clean
endif
14. 模块的运行过程
- 模块的分类
- 可加载模块:源码外编译,动态加载、动态卸载
- 内置模块:直接编译进内核,随内核启动初始化
- 使用 dump_stack() 打印函数调用栈
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk("Hello world\n");
dump_stack();
return 0;
}
static void __exit hello_exit(void)
{
printk("Goodbye world\n");
dump_stack();
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("uuuuu");
15. 模块机制实现分析
- 分析之前的准备
C 语言与链接脚本、Makefile、Kbuild 的交互
C 语言如何引用链接脚本中定义的符号
C 语言如何使用 Makefile 中定义的符号
C 语言如何使用 kbuild 配置变量
hello.c
|--> module_init(hello_init);
linux-5.10.4/include/linux/module.h
|--> #define module_init(x) __initcall(x);
linux-5.10.4/include/linux/init.h
|--> #define __initcall(fn) device_initcall(fn)
|--> #define device_initcall(fn) __define_initcall(fn, 6)
|--> #define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
|--> #define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
linux-5.10.4/arch/arm/kernel/vmlinux.lds
|--> __initcall6_start = .; KEEP(*(.initcall6.init))
hello.c
|--> module_init(hello_init);
linux-5.10.4/include/linux/module.h
|--> __initcall(hello_init);
linux-5.10.4/include/linux/init.h
|--> device_initcall(hello_init)
|--> __define_initcall(hello_init, 6)
|--> ___define_initcall(hello_init, 6, .initcall6)
|--> static initcall_t __initcall_hello_init6 __used \
__attribute__((__section__(".initcall6.init"))) = hello_init;
// hello_init 放在 .initcall6.init 段中
linux-5.10.4/arch/arm/kernel/vmlinux.lds
|--> __initcall6_start = .; KEEP(*(.initcall6.init))
15.1 attribute 和 section
- 补充一下
__attribute__
机制,GNU C 的一大特色就是__attribute__
机制。__attribute__
可以设置函数属性、变量属性和类型属性,也就是通过给函数或者变量声明属性值,以便让编译器能够对要编译的程序进行优化处理。具体可自行查阅。 - 对于 section 这个关键字,我们可以通过它将指定的变量定义到指定的段中。示例如下:
attribute((section(“section_name”)))
// 其作用是将作用的函数或数据放入指定的名为"section_name"对应的段中。
15.2 分析 hello_init
static initcall_t __initcall_hello_init6 __used __attribute__((__section__(".initcall6.init"))) = hello_init;
/*
精简一下就是下面这样,类型为initcall_t的static的变量__initcall_hello_init6,被初始化为hello_init
*/
static initcall_t __initcall_hello_init6 = hello_init;
// 剩余的__used,原型如下:
#define __used __attribute__((__used__)) //attribute((used))是GCC编译器的一个指令,
// 用来告诉编译器一个变量或函数可能会被用到,避免编译器在优化代码时将其删去。
// 一般情况下,编译器会删去未被使用过的变量或函数,以减少代码大小和提高程序效率。
/*
剩余的__attribute__((__section__(".initcall6.init"))):
将作用的变量__initcall_hello_init6放入到 .initcall6.init段中
*/
15.3 分析 module_init 调用流程
15.3.1 整体流程
linux-5.10.4\init\main.c
|--> start_kernel(void)
|--> arch_call_rest_init(void)
|--> rest_init(void)
|--> pid = kernel_thread(kernel_init, NULL, CLONE_FS);
|--> kernel_init(void *unused)
|--> kernel_init_freeable(void)
|--> do_basic_setup(void)
|--> do_initcalls(void)
|--> do_initcall_level(level, command_line)
|--> do_one_initcall(initcall_from_entry(fn))
15.3.2 do_initcalls
static void __init do_initcalls(void)
{
int level;
size_t len = strlen(saved_command_line) + 1;
char *command_line;
command_line = kzalloc(len, GFP_KERNEL);
if (!command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
// 遍历数组initcall_levels,每次执行do_initcall_level
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
/* Parser modifies command_line, restore it each time */
strcpy(command_line, saved_command_line);
do_initcall_level(level, command_line);
}
kfree(command_line);
}
上述代码 do_initcalls 中的 initcall_level 数组如下:
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,
};
数组中的 __initcall6_start 是不是有点眼熟,和我们前面分析的 linux-5.10.4/arch/arm/kernel/vmlinux.lds
有关:
linux-5.10.4/arch/arm/kernel/vmlinux.lds
|--> __initcall6_start = .; KEEP(*(.initcall6.init))
15.3.3 do_initcall_level
do_initcalls 中的 do_initcall_level 函数如下:
static void __init do_initcall_level(int level, char *command_line)
{
initcall_entry_t *fn;
parse_args(initcall_level_names[level],
command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, ignore_unknown_bootoption);
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));
}
对于当前的这个 level 优先级,通过 fn 遍历数组 initcall_levels[level],例如 对于level6,这里就会遍历 initcall_levels[6],也就是__initcall6_start。
15.3.4 do_one_initcall(initcall_from_entry(fn))
fn 就是 __initcall6_start 中的元素,根据链接脚本(如下所示),从而获取到 hello_init 来执行。
linux-5.10.4/arch/arm/kernel/vmlinux.lds
|--> __initcall6_start = .; KEEP(*(.initcall6.init))
// linux-5.10.4\include\linux\init.h
static inline initcall_t initcall_from_entry(initcall_entry_t *entry)
{
return *entry;
}
// linux-5.10.4\init\main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
...
ret = fn();
...
}