模块机制让kernel有伸缩性,既保有宏内核的高效,又有一定微内核的稳定性。
linux-5.16.2# touch drivers/char/hello.c
#include <linux/init.h> #include <linux/modules.h> static int __init hello_init(void) { printk("hello world\n"); return 0; } static void __exit hello_exit(void) { } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
将模块加入Kbuild
vi drivers/char/Kconfig
config HELLO tristate "hello demo" help demo for module
vi driver/char/Makefile
obj-$(CONFIG_HELLO) += hello.o
make menuconfig
将hello设置为M
make modules
将ko放到modules目录
mv hello.ko /lib/modules/5.16.2
加载模块
modprobe hello
卸载模块
rmmod hello
准备单独目录
root@ubuntu:~/wlt/build/my/hello# ls
hello.c Makefile
cat Makefile
.PHONY: all clean obj-m := hello.o CROSS_COMPILE := arm-linux-gnueabi- KBUILD_DIR := /root/wlt/build/linux-5.16.2 all: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean
.PHONY: all clean CROSS_COMPILE := arm-linux-gnueabi- KBUILD_DIR := /root/wlt/build/linux-5.16.2 all: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean
obj-m := hello.o
若是多个目标文件构成一个模块,则这样写
.PHONY: all clean obj-m := hello.o hello-objs := main.o add.o sub.o CROSS_COMPILE := arm-linux-gnueabi- KBUILD_DIR := /root/wlt/build/linux-5.16.2 all: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean
使用 module_param 导出参数
module_param(name, type, perm)
在main.c加入
#include <linux/moduleparam.h> static int num; module_param(num, int, 0664);
module_param(name, type, perm)
perm不能为 0666
模块加载后,可以在 /sys/module/hello/parameters/ 下 读写参数值
可以通过uboot给模块传参,bootargs 添加 'hello.num=111'
使用 EXPORT_SYMBOL(sym) 导出符号
构造math模块
#include <linux/module.h> #include <linux/init.h> #include <linux/moduleparam.h> int math_add(int a, int b) { return a + b; } EXPORT_SYMBOL(math_add); int math_sub(int a, int b) { return a - b; } EXPORT_SYMBOL(math_sub); static int __init math_init(void) { return 0; } module_init(math_init); MODULE_LICENSE("GPL");
math编译成功后会生成
root@ubuntu:~/wlt/build/my/math# cat Module.symvers
0x00000000 math_sub /root/wlt/build/my/math/math EXPORT_SYMBOL
0x00000000 math_add /root/wlt/build/my/math/math EXPORT_SYMBOL
构造 hello 模块,引用math的 add
#include <linux/module.h> #include <linux/init.h> #include <linux/moduleparam.h> extern int math_add(int a, int b); static int a; static int b; module_param(a, int, 0664); module_param(b, int, 0664); static int __init hello_init(void) { printk("a + b : %d\n", math_add(a, b)); return 0; } static void __exit hello_exit(void) { printk("hello exit\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
hello的Makefile 需要添加 match 的 导出符号表
ifneq ($(KERNELRELEASE),) obj-m := hello.o hello-objs := main.o else .PHONY: all clean KBUILD_EXTRA_SYMBOLS += /root/wlt/build/my/math/Module.symvers export KBUILD_EXTRA_SYMBOLS CROSS_COMPILE := arm-linux-gnueabi- KBUILD_DIR := /root/wlt/build/linux-5.16.2 all: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean: make -C $(KBUILD_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules clean endif
将模块放到 /lib/modules/${KVERSION}/
生成模块依赖
depmod -a
自动加载
modprobe hello
自动卸载
modprobe -r hello
一个字符设备驱动
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #define DEV_MAJOR 222 #define DEV_NAME "hello" static char hello_buf[256]; static ssize_t hello_read (struct file *fp, char __user *ubuf, size_t size, loff_t *pos) { unsigned long p = *pos; unsigned long count = size; if (p >= 256) return -1; if (count > 256 - p) count = 256 - p; return copy_to_user(ubuf, hello_buf + p, count); } static ssize_t hello_write (struct file *fp, const char __user *ubuf, size_t size, loff_t *pos) { unsigned long p = *pos; unsigned long count = size; if (p >= 256) return -1; if (count > 256 - p) count = 256 - p; return copy_from_user((char *)hello_buf + p, ubuf, count); } 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 struct file_operations hello_ops = { .owner = THIS_MODULE, .read = hello_read, .write = hello_write, .open = hello_open, .release = hello_release }; static int __init hello_init(void) { // 静态分配 主设备号, // 绑定 主设备号 和 设备名 和 fops // 如此,可通过 文件系统 找到 主设备号,进而找到 驱动 if (register_chrdev(DEV_MAJOR, DEV_NAME, &hello_ops) < 0) { printk("Failed to register_chrdev"); return -1; } return 0; } static void __exit hello_exit(void) { unregister_chrdev(DEV_MAJOR, DEV_NAME); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
测试程序
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd; char buf[256]; if ((fd = open("/dev/hello", O_RDWR)) < 0) { perror("open"); return -1; } if (write(fd, "hello", 5) < 0) { perror("write"); return -1; } if (read(fd, buf, sizeof(buf)) < 0) { perror("read"); return -1; } printf("read : %s\n", buf); close(fd); return 0; }
测试
加载驱动后,创建设备节点
mknod /dev/hello c 222 0
运行测试程序
yangxr@vexpress:/root # ./hello_test
read : hello
[<8010f358>] (unwind_backtrace) from [<8010b254>] (show_stack+0x10/0x14) [<8010b254>] (show_stack) from [<80836b74>] (dump_stack_lvl+0x40/0x4c) [<80836b74>] (dump_stack_lvl) from [<7f00500c>] (hello_init+0xc/0x1000 [hello]) [<7f00500c>] (hello_init [hello]) from [<80101f90>] (do_one_initcall+0x48/0x1e8) [<80101f90>] (do_one_initcall) from [<808313b0>] (do_init_module+0x54/0x208) [<808313b0>] (do_init_module) from [<801aca54>] (load_module+0x2058/0x2670) [<801aca54>] (load_module) from [<801ad1c4>] (sys_init_module+0x158/0x170) [<801ad1c4>] (sys_init_module) from [<80100060>] (ret_fast_syscall+0x0/0x54)
关键是三个函数
load_module : 检查模块信息,加载模块,重定位代码,构造 mod 对象
do_init_module:执行mod对象的init函数,删除 __init 段
do_one_initcall:调用 init 函数
1287 int __init_or_module do_one_initcall(initcall_t fn) 1288 { 1295 ..... 1297 ret = fn(); 3713 static noinline int do_init_module(struct module *mod) 3714 { ..... 3733 if (mod->init != NULL) 3734 ret = do_one_initcall(mod->init);
要调用init函数,必须构造 mod 对象。
模块编译时 生成 mod 对象
root@ubuntu:~/wlt/build/my/hello# cat hello.mod.c ... __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, }; ...
定义 mod 对象 在 .gnu.linkonce.this_module 段,
所以 模块加载时,是从 .gnu.linkonce.this_module段 加载 mod 对象,
而 .init = init_module,说明实际的 init 函数 符号名为 init_module
129 /* Each module must use one module_init(). */ 130 #define module_init(initfn) \ 131 static inline initcall_t __maybe_unused __inittest(void) \ 132 { return initfn; } \ 133 int init_module(void) __copy(initfn) \ 134 __attribute__((alias(#initfn))); \ 135 __CFI_ADDRESSABLE(init_module, __initdata);
可见 module_init宏的作用是 定义一个 函数 init_module,并将 initfn的代码复制给
init_module,并区别名。
所以 module_init(hello_init)后, init_module 就是 hello_init。
insmod命令会加载模块到内存,对模块进行合法性检查,并进行重定位,
由于 mod 对象位于 .gnu.linkonce.this_module 段,可以直接使用 mod 对象,
调用 mod->init 函数完成 模块初始化,释放 __init 段的内存。
module_init定义
include/linux/module.h
#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中定义
KBUILD_CFLAGS_KERNEL := KBUILD_CFLAGS_MODULE := -DMODULE k
所以若 编译的是 module 则定义 MODULE,否则不定义,
这里是内嵌内核,所以module_init的定义是
#define module_init(x) __initcall(x);
__initcall的定义是
253 #define __unique_initcall(fn, id, __sec, __iid) \ 254 ____define_initcall(fn, \ 255 __initcall_stub(fn, __iid, id), \ 256 __initcall_name(initcall, __iid, id), \ 257 __initcall_section(__sec, __iid)) 259 #define ___define_initcall(fn, id, __sec) \ 260 __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) #define __initcall(fn) device_initcall(fn)
所以__initcall最终展开为 ____define_initcall,
而____define_initcall定义
239 #ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 240 #define ____define_initcall(fn, __stub, __name, __sec) \ 241 __define_initcall_stub(__stub, fn) \ 242 asm(".section \"" __sec "\", \"a\" \n" \ 243 __stringify(__name) ": \n" \ 244 ".long " __stringify(__stub) " - . \n" \ 245 ".previous \n"); \ 246 static_assert(__same_type(initcall_t, &fn)); 247 #else 248 #define ____define_initcall(fn, __unused, __name, __sec) \ 249 static initcall_t __name __used \ 250 __attribute__((__section__(__sec))) = fn; 251 #endif
CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 的定义在 include/generated/autoconf.h(就是Kbuild生成的)。
我们看汇编(因为汇编更清晰)
240 #define ____define_initcall(fn, __stub, __name, __sec) \ 241 __define_initcall_stub(__stub, fn) \ 242 asm(".section \"" __sec "\", \"a\" \n" \ // 定义段 243 __stringify(__name) ": \n" \ // 在该段定义一个符号 244 ".long " __stringify(__stub) " - . \n" \ // 这个符号对应的空间为long大小,值为 __stringify(__stub) - . 245 ".previous \n"); \ // 使用以前的段(本段定义结束) 其中 __stringify(__stub) - . 为 函数地址 - 段起始地址,即这里保存一个偏移地址。 通过此偏移地址和段起始地址就能找到 函数地址。
示例
module_init(hello_init); __initcall(hello_init); device_initcall(hello_init, 6); __define_initcall(hello_init, 6); ___define_initcall(hello_init, 6, .initcall6); .section ".initcall6.init", "a" __initcall_hello_init6: .long hello_init - . .previous 在 段 .initcall6.init 定义一个符号 __initcall_hello_init6,值为 偏移地址(函数地址 - 段起始地址)。
总结:
module_init(fn)
定义 一个偏移值,定义在 特定的段 .initcall6.init.
从上面我们获得关键信息 initcall6,搜索可得:
1317 extern initcall_entry_t __initcall_start[]; 1318 extern initcall_entry_t __initcall0_start[]; 1319 extern initcall_entry_t __initcall1_start[]; 1320 extern initcall_entry_t __initcall2_start[]; 1321 extern initcall_entry_t __initcall3_start[]; 1322 extern initcall_entry_t __initcall4_start[]; 1323 extern initcall_entry_t __initcall5_start[]; 1324 extern initcall_entry_t __initcall6_start[]; 1325 extern initcall_entry_t __initcall7_start[]; 1326 extern initcall_entry_t __initcall_end[]; 1327 1328 static initcall_entry_t *initcall_levels[] __initdata = { 1329 __initcall0_start, 1330 __initcall1_start, 1331 __initcall2_start, 1332 __initcall3_start, 1333 __initcall4_start, 1334 __initcall5_start, 1335 __initcall6_start, 1336 __initcall7_start, 1337 __initcall_end, 1338 };
定义一个指针数组,并引用一些符号,但这和 .initcall6 段没关系
搜索 __initcallx_start ,发现定义在链接脚本中
59 .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_ftrac e_events = .; __start_ftrace_eval_maps = .; KEEP(*(_ftrace_eval_map)) __stop_ftrace_eval_maps = .; *(.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_tabl e = .; KEEP(*(__cpu_method_of_table)) KEEP(*(__cpu_method_of_table_end)) . = ALIGN(8); __cpuidle_method_of_tab le = .; 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_tabl e_end = .; . = ALIGN(8); __kunit_suites_start = .; KEEP(*(.kunit_test_suites)) __kunit_suites_end = .; . = ALI GN(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(*(.initcall 5s.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_ini tcall_end = .; . = ALIGN(4); __initramfs_start = .; KEEP(*(.init.ramfs)) . = ALIGN(8); KEEP(*(.init.ramfs.info )) }
__initcall0_start = .; KEEP((.initcall0.init)) KEEP((.initcall0s.init)) __initcall1_start = .;
可见,链接脚本将 .initcallN.init 的段的 内容链接到一起,并 定义 __initcallN_start 符号为 该段的起始地址, __initcallN+1_start符号为该段的结束地址。
一共有 7个 initcall段,最后的结束地址用符号 __initcall_end 标记。
结合c代码中定义,可知:
所有 module_init 修饰的函数 fn,会定义一个 long类型的变量,值为相对于 fn 的偏移量,所有 变量会组成一个数组,定义在 .init.data段,数组起始地址用 __initcall6_start 标记,结束地址用 __initcall7_start标记。所有初始化相关数组又构成一个大数组 initcall_levels
所以要调用函数 fn,必须遍历 函数 initcall_levels.
搜索initcall_levels的使用。
1287 int __init_or_module do_one_initcall(initcall_t fn) 1288 { ... 1295 1296 do_trace_initcall_start(fn); 1297 ret = fn(); 1298 do_trace_initcall_finish(fn, ret); ... 1313 return ret; 1314 } 1358 static void __init do_initcall_level(int level, char *command_line) 1359 { 1360 initcall_entry_t *fn; ... 1369 for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) 1370 do_one_initcall(initcall_from_entry(fn)); 1371 }
可见 若 level为 6,则执行 __initcall6_start数组的所有函数
而initcall_from_entry 为 当 地址值 加 内存的值,而内存中记录相对于module_init修饰的函数的偏移量,所以获得 模块初始化函数的地址。
250 static inline void *offset_to_ptr(const int *off) 251 { 252 return (void *)((unsigned long)off + *off); 253 } 122 static inline initcall_t initcall_from_entry(initcall_entry_t *entry) 123 { 124 return offset_to_ptr(entry); 125 }
do_initcall_level被do_initcalls调用,do_initcalls完成所有初始化函数的执行
1373 static void __init do_initcalls(void) 1374 { 1375 int level; 1376 size_t len = strlen(saved_command_line) + 1; 1377 char *command_line; 1378 1379 command_line = kzalloc(len, GFP_KERNEL); 1380 if (!command_line) 1381 panic("%s: Failed to allocate %zu bytes\n", __func__, len); 1382 1383 for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) { 1384 /* Parser modifies command_line, restore it each time */ 1385 strcpy(command_line, saved_command_line); 1386 do_initcall_level(level, command_line); 1387 } 1388 1389 kfree(command_line); 1390 }
而 do_basic_setup 调用 do_initcalls
kernel_init_freeable 调用 do_basic_setup
kernel_init 调用 kernel_init_freeable
kernel_init 调用完 kernel_init_freeable后 释放 无用的空间。
1490 static int __ref kernel_init(void *unused) 1491 { 1492 int ret; 1493 1494 /* 1495 * Wait until kthreadd is all set-up. 1496 */ 1497 wait_for_completion(&kthreadd_done); 1498 1499 kernel_init_freeable(); 1500 /* need to finish all async __init code before freeing the memory */ 1501 async_synchronize_full(); 1502 1503 system_state = SYSTEM_FREEING_INITMEM; 1504 kprobe_free_init_mem(); 1505 ftrace_free_init_mem(); 1506 kgdb_free_init_mem(); 1507 exit_boot_config(); 1508 free_initmem(); 1509 mark_readonly(); 1510
如下所示,将释放__init_begin 到 __init_end 指针指向的地址之间的空间。并打印 Free unused kernel image memory xxK
513 void free_initmem(void) 514 { 515 fix_kernmem_perms(); 516 517 poison_init_mem(__init_begin, __init_end - __init_begin); 518 if (!machine_is_integrator() && !machine_is_cintegrator()) 519 free_initmem_default(-1); 520 } 521 2527 static inline unsigned long free_initmem_default(int poison) 2528 { 2529 extern char __init_begin[], __init_end[]; 2530 2531 return free_reserved_area(&__init_begin, &__init_end, 2532 poison, "unused kernel image (initmem)"); 2533 } 8115 unsigned long free_reserved_area(void *start, void *end, int poison, const char *s) 8116 { 8117 void *pos; 8118 unsigned long pages = 0; 8119 8120 start = (void *)PAGE_ALIGN((unsigned long)start); 8121 end = (void *)((unsigned long)end & PAGE_MASK); 8122 for (pos = start; pos < end; pos += PAGE_SIZE, pages++) { 8123 struct page *page = virt_to_page(pos); 8124 void *direct_map_addr; 8125 8126 /* 8127 * 'direct_map_addr' might be different from 'pos' 8128 * because some architectures' virt_to_page() 8129 * work with aliases. Getting the direct map 8130 * address ensures that we get a _writeable_ 8131 * alias for the memset(). 8132 */ 8133 direct_map_addr = page_address(page); 8134 /* 8135 * Perform a kasan-unchecked memset() since this memory 8136 * has not been initialized. 8137 */ 8138 direct_map_addr = kasan_reset_tag(direct_map_addr); 8139 if ((unsigned int)poison <= 0xFF) 8140 memset(direct_map_addr, poison, PAGE_SIZE); 8141 8142 free_reserved_page(page); 8143 } 8144 8145 if (pages && s) 8146 pr_info("Freeing %s memory: %ldK\n", s, K(pages)); 8147 8148 return pages; 8149 }
__init_begin和 __init_end在链接脚本 arch/arm/kernel/vmlinux.lds中定义,
而其中有定义
将 .init.text 段放到 __init_begin 和 __init_end 之间。
我们回顾 module_init(fn) 时, fn 的声明为
static int __init fn(void)
根据目的,我们需要把 fn 的定义放到 __init_begin 和 __init_end 之间,以让其在执行后被释放。
我们查看 __init 宏的定义
50 #define __init __section(".init.text") __cold __latent_entropy __noinitretpoline __nocfi
可见 __init 就是将修饰的符号 定义在 .init.text段,
再看连接脚本中 .init.text的位置
30 __init_begin = .; ... . = ALIGN(8); .init.text : AT(ADDR(.init.text) - 0) { _sinittext = .; *(.init.text .init.text.*) *(.text.star tup) *(.meminit.text*) _einittext = .; } ... 65 __init_end = .;
可见 .init.text在 __init_begin和 __init_end之间,所以 被 __init宏修饰的 符号将会被free_initmem释放。
模块内嵌内核时,module_init宏将 定义一个 内存空间 使用 基址加变址的方式保存 被修饰的函数地址,并将所有同类型空间链接到同一段.initcall6.init ,并定义 符号 __initcall6_start作为数组首地址,内核初始化时会遍历数组以执行模块初始化函数。所有的初始化完成后,会释放 __init_begin 到__init_end之间的空间,由于 模块 初始化函数使用 __init 宏定义,会被连接到 __init_begin和__init_end之间,所以模块初始化函数会被释放。