基于
char_device_dome1项目整理,方便复习。
目录
- 从零开始:内核模块 Hello World
- 学习路线:Hello 之后学什么
- 字符设备、class、device 三者关系
- 驱动整体框架
- file_operations 四个核心函数
- read / write 怎么做
- 两种 buffer 策略
- 常见错误与踩坑
- 用户态测试程序
- 常用命令速查
1. 从零开始:内核模块 Hello World
最小模块结构
static int __init hello_init(void) { ... return 0; }
static void __exit hello_exit(void) { ... }
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
Makefile
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
注意: recipe 行必须用 Tab 缩进,不能用空格。
编译与加载
make # 生成 hello.ko
sudo insmod hello.ko
dmesg | tail # 查看内核日志
sudo rmmod hello
第一个坑:module_exit 必须返回 void
// ❌ 错误
static int __exit hello_exit(void) { return 0; }
// ✅ 正确
static void __exit hello_exit(void) { }
2. 学习路线:Hello 之后学什么
模块基础 → 字符设备框架 → file_operations → 用户态读写 → ioctl → 并发/资源管理
| 阶段 | 内容 |
|---|---|
| 1 | module_init/exit、printk、insmod/rmmod |
| 2 | register_chrdev、file_operations |
| 3 | open/read/write/release + copy_from_user/copy_to_user |
| 4 | class_create + device_create 自动创建设备节点 |
| 5 | ioctl、阻塞 read、mutex 并发保护 |
| 6 | cdev + 动态设备号、platform 驱动、设备树 |
3. 字符设备、class、device 三者关系
┌─────────────────────────────────────────────────────────┐
│ 字符设备 (chrdev) │
│ register_chrdev → 主设备号 X → file_operations │
└───────────────────────────┬─────────────────────────────┘
│ dev_t = MKDEV(major, minor)
┌───────────────────────────▼─────────────────────────────┐
│ class(设备类) │
│ class_create → /sys/class/hello/ │
└───────────────────────────┬─────────────────────────────┘
│ device_create
┌───────────────────────────▼─────────────────────────────┐
│ device(设备节点) │
│ 自动生成 /dev/hello │
└─────────────────────────────────────────────────────────┘
| 对象 | 作用 | 用户能看到 |
|---|---|---|
| 字符设备 | 注册驱动逻辑,绑定主设备号和 fops | 看不到(在内核里) |
| class | sysfs 分类,方便 udev 管理 | /sys/class/hello/ |
| device | 连接 chrdev 和 class,创建设备文件 | /dev/hello |
class 有必要吗?
- 不是绝对必须:可以只
register_chrdev,然后手动mknod /dev/hello c 主设备号 0 - 强烈建议写:
insmod后自动出现/dev/hello,是常规做法
用户读写时的调用链
echo "abc" > /dev/hello
→ 内核 VFS → 根据主设备号找到 chrdev_fops → hello_write()
cat /dev/hello
→ 内核 VFS → hello_read()
4. 驱动整体框架
hello_init(加载,顺序:注册 → class → device)
major = register_chrdev(0, DEVICE_NAME, &chrdev_fops);
hello_class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(hello_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);
register_chrdev(0, ...):0表示动态分配主设备号,成功返回 major,失败返回负数- 需要
#include <linux/device.h>才能用device_create/device_destroy
hello_exit(卸载,顺序与 init 相反)
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, DEVICE_NAME);
file_operations 注册
// 函数必须先声明(或在 fops 之前定义)
static int hello_open(...);
static int hello_release(...);
static ssize_t hello_read(...);
static ssize_t hello_write(...);
static const struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
5. file_operations 四个核心函数
| 函数 | 何时调用 | 作用 |
|---|---|---|
open |
用户 open("/dev/hello") |
初始化 per-open 资源 |
release |
用户 close() |
释放 per-open 资源 |
read |
用户 read() |
内核 → 用户空间 |
write |
用户 write() |
用户空间 → 内核 |
当前项目:管道型 open/release
struct hello_pipe {
char buffer[BUFFER_SIZE];
size_t data_len;
};
static int hello_open(...) {
pipe = kzalloc(sizeof(*pipe), GFP_KERNEL);
file_p->private_data = pipe;
return 0;
}
static int hello_release(...) {
kfree(file_p->private_data);
return 0;
}
6. read / write 怎么做
为什么不能直接 memcpy?
用户态和内核态内存隔离,用户传来的指针 buf 不能直接访问:
// ❌ 错误:会导致内核崩溃
memcpy(kernel_buf, buf, size);
// ✅ 正确
copy_from_user(kernel_buf, buf, size); // 写:用户 → 内核
copy_to_user(buf, kernel_buf, size); // 读:内核 → 用户
四个参数含义
ssize_t hello_read(struct file *file_p, char __user *buf,
size_t size, loff_t *offset)
| 参数 | 含义 |
|---|---|
file_p |
文件对象,含 private_data |
buf |
用户空间缓冲区(不能直接解引用) |
size |
用户请求读/写的字节数 |
offset |
文件偏移(类似普通文件的读写位置) |
copy_from_user / copy_to_user 返回值
- 返回 0:全部成功
- 返回 非 0:未拷贝成功的字节数 → 应返回
-EFAULT
write 标准逻辑
if (*offset >= BUFFER_SIZE)
return -ENOSPC;
to_write = BUFFER_SIZE - *offset;
if (to_write > size)
to_write = size;
copy_from_user(pipe->buffer + *offset, buf, to_write);
*offset += to_write;
if (*offset > pipe->data_len)
pipe->data_len = *offset;
return to_write; // 返回实际写入字节数,不是固定 size
read 标准逻辑
if (*offset >= pipe->data_len)
return 0; // EOF
to_read = pipe->data_len - *offset;
if (to_read > size)
to_read = size;
copy_to_user(buf, pipe->buffer + *offset, to_read);
*offset += to_read;
return to_read;
关键原则
- 用户指针必须用
copy_from_user/copy_to_user - 返回值是实际读写的字节数,不要无脑返回
size - 必须限制长度,不能越界 buffer
- 必须处理
offset和data_len
7. 两种 buffer 策略
方案 A:设备级 buffer(跨进程共享)
static char dev_buffer[512];
static size_t dev_data_len;
- 数据在模块里,
close不释放 echo > /dev/hello再cat /dev/hello可以- 适合:简单内存设备、全局状态
方案 B:管道型 buffer(当前项目)
// 每次 open 在 private_data 里分配 hello_pipe
// close 时 kfree
- 数据只在同一次 open 期间有效
echo和cat是两次 open,读不到之前写的数据- 适合:管道、FIFO、临时会话数据
| 设备级 buffer | 管道型 buffer | |
|---|---|---|
| 分配时机 | 模块加载 / 静态 | 每次 open |
| close 后 | 数据保留 | 数据释放 |
| echo + cat | ✓ | ✗ |
| 测试方式 | shell 命令即可 | 需要用户态 test.c |
8. 常见错误与踩坑
编译期错误
| 错误 | 原因 | 修复 |
|---|---|---|
hello_open undeclared |
fops 在函数定义之前 | 加前向声明 |
module_exit 返回 int |
退出函数必须 void | 改为 void __exit |
buff undeclared |
参数名拼写错误 | 改为 buf |
缺失分隔符 |
Makefile 用了空格 | 改用 Tab |
| 函数嵌套定义 | write 写在 read 里面 | 拆成两个独立函数 |
运行期错误:cat 段错误
原因: cat 一次 read 请求约 128KB,驱动从 512 字节 buffer 里拷 128KB → 内核越界 → 段错误。
修复: 限制 to_read = min(有效长度, size),不能直接用 size。
运行期问题:echo 写了,cat 读不到
原因(管道型): echo 结束后 close 释放了 buffer,cat 是新的 open,拿到空 buffer。
修复: 改用设备级 buffer,或用 test.c 在同一次 open 里 write + read。
运行期问题:test.c write 成功但 read 返回 0
原因: write 后 offset 移到末尾,read 从 offset 开始读,已 >= data_len。
修复: read 前 lseek(fd, 0, SEEK_SET)。
权限问题
# /dev/hello 默认 root 权限
sudo chmod 666 /dev/hello
错误码规范
| 场景 | 应返回 |
|---|---|
| 内存分配失败 | -ENOMEM |
| copy_from/to_user 失败 | -EFAULT |
| buffer 满了 | -ENOSPC |
| 不要用 | -1(不规范) |
9. 用户态测试程序
编译
gcc -Wall -o test test.c
运行
sudo insmod hello.ko
sudo chmod 666 /dev/hello
./test
sudo rmmod hello
测试流程(管道型必须同一次 open)
fd = open("/dev/hello", O_RDWR); // → hello_open
write(fd, buf, len); // → hello_write
lseek(fd, 0, SEEK_SET); // 偏移回到开头(必须!)
read(fd, buf, len); // → hello_read
close(fd); // → hello_release
预期输出
写入 18 字节: 黄文波牛逼!
读出 18 字节: 黄文波牛逼!
10. 常用命令速查
# 编译驱动
make
make clean
# 模块管理
sudo insmod hello.ko
sudo rmmod hello
lsmod | grep hello
modinfo hello.ko
# 查看日志
dmesg | tail
dmesg -w # 实时跟踪
# 设备节点
ls -l /dev/hello
ls -l /sys/class/hello/
# 权限
sudo chmod 666 /dev/hello
# 编译测试程序
gcc -Wall -o test test.c
./test
项目文件说明
| 文件 | 说明 |
|---|---|
hello.c |
字符设备驱动(管道型 buffer) |
Makefile |
内核模块编译 |
test.c |
用户态测试程序 |
hello.ko |
编译生成的内核模块 |
下一步学习建议
- 给驱动加
ioctl(自定义命令,如清空 buffer) - 用
mutex保护 buffer,测试多进程并发读写 - 把
register_chrdev换成alloc_chrdev_region+cdev_add(更现代写法) - 阅读内核源码
drivers/char/mem.c对照学习 - 推荐书籍:《Linux 设备驱动程序》第 3 版(LDD3)第 3 章
最小字符设备驱动 Checklist
[✓] module_init / module_exit
[✓] file_operations(open / release / read / write)
[✓] register_chrdev
[✓] class_create + device_create
[✓] hello_exit 反向清理
[✓] copy_from_user / copy_to_user
[✓] read/write 长度限制 + offset + data_len
[✓] make 成功生成 hello.ko
[✓] test.c 同一次 open 内 write + read 测试通过