在 Linux 内核中,kernel module
是一些可以让内核在需要时载入和执行的代码,并且可以在不需要时由内核卸载。在很早以前,每编写一个设备驱动都需要重新编译一次整个内核镜像,kernel module
机制的出现避免了每次给内核添加功能时都需要重新编译,极大的提升了内核的可扩展性。本文将介绍如何基于 kernel module
机制编写自己的 module,并介绍 kernel module
的基层实现原理,文中所有演示代码可以在 Github 中找到。
编写 Module 工作环境为 Ubuntu 20.04
内核版本为 5.4.0
:
1 2 $uname -aLinux VM-0-29-ubuntu 5.4.0-42-generic
下面是一个简单的 hello world
的 kernel module
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <linux/init.h> #include <linux/module.h> static int hello_init (void ) { printk(KERN_ALERT "Hello, world!\n" ); return 0 ; } static void hello_exit (void ) { printk(KERN_ALERT "GoodBye, cruel world!\n" ); } module_init(hello_init); module_exit(hello_exit); MODULE_AUTHOR("houmin.wei@outlook.com" ); MODULE_LICENSE("Dual BSD/GPL" ); MODULE_DESCRIPTION("A Hello, world Module" );
module_init
为模块入口函数,在模块加载时被调用执行
module_exit
为模块出口函数,在模块卸载被调用执行
为了将上述代码编译成内核模块,使用 make
进行编译,下面是使用的 Makefile
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ifneq ($(KERNELRELEASE) , ) obj-m := hello.o else KERNELDIR ?= /lib/modules/$(shell uname -r) /build PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions endif
其中,obj-m指定了目标文件的名称,文件名需要和源文件名相同(扩展名除外),以便于make自动推导。
使用 make
命令编译模块,得到模块文件 hello.ko
和一些中间文件。
1 2 3 4 5 6 7 8 9 $ make make -C /lib/modules/5.4.0-42-generic/build M=/home/ubuntu/houmin/hello modules make[1]: Entering directory '/usr/src/linux-headers-5.4.0-42-generic' CC [M] /home/ubuntu/houmin/hello/hello.o Building modules, stage 2. MODPOST 1 modules CC [M] /home/ubuntu/houmin/hello/hello.mod.o LD [M] /home/ubuntu/houmin/hello/hello.ko make[1]: Leaving directory '/usr/src/linux-headers-5.4.0-42-generic'
加载模块 :执行命令insmod hello.ko
加载模块。注意insmod
命令不会自动加载依赖项,如果你编写的驱动模块依赖了其他模块,则可以使用modprobe
命令自动加载依赖项。
卸载模块 :执行命令rmmod hello
卸载模块
验证输出 :可以用 dmesg
查看内核日志:
1 2 [12517.215951] Hello, world! [14157.446937] GoodBye, cruel world!
Module 原理 内核链接 查看内核模块的 ko
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ readelf -h hello.ko ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 2904 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 19 Section header string table index: 18
可以看到,hello.ko
的文件类型为可重定位目标文件,这和一般的目标文件格式没有任何区别。我们知道,目标文件是不能直接执行的,它需要经过链接器的地址空间分配、符号解析和重定位的过程,转化为可执行文件才能执行。实际上,内核将 hello.ko
加载后对其进行了链接 。
模块加载 模块数据结构的 init
和 exit
函数指针记录了我们定义的模块入口函数和出口函数。
kernel/module.h 1 2 3 4 5 6 7 8 9 10 struct module { int (*init)(void ); void (*exit )(void ); };
模块加载由内核的系统调用init_module完成。
kernel/module.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long , len, const char __user *, uargs) { struct module *mod ; int ret = 0 ; mod = load_module(umod, len, uargs); if (mod->init != NULL ) ret = do_one_initcall(mod->init); return 0 ; }
系统调用 init_module
由 SYSCALL_DEFINE3(init_module...)
实现,其中有两个关键的函数调用,load_module
用于模块加载,do_one_initcall
用于回调模块的 init
函数。
函数 load_module
的实现为:
kernel/module.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static struct module *load_module (void __user *umod, unsigned long len, const char __user *uargs) { struct load_info info = { NULL , }; struct module *mod ; long err; err = copy_and_check(&info, umod, len, uargs); if (err) return ERR_PTR(err); mod = layout_and_allocate(&info); if (IS_ERR(mod)) { err = PTR_ERR(mod); goto free_copy; } err = simplify_symbols(mod, &info); if (err < 0 ) goto free_modinfo; err = apply_relocations(mod, &info); if (err < 0 ) goto free_modinfo; }
函数load_module内有四个关键的函数调用:
copy_and_check
将模块从用户空间拷贝到内核空间
layout_and_allocate
为模块进行地址空间分配
simplify_symbols
为模块进行符号解析
apply_relocations
为模块进行重定位
由此可见,模块加载时,内核为模块文件 hello.ko
进行了链接 的过程。
至于函数 do_one_initcall
的实现就比较简单了,即调用了模块的入口函数 init
。
1 2 3 4 5 6 7 8 9 10 11 12 13 int __init_or_module do_one_initcall (initcall_t fn) { int count = preempt_count(); int ret; if (initcall_debug) ret = do_one_initcall_debug(fn); else ret = fn(); return ret; }
模块卸载 模块卸载由内核的系统调用delete_module完成。
kernel/module.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SYSCALL_DEFINE2(delete_module, const char __user *, name_user, unsigned int , flags) { struct module *mod ; char name[MODULE_NAME_LEN]; int ret, forced = 0 ; if (mod->exit != NULL ) mod->exit (); free_module(mod); }
通过回调exit完成模块的出口函数功能,最后调用 free_module
将模块卸载。
参考资料