如果你希望写一篇更纯粹、更侧重于**“字符设备驱动基础架构”**的博文,可以参考下面这一篇。这篇博文去掉了特定的 ioctl 细节,专注于字符设备驱动的“三大支柱”:设备号管理、文件操作接口、以及内核/用户空间的数据交换


Linux 内核开发:手把手教你构建一个规范的字符设备驱动

1. 什么是字符设备驱动?

字符设备(Character Device)是 Linux 驱动中最常见的类型之一。它像一个字节流(如文件)一样被访问。不同于块设备(如硬盘)需要随机读写,字符设备通常按照先后顺序进行数据的读取和写入,常见的键盘、串口、显示器驱动都属于这一类。


2. 字符设备驱动的“三大支柱”

开发一个字符设备驱动,主要围绕以下三个核心环节:

① 设备的“身份证明”:设备号(dev_t)

在 Linux 中,每个设备都有一个主设备号(Major Number)和次设备号(Minor Number)。

  • 主设备号:对应驱动程序。
  • 次设备号:对应驱动程序控制的具体设备。
    规范写法:建议使用 alloc_chrdev_region 让内核自动分配,避免手动指定的冲突。

② 驱动的“大脑”:文件操作接口(file_operations)

这是用户态与内核态的“翻译官”。当用户调用 read()write() 时,内核会通过这个结构体找到驱动中对应的函数。

③ 安全的“关卡”:空间拷贝

内核空间和用户空间是隔离的。驱动程序绝对不能直接解引用用户态传来的指针,必须使用:

  • copy_from_user():安全地从用户态获取数据。
  • copy_to_user():安全地发送数据给用户态。

3. 核心代码框架解析

我们将实现一个简单的“内存回显设备”,用户写入什么,下次就能读取出什么。

3.1 驱动初始化:注册与自动创建节点

现代驱动不再需要手动执行 mknod 命令,通过 class_createdevice_create 可以实现 /dev/ 下节点的自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int __init mychardev_init(void) {
// 1. 申请设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");

// 2. 初始化 cdev 并绑定 fops
cdev_init(&my_cdev, &mychardev_fops);
cdev_add(&my_cdev, dev_num, 1);

// 3. 创建类和设备节点(实现自动生成 /dev/mychardev)
my_class = class_create(THIS_MODULE, "my_class");
device_create(my_class, NULL, dev_num, NULL, "mychardev");

return 0;
}

3.2 读写逻辑:内存数据交换

为了保证多进程访问时的安全,我们引入了 mutex 互斥锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *ppos) {
size_t to_copy = ...;
// 使用 copy_to_user 将内核缓冲区数据传给用户
if (copy_to_user(buf, kernel_buf + *ppos, to_copy))
return -EFAULT;

*ppos += to_copy;
return to_copy;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) {
// 使用 copy_from_user 从用户空间接收数据
if (copy_from_user(kernel_buf, buf, len))
return -EFAULT;

return len;
}

4. 实验与验证

编译驱动

使用标准的内核 Makefile 编译生成 .ko 文件:

1
2
make
sudo insmod mychardev.ko

快速测试

甚至不需要写 C 程序,直接利用 Shell 命令即可测试驱动:

1
2
3
4
5
# 写入测试
echo "Hello Kernel" > /dev/mychardev

# 读取测试
cat /dev/mychardev

查看内核日志

1
dmesg | tail

你会看到驱动输出的加载信息以及读写字节数。


5. 学习总结与进阶建议

编写字符设备驱动时,有几个规范必须遵守:

  1. 始终检查返回值:内核是脆弱的,任何分配失败(如内存、设备号)都必须有完善的 goto 回滚逻辑。
  2. 保护共享资源:只要有全局变量,就必须考虑并发。互斥锁(Mutex)是最简单有效的保护方式。
  3. 遵循 GPL 协议:在模块末尾声明 MODULE_LICENSE("GPL")

下一步建议:在掌握了基础读写后,可以尝试研究 poll(非阻塞IO)或 unlocked_ioctl(设备控制),这些是通往中级驱动工程师的必经之路。