Linux驱动开发:环境准备与报错处理

目录

一、宏内核与微内核、以及LINUX的内核

二、编译、安装头文件操作

三、printf和printk的区别

四、static与__init的作用

(1)static

(2)__init

五、helloworld驱动示例

(1)Makefile与驱动代码

(2)为什么KERN_EMERG级别日志仍然不会输出到控制台?伪终端控制台!

(3)消除VSCode的编辑器飘红


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 是 "过滤器",帮你从海量文本里快速找到想要的信息。

(3)消除VSCode的编辑器飘红

相关推荐
MC_J2 小时前
Linux 6.1 移植RTL8723du驱动
linux·arm
彭泽布衣2 小时前
Linux如何指定源端口打流
linux·运维·网络
Ciel_75212 小时前
OpenClaw 深度进阶:记忆系统、多智能体架构与自动化调度全解析
运维·自动化
晨晖22 小时前
Linux命令3
linux·运维·服务器
Mike117.2 小时前
GBase 8a 数据同步实践:从 T+1 同步、实时镜像到一写多读的落地思路
java·服务器·数据库
素雨迁喜2 小时前
Linux平台下git工具的使用
linux·运维·git
智算菩萨3 小时前
使用免费托管平台搭建并部署静态与动态网页教程
服务器·html5·网页·网页部署
十年编程老舅3 小时前
Linux DMA 技术深度拆解
linux·网络·linux内核·dma·c/c++·内存访问
jianqiang.xue3 小时前
ESP32-S3 运行 Linux 全指南:从 RISC-V 模拟器移植到 8 秒快速启动
linux·stm32·单片机·mongodb·risc-v·esp32s3