Linux 内核驱动开发入门:从环境搭建到第一个字符设备驱动
面向读者:
- 会写 C 和
make
、但对insmod
心存敬畏的 Linux 应用开发工程师- 想打开"内核世界大门"的嵌入式在校生
- 被"段错误"劝退、又心痒想点亮小灯的极客
读完收获:① 一套能反复编译
>=20
次不炸的驱动开发环境(Ubuntu 22.04 + QEMU + 5.15 内核)② 亲手写出
< 100
行的"hello-char"字符设备,并看懂dmesg
每一行③ 掌握"加载-查看-卸载-调试"完整闭环,为后续
platform
/device-tree
/中断/时钟树打地基
一、前置知识清单(只需 3 项)
技能 | 最低要求 | 推荐速补链接 |
---|---|---|
C 语言 | 能写结构体 + 函数指针 | 《C Primer Plus》第 6 章 |
Makefile | 看得懂目标、依赖、变量 | GNU Make 官方 10 分钟教程 |
Shell 基础 | ls /dev dmesg -H |
man 手册即可 |
不懂内核 API?------没关系,本文出现的每一个函数都会给原型 + 一行注释。
没硬件?------用
QEMU
跑纯内存虚拟板子,同样能insmod
。
二、20 分钟搭好"不踩坑"开发环境
1. 宿主机选型
- Windows:WSL2 + Ubuntu 22.04(内核 5.15+)
- macOS:UTM 装 Ubuntu 22.04 ARM64
- Linux:直接真机,建议
distro >= Ubuntu 20.04
2. 一键脚本(Ubuntu 22.04 验证通过)
bash
#!/bin/bash
# file: setup_env.sh
sudo apt update && sudo apt install -y \
build-essential git vim cscope ctags \
linux-headers-$(uname -r) \
qemu qemu-system-x86 \
flex bison libssl-dev libelf-dev bc
执行完占用磁盘约 2.1 GB。
3. 验证工具链
bash
$ gcc --version # >= 9.0 即可
$ make -v # >= 4.0
$ qemu-system-x86_64 --version
三、下载并编译"同源"内核(5.15 LTS)
目标:让驱动模块和内核版本号 完全一致 ,避免
vermagic
加载失败。
bash
# 1. 取官方长期支持版
$ git clone --depth 1 -b linux-5.15.y \
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ~/kernel-5.15
$ cd ~/kernel-5.15
# 2. 生成默认配置(x86_64)
$ make x86_64_defconfig
# 3. 打开"启用模块版本校验"方便调试
$ make menuconfig
-> Enable loadable module support
-> Module versioning support (CONFIG_MODVERSIONS) = y
-> Kernel hacking
-> Compile-time checks and compiler options
-> Compile the kernel with debug info (CONFIG_DEBUG_INFO) = y
# 4. 编译(i7-11800H 约 12 min)
$ make -j$(nproc)
# 5. 安装模块到 /lib/modules/5.15.0+
$ sudo make modules_install
$ sudo cp arch/x86/boot/bzImage /boot/vmlinuz-5.15-lab
用 QEMU 启动自编译内核(无真实硬件也 OK)
bash
$ qemu-system-x86_64 \
-kernel /boot/vmlinuz-5.15-lab \
-drive file=rootfs.img,format=raw \ # 可共用宿主机目录
-append "root=/dev/sda rw console=ttyS0 nokaslr" \
-nographic -m 2G -smp 2
nokaslr
关闭地址随机化,方便gdb
调试。
四、字符设备驱动"hello-char"源码
功能:
- 注册设备节点
/dev/hello
- 支持
open/close/read/write
- 用
kmalloc
分配 4 KB 环形缓冲区- 用
pr_info
打印每次操作
目录树:
hello_char/
├── hello_char.c
├── Makefile
└── README.md
1. hello_char.c(代码共 88 行,含空行)
c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> /* copy_to_user */
#include <linux/slab.h> /* kmalloc */
#define DEVICE_NAME "hello"
#define BUF_SIZE 4096
static int major;
static char *buf;
static int wp;
static dev_t dev;
static struct cdev hello_cdev;
/* open */
static int hello_open(struct inode *inode, struct file *filp)
{
pr_info("hello: open\n");
return 0;
}
/* read */
static ssize_t hello_read(struct file *filp, char __user *user,
size_t sz, loff_t *off)
{
int ret;
if (sz > BUF_SIZE) sz = BUF_SIZE;
ret = copy_to_user(user, buf, sz);
return ret == 0 ? sz : -EFAULT;
}
/* write */
static ssize_t hello_write(struct file *filp, const char __user *user,
size_t sz, loff_t *off)
{
int ret;
if (sz > BUF_SIZE - wp) sz = BUF_SIZE - wp;
ret = copy_from_user(buf + wp, user, sz);
if (ret == 0) {
wp += sz;
return sz;
}
return -EFAULT;
}
/* release */
static int hello_release(struct inode *inode, struct file *filp)
{
pr_info("hello: release\n");
return 0;
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
};
/* 模块初始化 */
static int __init hello_init(void)
{
int err;
/* 1. 申请设备号 */
err = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
if (err < 0) goto fail;
major = MAJOR(dev);
/* 2. 分配缓冲区 */
buf = kmalloc(BUF_SIZE, GFP_KERNEL);
if (!buf) { err = -ENOMEM; goto unregister; }
/* 3. 注册字符设备 */
cdev_init(&hello_cdev, &hello_fops);
hello_cdev.owner = THIS_MODULE;
err = cdev_add(&hello_cdev, dev, 1);
if (err) goto free_buf;
pr_info("hello: loaded, major=%d, device /dev/%s\n", major, DEVICE_NAME);
return 0;
free_buf:
kfree(buf);
unregister:
unregister_chrdev_region(dev, 1);
fail:
return err;
}
/* 模块卸载 */
static void __exit hello_exit(void)
{
cdev_del(&hello_cdev);
kfree(buf);
unregister_chrdev_region(dev, 1);
pr_info("hello: unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lab<lab@example.com>");
MODULE_DESCRIPTION("The simplest char device driver");
2. Makefile(万能模板,兼容多内核版本)
makefile
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := hello_char.o
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
五、编译-加载-测试一条龙
1. 编译
bash
$ cd hello_char
$ make
LD [M] hello_char.ko
2. 插入模块 & 查看设备号
bash
$ sudo insmod hello_char.ko
$ dmesg | tail
[ 112.345] hello: loaded, major=240, device /dev/hello
3. 手动创建设备节点(也可自动,见下一节)
bash
$ sudo mknod /dev/hello c 240 0
$ sudo chmod 666 /dev/hello
4. 读写测试
bash
$ echo "kernel driver" > /dev/hello
$ cat /dev/hello
kernel driver
5. 卸载
bash
$ sudo rmmod hello_char
$ dmesg | tail
[ 180.112] hello: unloaded
六、进阶:自动创建设备节点(udev)
用 device_create
+ class_create
即可省去 mknod
。
c
static struct class *hello_class;
/* 在 hello_init 末尾添加 */
hello_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(hello_class, NULL, dev, NULL, DEVICE_NAME);
/* 在 hello_exit 开头移除 */
device_destroy(hello_class, dev);
class_destroy(hello_class);
重新加载后,/dev/hello
自动生成,权限 660,可写 udev 规则改为 666。
七、调试三板斧:打印、查看、断点
1. pr_info
+ dmesg -w
- 级别:
KERN_INFO
可显示,KERN_DEBUG
需打开DEBUG
宏 - 限速:同一行多次打印用
pr_info_once()
2. 模块信息 & 符号表
bash
$ modinfo hello_char.ko
$ cat /proc/modules | grep hello
$ sudo cat /sys/module/hello_char/sections/.text
3. gdb
+ qemu
源码级调试(可选)
QEMU 启动加 -s -S
,另开终端:
bash
$ gdb vmlinux
(gdb) target remote :1234
(gdb) add-symbol-file hello_char.ko 0xffffffffc0000000
(gdb) b hello_write
(gdb) c
八、常见错误对照表(收藏级)
现象 | 原因 | 解决 |
---|---|---|
insmod: ERROR: could not insert module hello_char.ko: Invalid module format |
vermagic 不匹配,模块用 5.15 编译,内核是 5.4 | 确保用同版本 KERNELDIR |
Unknown symbol __kmalloc |
未 include <linux/slab.h> |
加头文件 |
Device or resource busy |
设备节点被占用,或忘记 cdev_del |
lsof /dev/hello 后 kill |
Unable to handle kernel NULL pointer dereference |
空指针解引用 | 用 kgdb /kdump 定位 |
九、下一步往哪走?
- 同步/互斥:把环形缓冲区改成
kfifo
,加自旋锁 - 中断/GPIO:在树莓派上点亮 LED,用
gpiod
新接口 - platform + 设备树:写 I²C 温湿度传感器驱动
- 调试工具进阶:
ftrace
,perf
,eBPF
十、总结:你刚完成了"从 0 到 1"
✅ 会编译内核与模块
✅ 理解字符设备骨架:申请设备号→cdev_add
→file_operations
✅ 掌握 insmod/rmmod/dmesg
调试闭环
✅ 会用 copy_to_user/copy_from_user
做内核-用户数据交换
内核世界的大门已推开,下一站:中断、DMA、设备树、电源管理......
保持好奇,保持敬畏,Happy Hacking!