目录
(2)为什么KERN_EMERG级别日志仍然不会输出到控制台?伪终端控制台!
Linux 内核驱动开发听上去很 "高大上",其实本质就是换了个开发环境写代码------ 用户态程序是跑在 glibc 库搭好的 "舞台" 上,调用它封装好的函数;而驱动开发,就是要走到这个 "舞台" 的最底层,去实现 glibc 本身都要依赖的那些内核接口,是打通应用与硬件的关键一环。
一、宏内核与微内核、以及LINUX的内核
-
宏内核(Monolithic Kernel) :所有内核功能(进程管理、内存管理、文件系统、设备驱动等)都运行在同一个内核地址空间中,性能高但模块耦合度高。Linux 属于宏内核,但通过可加载内核模块(LKM)实现了类似微内核的模块化扩展能力。
-
微内核(Microkernel):仅将最核心的功能(如进程调度、内存管理)放在内核态,其他功能(如驱动、文件系统)运行在用户态,稳定性更高但性能开销较大。代表:QNX、Minix。
-
Linux 内核特点 :宏内核架构 + 动态模块加载机制,既保证了内核执行效率,又能灵活加载 / 卸载驱动模块,是嵌入式与服务器领域的主流选择。

二、内核头文件引入、编辑器路径配置修改
在学习内核驱动之前,我们首先需要包含3个头文件:
cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
module.h: 包含模块加载 / 卸载、声明、符号导出等函数,让.ko模块能被动态链入内核地址空间,维持宏内核结构不变。
init.h: 定义module_init()/module_exit()宏,标记模块的初始化与清理函数周期。好比init.h是给模块打标签、写入口地址;而module是真正实现模块加载、卸载流程的人。
kernel.h: 提供内核基础功能的接口声明,如内核打印printk、调试断言、日志级别、工具宏等,是内核开发的通用基础工具包。
当然,如果你按照上述的操作进行会发现:中间这个头文件飘红了(本质是3个都没有,但编辑器只飘红了一个)。这其实是因为一般的Ubuntu发行版都只会提供应用层使用的头文件,而上述头文件是内核开发人员才会接触到的,所以需要先从网上下载这些头文件,才能进行内核态开发。

cpp
sudo apt install linux-headers-$(uname -r)
在bash中使用上述命令安装该头文件。诶,为什么安装后仍然飘红呢?这是因为编辑器默认只会搜索到用户态头文件路径,不会主动找到/usr/src/linux-headers-xxx/这种内核头文件路径,需要手动配置搜索路径。

