2、Linux驱动初探:Linux内核模块基础,第一个Hello World驱动。

好,咱们终于走到这一步了。

从裸机到Linux,很多人觉得跨度很大。其实你想想看,裸机程序是直接跑在硬件上的,而Linux驱动,说白了就是跑在内核里的一个特殊程序。这个特殊程序,就叫「内核模块」。

我刚开始接触Linux驱动时,第一件事就是写一个Hello World模块。别小看它,它能帮你验证整个开发环境、编译工具链、内核源码树是不是通的。如果Hello World跑不起来,后面写啥驱动都是白搭。

2.1 什么是内核模块?

内核模块,英文叫Loadable Kernel Module(LKM)。

它是一段可以在系统运行时动态加载到内核的代码。不需要重新编译整个内核,也不需要重启机器。这一点,跟裸机开发完全不同——裸机程序你得重新烧录、复位才能跑新代码。

我遇到过不少从单片机转过来的朋友,他们习惯把功能全编译进一个二进制里。但在Linux里,驱动通常以模块形式存在。这样做的好处很明显:

  • 动态加载/卸载:用insmod加载,用rmmod卸载,不用重启
  • 节省内存:不用的驱动就不加载,内核镜像更小
  • 方便调试:改代码后只需重新编译模块,不用重编内核

嗯,这里要注意:内核模块运行在内核空间,拥有最高权限。写错了,系统直接崩溃,连个Segmentation fault都不会给你。所以,写驱动时一定要小心。

2.2 第一个Hello World模块

咱们直接上代码。这是最精简的版本,但五脏俱全。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, World! Module loaded.\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, World! Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");

这段代码,我拆开给你讲。

2.3 代码逐行解析

头文件

  • linux/module.h:模块相关的宏和函数,比如module_initmodule_exit
  • linux/kernel.h:内核提供的各种宏,比如printk的日志级别
  • linux/init.h__init__exit宏的定义

入口和出口函数

  • module_init(hello_init):告诉内核,加载模块时调用hello_init
  • module_exit(hello_exit):告诉内核,卸载模块时调用hello_exit

这两个宏是必须的。没有它们,内核不知道你的模块从哪里开始、从哪里结束。

printk

这是内核版的printf。但它不能直接输出到终端,而是写到内核日志缓冲区。你可以用dmesg命令查看。我刚开始学的时候,总以为printk会像printf一样直接打印到屏幕上,结果啥也没看到,还以为模块没加载成功。后来才知道,得用dmesg | tail看。

__init 和 __exit

这两个宏是优化用的。__init标记的函数,在模块加载完成后,内核会释放它占用的内存。__exit标记的函数,在模块编译进内核时会被忽略。说白了,就是省内存的小技巧。

2.4 编写Makefile

模块的编译跟普通C程序不一样。你不能直接用gcc,得用内核的构建系统。Makefile长这样:

obj-m := hello.o

KDIR := /lib/modules/$(shell uname -r)/build

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

解释一下:

  • obj-m := hello.o:告诉内核构建系统,把hello.c编译成一个模块
  • KDIR:指向当前运行内核的源码树路径。注意,这个路径下必须有编译好的内核头文件和Makefile
  • -C $(KDIR):切换到内核源码目录执行Make
  • M=$(PWD):指定模块源码所在的目录

我遇到过新手直接在自己目录下敲make,结果报一堆错。原因就是没指定内核源码路径。记住,模块编译必须依赖内核的构建系统。

2.5 编译与测试

在终端里执行:

make

如果一切顺利,你会看到:

make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
  CC [M]  hello.o
  MODPOST
  CC [M]  hello.mod.o
  LD [M]  hello.ko

生成了hello.ko文件。这就是内核模块。

加载模块:

sudo insmod hello.ko

查看日志:

dmesg | tail

你应该能看到:

[ 1234.567890] Hello, World! Module loaded.

卸载模块:

sudo rmmod hello

再看日志:

[ 1234.678901] Goodbye, World! Module unloaded.

嗯,到这里,你的第一个Linux驱动就跑起来了。

提示: 如果dmesg输出太多,可以用dmesg | grep "Hello"过滤。我习惯在printk里加个模块名前缀,方便定位。
警告: 不要在开发板上直接加载未测试的模块。我曾经在树莓派上加载一个有bug的模块,结果内核直接panic,只能拔电重启。建议先在虚拟机或开发板上测试。

2.6 模块信息与依赖

你可以用modinfo查看模块的详细信息:

modinfo hello.ko

输出类似:

filename:       /home/user/hello/hello.ko
license:        GPL
author:         Your Name
description:    A simple Hello World module
depends:

这里depends是空的,说明这个模块不依赖其他模块。如果你的驱动用到了其他模块提供的接口,这里会列出依赖关系。加载模块时,内核会自动先加载依赖模块。

2.7 常见问题与避坑

  • 编译报错:找不到头文件——检查内核源码树是否安装,/lib/modules/$(uname -r)/build是否存在
  • insmod报错:Invalid module format——模块编译时的内核版本与当前运行的内核版本不一致。我遇到过最坑的一次,是升级了内核忘了重启,结果编译出来的模块跟旧内核不匹配
  • printk不输出——检查日志级别,或者用dmesg -w实时查看
  • rmmod报错:Module is in use——有其他进程在使用这个模块,或者模块内部有资源没释放
核心要点:
  • 内核模块是动态加载的驱动代码
  • 入口函数用module_init注册,出口函数用module_exit注册
  • 编译必须依赖内核构建系统,不能直接用gcc
  • 调试信息用printk输出,用dmesg查看

好了,这就是第一个Hello World驱动。别看它简单,它背后涉及了模块的加载机制、内核的编译系统、日志系统等核心概念。把这些搞明白,后面写真正的硬件驱动时,你才不会手忙脚乱。

下一章,咱们会深入内核模块的更多细节,比如参数传递、符号导出、内存分配等。到时候,我会结合我在项目里踩过的坑,给你一一拆解。