Linux 字符设备驱动开发:从内核模块到设备节点的全链路实践
一、字符设备驱动的工程定位:为什么它是 Linux 驱动开发的入门必修
Linux 设备驱动分为三类:字符设备、块设备、网络设备。字符设备是最基础的驱动类型——它以字节流的方式提供数据访问,典型的例子包括串口、LED、按键、传感器。
字符设备驱动是 Linux 驱动开发的入门必修课,原因有三。第一,概念完整:字符设备驱动涵盖了内核模块、设备注册、文件操作、中断处理、并发控制等核心概念,是理解 Linux 驱动模型的基础。第二,实现简洁:一个完整的字符设备驱动约 200-300 行代码,可以在一个工作日内完成。第三,调试友好:字符设备通过 /dev 节点访问,可以用 cat、echo 等标准工具测试,无需专门的测试程序。
二、字符设备驱动的核心架构
flowchart TD
A[用户空间] -->|open/read/write/close| B[系统调用接口]
B --> C[VFS 虚拟文件系统]
C --> D[字符设备框架]
D --> E[file_operations 结构体]
E --> F[驱动实现的操作函数]
F --> G[硬件操作]
G --> H[设备寄存器]
subgraph 内核空间
C
D
E
F
G
end
subgraph 驱动注册流程
I[alloc_chrdev_region] --> J[分配设备号]
J --> K[cdev_init] --> L[初始化 cdev]
L --> M[cdev_add] --> N[添加字符设备]
N --> O[device_create] --> P[创建 /dev 节点]
end
设备号:每个字符设备有一个唯一的主设备号和次设备号。主设备号标识驱动程序,次设备号标识同一驱动下的不同设备实例。
cdev 结构体:内核中字符设备的抽象,包含设备号和 file_operations 指针。
file_operations:驱动程序提供给内核的函数指针表,定义了 open、read、write、release 等操作的实现。
设备节点:用户空间通过 /dev/xxx 节点访问字符设备,节点的主设备号与驱动注册的主设备号匹配。
三、字符设备驱动实现
3.1 完整的字符设备驱动
// chardev.c
// Linux 字符设备驱动示例:虚拟传感器设备
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#define DEVICE_NAME "vsensor"
#define BUF_SIZE 256
// 设备私有数据结构
struct vsensor_dev {
struct cdev cdev; // 字符设备结构
struct mutex lock; // 互斥锁,保护并发访问
char data_buf[BUF_SIZE]; // 数据缓冲区
int data_len; // 当前数据长度
dev_t dev_num; // 设备号
};
static struct vsensor_dev *vsensor;
static dev_t dev_num;
static struct class *vsensor_class;
// open 操作:初始化设备状态
static int vsensor_open(struct inode *inode, struct file *filp)
{
struct vsensor_dev *dev;
// 通过 inode 获取设备私有数据
dev = container_of(inode->i_cdev, struct vsensor_dev, cdev);
filp->private_data = dev;
// 初始化缓冲区(模拟传感器数据)
mutex_lock(&dev->lock);
snprintf(dev->data_buf, BUF_SIZE,
"temperature: %.1f\nhumidity: %.1f\n",
25.0 + (prandom_u32() % 100) / 10.0,
60.0 + (prandom_u32() % 100) / 10.0);
dev->data_len = strlen(dev->data_buf);
mutex_unlock(&dev->lock);
pr_info("vsensor: 设备已打开\n");
return 0;
}
// read 操作:将传感器数据拷贝到用户空间
static ssize_t vsensor_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct vsensor_dev *dev = filp->private_data;
int bytes_to_copy;
int ret;
mutex_lock(&dev->lock);
// 如果已经读完,返回 0 表示 EOF
if (*f_pos >= dev->data_len) {
mutex_unlock(&dev->lock);
return 0;
}
// 计算本次可读取的字节数
bytes_to_copy = min(count, (size_t)(dev->data_len - *f_pos));
// 将内核数据拷贝到用户空间
// 必须使用 copy_to_user,不能直接 memcpy
// 因为用户空间地址可能无效或被换出
ret = copy_to_user(buf, dev->data_buf + *f_pos, bytes_to_copy);
if (ret) {
mutex_unlock(&dev->lock);
return -EFAULT; // 拷贝失败,返回错误
}
*f_pos += bytes_to_copy;
mutex_unlock(&dev->lock);
pr_info("vsensor: 读取 %d 字节\n", bytes_to_copy);
return bytes_to_copy;
}
// write 操作:接收用户写入的配置命令
static ssize_t vsensor_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
struct vsensor_dev *dev = filp->private_data;
int bytes_to_copy;
int ret;
char kbuf[BUF_SIZE];
// 限制写入长度,防止缓冲区溢出
bytes_to_copy = min(count, (size_t)(BUF_SIZE - 1));
// 从用户空间拷贝数据到内核
ret = copy_from_user(kbuf, buf, bytes_to_copy);
if (ret) {
return -EFAULT;
}
kbuf[bytes_to_copy] = '\0';
mutex_lock(&dev->lock);
// 处理配置命令(简化实现)
if (strncmp(kbuf, "reset", 5) == 0) {
// 重置传感器数据
snprintf(dev->data_buf, BUF_SIZE,
"temperature: %.1f\nhumidity: %.1f\n",
25.0 + (prandom_u32() % 100) / 10.0,
60.0 + (prandom_u32() % 100) / 10.0);
dev->data_len = strlen(dev->data_buf);
pr_info("vsensor: 传感器数据已重置\n");
}
mutex_unlock(&dev->lock);
return bytes_to_copy;
}
// release 操作:清理设备状态
static int vsensor_release(struct inode *inode, struct file *filp)
{
pr_info("vsensor: 设备已关闭\n");
return 0;
}
// 文件操作函数表
static const struct file_operations vsensor_fops = {
.owner = THIS_MODULE,
.open = vsensor_open,
.read = vsensor_read,
.write = vsensor_write,
.release = vsensor_release,
};
// 模块初始化
static int __init vsensor_init(void)
{
int ret;
// 第一步:分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("vsensor: 分配设备号失败\n");
return ret;
}
pr_info("vsensor: 主设备号 = %d, 次设备号 = %d\n",
MAJOR(dev_num), MINOR(dev_num));
// 第二步:分配设备结构体
vsensor = kzalloc(sizeof(*vsensor), GFP_KERNEL);
if (!vsensor) {
ret = -ENOMEM;
goto fail_alloc;
}
vsensor->dev_num = dev_num;
mutex_init(&vsensor->lock);
// 第三步:初始化并添加 cdev
cdev_init(&vsensor->cdev, &vsensor_fops);
vsensor->cdev.owner = THIS_MODULE;
ret = cdev_add(&vsensor->cdev, dev_num, 1);
if (ret) {
pr_err("vsensor: cdev_add 失败\n");
goto fail_cdev;
}
// 第四步:创建设备类和设备节点
vsensor_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(vsensor_class)) {
ret = PTR_ERR(vsensor_class);
goto fail_class;
}
// device_create 会在 /dev 下自动创建设备节点
// 无需手动 mknod
if (IS_ERR(device_create(vsensor_class, NULL, dev_num, NULL, DEVICE_NAME))) {
ret = -EINVAL;
goto fail_device;
}
pr_info("vsensor: 驱动加载成功\n");
return 0;
fail_device:
class_destroy(vsensor_class);
fail_class:
cdev_del(&vsensor->cdev);
fail_cdev:
kfree(vsensor);
fail_alloc:
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 模块卸载
static void __exit vsensor_exit(void)
{
// 按注册的逆序清理资源
device_destroy(vsensor_class, dev_num);
class_destroy(vsensor_class);
cdev_del(&vsensor->cdev);
kfree(vsensor);
unregister_chrdev_region(dev_num, 1);
pr_info("vsensor: 驱动已卸载\n");
}
module_init(vsensor_init);
module_exit(vsensor_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("Virtual Sensor Character Device Driver");
3.2 编译与测试
# Makefile
obj-m += chardev.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
# 编译驱动
make
# 加载驱动模块
sudo insmod chardev.ko
# 查看设备节点
ls -l /dev/vsensor
# 读取传感器数据
cat /dev/vsensor
# 写入配置命令
echo "reset" > /dev/vsensor
# 查看内核日志
dmesg | tail -20
# 卸载驱动
sudo rmmod chardev
四、架构权衡与适用边界
互斥锁 vs 自旋锁的选择。字符设备的 read/write 操作可能睡眠(如 copy_to_user),必须使用互斥锁(mutex)而非自旋锁(spinlock)。自旋锁在中断上下文中使用,持有期间不允许睡眠。
缓冲区大小的权衡。内核空间的内存有限,缓冲区不宜过大。对于传感器数据,256 字节通常足够。对于需要传输大量数据的场景(如视频帧),应使用 mmap 或 DMA 而非 read/write。
设备节点权限管理。默认创建的设备节点权限为 0600(仅 root 可访问)。生产环境中需要通过 udev 规则设置合适的权限,允许特定用户组访问。
适用边界:字符设备驱动适用于低速、按字节流访问的设备(传感器、LED、按键)。对于需要块级访问的存储设备,应使用块设备驱动。对于网络通信,应使用网络设备驱动。对于高性能数据传输,应考虑 DMA 或 mmap 方案。
五、总结
Linux 字符设备驱动的开发流程包含四个核心步骤:分配设备号(alloc_chrdev_region)、初始化 cdev(cdev_init + cdev_add)、创建设备类和节点(class_create + device_create)、实现 file_operations。关键注意事项:用户空间数据拷贝必须使用 copy_to_user/copy_from_user,并发访问必须用 mutex 保护(read/write 可能睡眠),设备节点通过 device_create 自动创建无需手动 mknod。字符设备适用于低速字节流设备,高性能场景应考虑 DMA 或 mmap。

1599

被折叠的 条评论
为什么被折叠?



