wenbo2005.online

学习记录 / Linux

Linux 内核学习第二课

Linux2026-08-11

基于 char_device_dome1 项目整理,方便复习。


目录

  1. 从零开始:内核模块 Hello World
  2. 学习路线:Hello 之后学什么
  3. 字符设备、class、device 三者关系
  4. 驱动整体框架
  5. file_operations 四个核心函数
  6. read / write 怎么做
  7. 两种 buffer 策略
  8. 常见错误与踩坑
  9. 用户态测试程序
  10. 常用命令速查

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;

关键原则

  1. 用户指针必须用 copy_from_user / copy_to_user
  2. 返回值是实际读写的字节数,不要无脑返回 size
  3. 必须限制长度,不能越界 buffer
  4. 必须处理 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 编译生成的内核模块

下一步学习建议

  1. 给驱动加 ioctl(自定义命令,如清空 buffer)
  2. 用 mutex 保护 buffer,测试多进程并发读写
  3. 把 register_chrdev 换成 alloc_chrdev_region + cdev_add(更现代写法)
  4. 阅读内核源码 drivers/char/mem.c 对照学习
  5. 推荐书籍:《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 测试通过