
目录一、宏内核与微内核、以及LINUX的内核二、编译、安装头文件操作三、printf和printk的区别四、static与__init的作用1static2__init五、helloworld驱动示例1Makefile与驱动代码2为什么KERN_EMERG级别日志仍然不会输出到控制台伪终端控制台3消除VSCode的编辑器飘红Linux 内核驱动开发听上去很 “高大上”其实本质就是换了个开发环境写代码—— 用户态程序是跑在 glibc 库搭好的 “舞台” 上调用它封装好的函数而驱动开发就是要走到这个 “舞台” 的最底层去实现 glibc 本身都要依赖的那些内核接口是打通应用与硬件的关键一环。一、宏内核与微内核、以及LINUX的内核宏内核Monolithic Kernel所有内核功能进程管理、内存管理、文件系统、设备驱动等都运行在同一个内核地址空间中性能高但模块耦合度高。Linux 属于宏内核但通过可加载内核模块LKM实现了类似微内核的模块化扩展能力。微内核Microkernel仅将最核心的功能如进程调度、内存管理放在内核态其他功能如驱动、文件系统运行在用户态稳定性更高但性能开销较大。代表QNX、Minix。Linux 内核特点宏内核架构 动态模块加载机制既保证了内核执行效率又能灵活加载 / 卸载驱动模块是嵌入式与服务器领域的主流选择。二、内核头文件引入、编辑器路径配置修改在学习内核驱动之前我们首先需要包含3个头文件#include linux/module.h #include linux/init.h #include linux/kernel.hmodule.h包含模块加载 / 卸载、声明、符号导出等函数让.ko模块能被动态链入内核地址空间维持宏内核结构不变。init.h定义module_init()/module_exit()宏标记模块的初始化与清理函数周期。好比init.h是给模块打标签、写入口地址而module是真正实现模块加载、卸载流程的人。kernel.h提供内核基础功能的接口声明如内核打印printk、调试断言、日志级别、工具宏等是内核开发的通用基础工具包。当然如果你按照上述的操作进行会发现中间这个头文件飘红了本质是3个都没有但编辑器只飘红了一个。这其实是因为一般的Ubuntu发行版都只会提供应用层使用的头文件而上述头文件是内核开发人员才会接触到的所以需要先从网上下载这些头文件才能进行内核态开发。sudo apt install linux-headers-$(uname -r)在bash中使用上述命令安装该头文件。诶为什么安装后仍然飘红呢这是因为编辑器默认只会搜索到用户态头文件路径不会主动找到/usr/src/linux-headers-xxx/这种内核头文件路径需要手动配置搜索路径。在你个项目工作区下找到该json配置文件并修改他。不过值得注意的是这里的修改只会在当前工作区生效如果你以后要一直从事内核开发可以直接在编辑器的全局json配置中修改。我选择只在当前工作区修改的好处就是以后可以既内核开发、也可以用户开发不会全局污染头文件搜索路径只不过每次开发前会麻烦点。先用uname -r查询你的内核版本号然后复制替换到下面的“你的内核版本”部分。{ 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是一个异步日志系统那么必定会有各种日志级别cat /proc/sys/kernel/printk你可以使用上述代码查看当前Linux的printk级别不同位置的含义不同第2位是默认printk日志打印级别而第一位表示刷新到控制台的日志级别下限。四、static与__init的作用在 Linux 内核模块开发中static和__init是两个非常关键的修饰符它们分别从作用域控制和内存优化两个维度规范内核模块的代码行为。1staticstatic是 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驱动示例1Makefile与驱动代码在使用驱动之前要首先把驱动模块编译出来。在用户态开发中我们是直接使用的g但是在内核中需要使用Makefile不过关于Makefile的编写我还未曾了解所以这里直接把它贴出来以后直接复制即可。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#includelinux/module.h #includelinux/init.h #includelinux/kernel.h static int __init hello_init(void) { printk(KERN_EMERGHello, kernel!\n); return 0; } static void __exit hello_exit(void) { printk(KERN_EMERGGoodBye, 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查看当前有哪些模块正在运行。补充说明1dmesg—— 内核日志查看器 全称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 行。理解就像水管一样把前一个命令的输出流接到后一个命令的输入流。3grep—— 文本搜索工具 全称global regular expression print全局正则表达式打印作用在文本中搜索包含指定关键词的行只输出匹配的内容。例子 1dmesg | grep Hello只显示内核日志里包含Hello的行过滤掉其他无关信息。例子 2lsmod | grep helloworld先执行lsmod列出所有加载的内核模块。再用grep搜索包含helloworld的行确认你的模块是否正在运行。核心grep是 “过滤器”帮你从海量文本里快速找到想要的信息。3消除VSCode的编辑器飘红