Linux Kernel Module
在 Linux 内核中,kernel module 是一些可以让内核在需要时载入和执行的代码,并且可以在不需要时由内核卸载。在很早以前,每编写一个设备驱动都需要重新编译一次整个内核镜像,kernel module 机制的出现避免了每次给内核添加功能时都需要重新编译,极大的提升了内核的可扩展性。本文将介绍如何基于 kernel module 机制编写自己的 module,并介绍 kernel module 的基层实现原理,文中所有演示代码可以在 Github 中找到。
编写 Module
工作环境为 Ubuntu 20.04 内核版本为 5.4.0:
|
|
下面是一个简单的 hello world 的 kernel module:
|
|
module_init为模块入口函数,在模块加载时被调用执行module_exit为模块出口函数,在模块卸载被调用执行
为了将上述代码编译成内核模块,使用 make 进行编译,下面是使用的 Makefile:
|
|
其中,obj-m 指定了目标文件的名称,文件名需要和源文件名相同(扩展名除外),以便于 make 自动推导。
使用 make 命令编译模块,得到模块文件 hello.ko 和一些中间文件。
|
|
- 加载模块:执行命令
insmod hello.ko加载模块。注意insmod命令不会自动加载依赖项,如果你编写的驱动模块依赖了其他模块,则可以使用modprobe命令自动加载依赖项。 - 卸载模块:执行命令
rmmod hello卸载模块 - 验证输出:可以用
dmesg查看内核日志:
[12517.215951] Hello, world!
[14157.446937] GoodBye, cruel world!
Module 原理
内核链接
查看内核模块的 ko 文件:
|
|
可以看到,hello.ko 的文件类型为可重定位目标文件,这和一般的目标文件格式没有任何区别。我们知道,目标文件是不能直接执行的,它需要经过链接器的地址空间分配、符号解析和重定位的过程,转化为可执行文件才能执行。实际上,内核将 hello.ko 加载后对其进行了链接。
模块加载
模块数据结构的 init 和 exit 函数指针记录了我们定义的模块入口函数和出口函数。
|
|
模块加载由内核的系统调用 init_module 完成。
|
|
系统调用 init_module 由 SYSCALL_DEFINE3(init_module...) 实现,其中有两个关键的函数调用,load_module 用于模块加载,do_one_initcall 用于回调模块的 init 函数。
函数 load_module 的实现为:
|
|
函数 load_module 内有四个关键的函数调用:
copy_and_check将模块从用户空间拷贝到内核空间layout_and_allocate为模块进行地址空间分配simplify_symbols为模块进行符号解析apply_relocations为模块进行重定位
由此可见,模块加载时,内核为模块文件 hello.ko 进行了链接的过程。
至于函数 do_one_initcall 的实现就比较简单了,即调用了模块的入口函数 init。
|
|
模块卸载
模块卸载由内核的系统调用 delete_module 完成。
|
|
通过回调 exit 完成模块的出口函数功能,最后调用 free_module将模块卸载。
参考资料
-
No backlinks found.