在你个项目工作区下找到该json配置文件,并修改他。不过值得注意的是:这里的修改只会在当前工作区生效,如果你以后要一直从事内核开发,可以直接在编辑器的全局json配置中修改。我选择只在当前工作区修改的好处就是:以后可以既内核开发、也可以用户开发,不会全局污染头文件搜索路径,只不过每次开发前会麻烦点。
先用uname -r查询你的内核版本号,然后复制,替换到下面的"你的内核版本"部分。
cpp
{
"configurations": [
{
"name": "Linux-Kernel",
"includePath": [
"${workspaceFolder}/**",
"/usr/src/linux-headers-你的内核版本/include/**",
"/usr/src/linux-headers-你的内核版本/arch/x86/include/**",
"/usr/src/linux-headers-你的内核版本/include/uapi/**"
],
"defines": [
"__KERNEL__",
"MODULE"
],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
配置完成后保存,并重启VSCode,让配置生效。再打开后发现编辑器已经能正确搜索到该路径了。

三、printf和printk的区别
printf是用户态glibc库提供的库函数(被进程加载链接到自己的进程地址空间中使用),对于一般的应用层开发足矣。但是我们这里要做的是内核开发,而内核态无法调用用户态函数,所以必须使用内核本身提供的printk函数。
核心特点是:
必须指定打印等级(也可省略,使用系统默认等级);
输出内容不会直接显示在终端,需通过
dmesg/cat /var/log/kern.log查看。而是直接被写入内核的环形缓冲区,最后被内核守护进程异步的写入log文件(异步:消费者生产者模型)。用户态的printf本质上是写入其他文件中,因为我们说一个C程序默认打开了三个文件:stdio、stdout、stderror,但是内核中不存在进程、文件描述符的概念,所以在这里不适用。
既然pringk是一个异步日志系统,那么必定会有各种日志级别:

cpp
cat /proc/sys/kernel/printk
你可以使用上述代码查看当前Linux的printk级别:

不同位置的含义不同:第2位是默认printk日志打印级别,而第一位表示刷新到控制台的日志级别下限。

四、static与__init的作用
在 Linux 内核模块开发中,static 和 __init 是两个非常关键的修饰符,它们分别从作用域控制 和内存优化两个维度,规范内核模块的代码行为。
(1)static
static 是 C 语言的关键字,在内核模块中主要有两个核心用途:
限制函数 / 变量的作用域
被
static修饰的函数或全局变量,作用域被限制在当前源文件内,无法被其他内核模块或文件直接调用。这避免了内核符号命名冲突,保护了模块内部实现细节,符合封装的设计思想。
示例:
// 仅在当前文件可见,不会导出到内核符号表 static int demo_init(void) { printk(KERN_INFO "Module init\n"); return 0; }保持局部变量的生命周期
修饰函数内的局部变量时,变量会被存储在静态数据段,而非栈中,函数调用结束后值不会丢失,常用于需要持久化状态的场景。
内核驱动中较少用此特性,更多用于限制全局符号的可见性。
(2)__init
__init 是内核定义的宏(本质是 __attribute__((__section__(".init.text")))),专门用于修饰模块初始化函数:
标记初始化代码段
被
__init修饰的函数,会被链接器放到专门的.init.text段中。模块加载完成后,内核会自动释放这段内存,因为初始化代码只在加载时执行一次 ,之后不再需要,以此节约内核内存。好比一个安装链接部分,安装加载到内核中就不再需要了。
配套的
__exit宏
与
__init对应的__exit宏,用于修饰模块清理函数,标记这段代码仅在模块卸载时执行,同样会被放到特定段(.exit.text),在模块可被卸载时才保留。示例:
// 初始化函数:加载后释放内存 static int __init demo_init(void) { printk(KERN_INFO "Module loaded\n"); return 0; } // 清理函数:仅在卸载时执行 static void __exit demo_exit(void) { printk(KERN_INFO "Module unloaded\n"); } module_init(demo_init); module_exit(demo_exit);
五、helloworld驱动示例
(1)Makefile与驱动代码
在使用驱动之前,要首先把驱动模块编译出来。在用户态开发中我们是直接使用的g++,但是在内核中需要使用Makefile,不过关于Makefile的编写我还未曾了解,所以这里直接把它贴出来,以后直接复制即可。
cpp
obj-m += helloworld.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
cpp
#include<linux/module.h>
#include<linux/init.h>
#include<linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_EMERG"Hello, kernel!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_EMERG"GoodBye, kernel!\n");
}
module_init(hello_init); //注册模块函数--当一个模块想要被加载到内核中必须调用它
module_exit(hello_exit); //注销模块函数--当该动态加载的模块要被卸载时调用退出函数
//只有这句话是必须要的,因为Linux遵循开源协议,写这句话表示你同意开源了
MODULE_LICENSE("GPL");
//下面的都是对该模块的描述字段(选)
MODULE_AUTHOR("ymh");
MODULE_DESCRIPTION("helloworldmodule");
MODULE_ALIAS("testmodule");

(2)为什么KERN_EMERG级别日志仍然不会输出到控制台?伪终端控制台!
不过由于我使用的是VSCode下的Ubuntu,可能由于VSCode连接的Linux机器是一个伪终端,即用户态程序SSH创建的模拟终端进程,而内核的printk只会输出到内核指定的默认终端,不会转发到伪终端。所以在VSCode中没有显示,而在下图的Ubuntu中直接就显示了KERN_EMERG级别的日志。

发现可以正常使用dmesg读取出内核日志消息。当然你也可以在加载内核模块后使用lsmod查看当前有哪些模块正在运行。

补充说明:
(1)
dmesg------ 内核日志查看器 📜
全称 :
display message(显示消息)本质:读取内核 ** 环形缓冲区(ring buffer)** 里的日志,输出到终端。
作用:
查看内核启动信息、硬件驱动状态、内核模块的
printk输出等。你的
Hello, kernel!/GoodBye, kernel!就是通过printk写入这个缓冲区,再用dmesg看到的。常用参数:
dmesg | tail -n:只看最后n条日志(你用的tail -4就是看最后 4 条)。
sudo dmesg -C:清空缓冲区日志(方便看最新操作)。
dmesg -T:显示人类可读的时间戳。(2)
|------ 管道符 🚰
作用 :把前一个命令的输出 ,作为后一个命令的输入,实现命令串联。
例子:
dmesg | tail -4
先执行
dmesg,输出所有内核日志。再把这些日志 "喂给"
tail -4,只保留最后 4 行。理解:就像水管一样,把前一个命令的输出流,接到后一个命令的输入流。
(3)
grep------ 文本搜索工具 🔍
全称 :
global regular expression print(全局正则表达式打印)作用 :在文本中搜索包含指定关键词的行,只输出匹配的内容。
例子 1:
dmesg | grep "Hello"
- 只显示内核日志里包含
Hello的行,过滤掉其他无关信息。例子 2:
lsmod | grep helloworld
先执行
lsmod列出所有加载的内核模块。再用
grep搜索包含helloworld的行,确认你的模块是否正在运行。核心:
grep是 "过滤器",帮你从海量文本里快速找到想要的信息。



