2-内核开发-第一个内核Hello模块开发案例
目录
课程简介:
Linux内核开发入门是一门旨在帮助学习者从最基本的知识开始学习Linux内核开发的入门课程。该课程旨在为对Linux内核开发感兴趣的初学者提供一个扎实的基础,让他们能够理解和参与到Linux内核的开发过程中。
课程特点:
1. 入门级别:该课程专注于为初学者提供Linux内核开发的入门知识。无论你是否具有编程或操作系统的背景,该课程都将从最基本的概念和技术开始,逐步引导学习者深入了解Linux内核开发的核心原理。
2. 系统化学习:课程内容经过系统化的安排,涵盖了Linux内核的基础知识、内核模块编程、设备驱动程序开发等关键主题。学习者将逐步了解Linux内核的结构、功能和工作原理,并学习如何编写和调试内核模块和设备驱动程序。
3. 实践导向:该课程强调实践,通过丰富的实例和编程练习,帮助学习者将理论知识应用到实际的Linux内核开发中。学习者将有机会编写简单的内核模块和设备驱动程序,并通过实际的测试和调试来加深对Linux内核开发的理解。
4. 配套资源:为了帮助学习者更好地掌握课程内容,该课程提供了丰富的配套资源,包括教学文档、示例代码、实验指导和参考资料等。学习者可以根据自己的学习进度和需求,灵活地利用这些资源进行学习和实践。
无论你是计算机科学专业的学生、软件工程师还是对Linux内核开发感兴趣的爱好者,Linux内核开发入门课程都将为你提供一个扎实的学习平台,帮助你掌握Linux内核开发的基础知识,为进一步深入研究和应用Linux内核打下坚实的基础。
这一讲主要讲述如何开发第一个Linux驱动程序模块,动手开发代码,运行加载卸载模块
课程内容和目标:
- 编写实现最简单的内核模块- hello
- 如何加载内核模块hello
- 如何卸载内核模块hello
先从安装所需依赖、然后以创建基础内核模块hello 进行展开。
1.安装环境检查及准备
Linux 内核开发环境没有搭建好的,需要看第一讲内容 1-内核开发环境ubuntu+virtualbox+mobaXterm搭建-CSDN博客
现在默认你已经搭建好环境。请检查以下项是否已经ready。
VirtualBox
Ubuntu:20.04.1-Ubuntu
mobaXterm personal editon
Ubuntu网络环境设置好可以访问外部网络,方便 mobaXterm 可以通过ssh 连接
VirtualBox 软件设置详细信息如下:
上节课已经安装好常用软件
sudo apt install net-tools
sudo apt install vim
sudo apt-get install gcc
2. 安装依赖项目
进行内核模块开发,需要安装对应的headers 文件,编译优c,Makefile 文件需要编译环境。
安装build-essential 和 kernel headers,执行以下命令:
$apt-get install linux-headers-$(uname -r)
build-essential
软件包提供了在基于 Debian 的系统上编译和构建软件所需的基本工具。
linux-headers-$(uname -r)
软件包提供了当前正在运行的内核的头文件。
为啥需要安装linux-headers? 在进行内核模块开发时需要安装 Linux headers 的原因如下:
- 内核头文件声明:Linux headers 包含内核中使用的所有数据结构、函数和宏的声明。这些声明对于编写与内核交互的模块至关重要。
- 编译模块:编译内核模块时,编译器需要知道内核中使用的符号的类型和大小。这些信息在 Linux headers 中提供。
- 符号解析:当内核加载模块时,它需要解析模块中使用的符号。这些符号在 Linux headers 中定义。
- 避免内核和模块之间的不匹配:Linux headers 对于确保内核和模块之间的兼容性非常重要。如果内核和模块的版本不匹配,则模块可能无法正常工作或可能导致系统不稳定。
安装header 文件后,安装kernel build 工具,以便进行代码编译。
安装的输出结果如下,说明安装成功
peach@peach-VirtualBox:~$ sudo apt-get install linux-headers-$(uname -r)
Reading package lists... Done
Building dependency tree
Reading state information... Done
linux-headers-5.15.0-91-generic is already the newest version (5.15.0-91.101~20 .04.1).
linux-headers-5.15.0-91-generic set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 63 not upgraded.
peach@peach-VirtualBox:~$ sudo apt-get install build-essential
[sudo] password for peach:
Reading package lists... Done
Building dependency tree
Reading state information... Done
build-essential is already the newest version (12.8ubuntu1.1).
0 upgraded, 0 newly installed, 0 to remove and 63 not upgraded.
3. 开始helloworld module 开发
Linux 内核模块是一种可加载的代码,它可以动态地添加到正在运行的内核中。模块提供了扩展内核功能的机制,而无需重新编译整个内核。开发模块通常用于以下目的:
- 添加对新硬件的支持
- 实现新文件系统
- 提供网络协议支持
- 实现安全功能
- 增强现有功能
本次课程实验,是创建一个最简单的内核模块。首先创建项目目录HelloModule
cd ~
mkdir HelloModule
编写内核模块,我们需要先了解下编写内核模块的规范以及最佳实践
- 模块名称:模块名称必须唯一,并且不应与内核中已经存在的任何其他模块名称相同。
- 目标:目标指定要构建的模块的类型。最常见的目标是
modules
,它会创建一个可加载的内核模块。 - 源文件:源文件指定用于构建模块的源代码文件。
- 依赖项:依赖项指定模块构建和加载所需的任何其他模块或内核功能。
- 编译选项:编译选项指定用于编译模块的编译器选项。
- 安装选项:安装选项指定如何安装模块。
vim hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk swing");
MODULE_DESCRIPTION("A simple hello world module");
MODULE_VERSION("1.0.0");
static int __init hellowrld_init(void)
{
printk(KERN_INFO "hello world \n");
return 0;
}
static void __exit helloworld_exit(void)
{
printk(KERN_INFO "GOODBYE HELLO WORLD!\n");
}
module_init(hellowrld_init);
module_exit(helloworld_exit)
这里就代码重点讲解三点个注意点,其他都是普通的c语言语法程序,相必大家都能够看懂理解。
(1)MODULE_ 宏定义
- MODULE_LICENSE("GPL"); 指定模块的许可证。GPL(通用公共许可证)是一种流行的开源许可证。
- MODULE_AUTHOR("kk swing"); 指定模块的作者。
- MODULE_DESCRIPTION("A simple hello world module"); 提供模块的简要描述。
- MODULE_VERSION("1.0.0"); 指定模块的版本。
这些宏对于提供有关模块的重要信息非常有用,例如其许可证、作者、用途和版本。此信息在编译、加载和使用模块时都会用到。
注意虽然这些宏是可选的,但是作为一个标准的内核模块,您在开发内核模块时使用它们。有助于使模块更易于被他人理解和使用。
(2)代码中,我们使用了两个宏__init 和 __exit,定义模块初始化函数,__init 和 __exit 宏用于指定模块初始化和清理函数。
__init 宏用于标记模块的初始化函数。该函数将在模块加载时调用。模块初始化函数通常用于分配资源、注册回调函数和执行其他初始化任务。
__exit 宏用于标记模块的清理函数。该函数将在模块卸载时调用。模块清理函数通常用于释放资源、注销回调函数和执行其他清理任务。
static int __init helloworld_init(void) 函数在模块加载时被调用。它将展开为 module_init() 函数。
内核中除了__init __exit 宏,钩子函数外还有很多类似的钩子,后面遇到再进行讲解。
(3)代码中的 KERN_INFO
具体含义是 Linux 内核中使用的日志级别,有点类似与Java 打日志有几种级别。KERN_INFO
是一个宏,它展开为一个整数常量,注意他不是一个函数。KERN_INFO
它表示要打印的信息具有信息性,并且对于用户或系统管理员来说可能是有用的。Linux 内核定义了几个日志级别,每个级别都有一个相关的优先级:
日志级别 | 优先级 |
KERN_EMERG | 0 |
KERN_ALERT | 1 |
KERN_CRIT | 2 |
KERN_ERR | 3 |
KERN_WARNING | 4 |
KERN_NOTICE | 5 |
KERN_INFO | 6 |
KERN_DEBUG | 7 |
KERN_INFO 的优先级为 6,这意味着它将打印所有优先级为 6 或更高(即 KERN_NOTICE、KERN_WARNING、KERN_ERR、KERN_CRIT、KERN_ALERT 和 KERN_EMERG)的信息。这里的逻辑和Java 开发工程定义的Log 级别是相似并且一致。printk 打印的输出内容不会在控制台输出。
Makefile
obj-m += hello.o
CFLAGS := -Wall -O2
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
注意编写Makefile 需要注意的点
- Makefile 必须使用 Tab 缩进,而不是空格。上面的 make 签名都是Tab 缩进。如果不确定或想比较好的避免此类问题,可以是哟个vs code 编辑器,在编写Makefile文件时,回车后,按下tab他会自动转成Tab,而不是空格
- Makefile 的行必须以换行符结尾。
- Makefile 中的注释是以
#
开头。 - Makefile 中的变量是以
$
开头。
modules 命令,说明构建的时一个内核模块。
4.编译
编写好上面的文件后,执行make 进行编译代码
peach@peach-VirtualBox:~/HelloModule$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/peach/HelloModule modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/peach/HelloModule/hello.o
MODPOST /home/peach/HelloModule/Module.symvers
CC [M] /home/peach/HelloModule/hello.mod.o
LD [M] /home/peach/HelloModule/hello.ko
BTF [M] /home/peach/HelloModule/hello.ko
Skipping BTF generation for /home/peach/HelloModule/hello.ko due to unavailability of vmlinux
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
执行make进行编译 ,有报错则需要解决相应的报错。编译通过后,会有 ko 模块文件生成。编译后,会看到很多输出内容,其中重点关注的是 ko 文件。
drwxrwxr-x 2 peach peach 4096 4月 20 08:31 ./
drwxr-xr-x 18 peach peach 4096 4月 20 13:31 ../
-rw-rw-r-- 1 peach peach 460 4月 20 08:30 hello.c
-rw-rw-r-- 1 peach peach 216168 4月 20 08:31 hello.ko
-rw-rw-r-- 1 peach peach 228 4月 20 08:31 .hello.ko.cmd
-rw-rw-r-- 1 peach peach 33 4月 20 08:31 hello.mod
-rw-rw-r-- 1 peach peach 856 4月 20 08:31 hello.mod.c
-rw-rw-r-- 1 peach peach 127 4月 20 08:31 .hello.mod.cmd
-rw-rw-r-- 1 peach peach 108912 4月 20 08:31 hello.mod.o
-rw-rw-r-- 1 peach peach 30838 4月 20 08:31 .hello.mod.o.cmd
-rw-rw-r-- 1 peach peach 108744 4月 20 08:31 hello.o
-rw-rw-r-- 1 peach peach 29543 4月 20 08:31 .hello.o.cmd
-rw-rw-r-- 1 peach peach 159 4月 20 08:30 Makefile
-rw-rw-r-- 1 peach peach 33 4月 20 08:31 modules.order
-rw-rw-r-- 1 peach peach 153 4月 20 08:31 .modules.order.cmd
-rw-rw-r-- 1 peach peach 0 4月 20 08:31 Module.symvers
-rw-rw-r-- 1 peach peach 203 4月 20 08:31 .Module.symvers.cmd
hello.ko 这个文件是我们要的文件,这个文件可以安装到内核中。
5.安装内核模块
有了模块文件后,我们就可以进行安装测试了,安装刚才的内核模块
sudo insmod hello.ko
insmod 执行后,主要经过了以下处理过程。
- 解析模块文件:内核将解析模块文件以获取模块的信息,例如模块名称、符号表和依赖关系。
- 检查依赖关系:内核将检查模块的依赖关系以确保它们已加载。如果缺少任何依赖项,内核将尝试加载它们。
- 分配内存:内核将为模块分配内存。
- 初始化模块:内核将调用模块的 init_module() 函数来初始化模块。
- 插入模块:内核将模块插入内核符号表中。这意味着内核现在可以访问模块导出的符号。
- 执行模块代码:内核将执行模块代码。
为了加载模块,可以使用 insmod 命令。当加载模块时,内核将调用模块的 init 函数。init 函数负责初始化模块并使其准备好使用。
__init 宏表示 init 函数只在模块加载时调用一次。这意味着该函数将在模块加载后从内核中删除。
唉,怎么没有输出内容?,输出在哪里呢?。
原来是使用printk 打印的输出,在kernel log 里面。可以使用dmesg 命令查看,这个命令很有用,查看进程异常退出的时候,很方便,例如java 开发工程师,有时候会看到测试环境的 java 进程跑着跑着,突然不见了,但又不知道啥原因导致进程死掉的,就可以使用这个命令去查看分析, 定位进程为啥原因挂掉。
执行dmesg 命令,看是否可以看到我们自动hello 模块,被内核加载时的printk 语句输出。
$dmesg
[43296.687898] clocksource: 'tsc' is current clocksource.
[43296.687901] tsc: Marking TSC unstable due to clocksource watchdog
[43296.815155] clocksource: Not enough CPUs to check clocksource 'tsc'.
[43296.815182] clocksource: Switched to clocksource kvm-clock
[43302.845589] e1000: enp0s3 NIC Link is Down
[43302.846080] e1000 0000:00:03.0 enp0s3: Reset adapter
[43304.925835] e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
[46724.369501] hello: loading out-of-tree module taints kernel.
[46724.369546] hello: module verification failed: signature and/or required key missing - tainting kernel
[46724.369734] hello world
6.卸载 module
当不需要模块是,需要进行卸载,卸载操作 rmmod 主要执行步骤如下。
- 查找模块:内核通过检查内核符号表来查找模块。
- 检查模块是否正在使用:内核检查模块是否正在被其他内核对象(如进程或设备驱动程序)使用。如果模块正在使用,内核将无法卸载它。
- 调用模块的 cleanup_module() 函数:内核调用模块的 cleanup_module() 函数来清理模块。这包括释放模块分配的任何资源和注销模块导出的任何符号。
- 从内核符号表中删除模块:内核从内核符号表中删除模块。这意味着内核不再可以访问模块导出的符号。
- 释放模块内存:内核释放模块分配的内存
- 打印卸载消息:内核打印以下消息以指示模块已卸载
执行卸载hello 模块操作
sudo rmmod hello
再次执行 使用dmesg 命令查看日志输出,发现exit 函数已经被调用。
$dmesg
[43296.687901] tsc: Marking TSC unstable due to clocksource watchdog
[43296.815155] clocksource: Not enough CPUs to check clocksource 'tsc'.
[43296.815182] clocksource: Switched to clocksource kvm-clock
[43302.845589] e1000: enp0s3 NIC Link is Down
[43302.846080] e1000 0000:00:03.0 enp0s3: Reset adapter
[43304.925835] e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX
[46724.369501] hello: loading out-of-tree module taints kernel.
[46724.369546] hello: module verification failed: signature and/or required key missing - tainting kernel
[46724.369734] hello world
[46856.430091] GOODBYE HELLO WORLD!
验证了卸载模块成功。
7.lsmod 查看内核当前加载模块
lsmod 可用于查看当前有哪些内核模块。
peach@peach-VirtualBox:~/HelloModule$ sudo insmod hello.ko
peach@peach-VirtualBox:~/HelloModule$ lsmod |grep hello
hello 16384 0
8.总结
以上就是一个最简单的内核模块开发完成。这个hello模块没有干什么事情,只是简单的打印了模块加载卸载过程中,输出日志,标识内核已经调用此方法。通过这个实验,我们知道了如何编写内核模块,内核模块Makefile 如何编写,如何加载内核模块,卸载内核模块,如何查找内核模块。
9.附录
有些内核编程规范及风格文档