嵌入式Linux字符设备驱动开发:从内核模块到用户接口
字符设备是Linux驱动开发的基础入口。本文从核心数据结构出发,拆解cdev注册、file_operations回调、设备号分配、自动节点创建的完整流程,给出兼容Linux 6.x的可运行代码,帮你避开API版本迁移的坑。
一、原理简析
字符设备(Character Device)是Linux三大设备类型之一,与块设备、网络设备并列。它的核心特征是按字节流顺序访问,不支持随机寻址,数据通常不经页缓存直接在驱动与用户空间之间传递。串口、键盘、LED、传感器等都是典型的字符设备。
理解字符设备驱动,关键是掌握三个核心数据结构及其关系:
dev_t:设备号,32位无符号整数,高12位为主设备号(标识驱动),低20位为次设备号(标识设备实例)。通过MKDEV(major, minor)构造,MAJOR(dev)和MINOR(dev)分解。struct cdev:内核内部表示字符设备的核心结构体,内嵌kobject、owner指针、file_operations指针、设备号和次设备计数。struct file_operations:驱动与VFS之间的桥梁,定义了open、read、write、release、unlocked_ioctl等回调函数,用户空间的系统调用最终分派到这里。
整个调用链路:用户空间open()/read()/write() → 系统调用 → VFS层 → file_operations回调 → 驱动程序 → 硬件设备。
二、开发流程
字符设备驱动开发就是"注册-实现-销毁",具体分七步:
第一步:分配设备号。调用alloc_chrdev_region()让内核动态分配一个未使用的主设备号,或用register_chrdev_region()静态指定。推荐动态分配,避免冲突。
第二步:初始化cdev。调用cdev_init()将cdev结构体与file_operations绑定,并设置cdev.owner = THIS_MODULE。
第三步:添加cdev到内核。调用cdev_add()将cdev注册到内核,此后内核就知道该设备号对应的操作函数集。
第四步:创建设备类。调用class_create()创建一个设备类,为后续自动创建设备节点做准备。
第五步:创建设备节点。调用device_create()在/dev下自动创建设备文件,udev/mdev会根据类和设备信息生成节点。
第六步:实现file_operations回调。编写open、release、read、write、unlocked_ioctl等函数,完成具体的设备操作逻辑。
第七步:模块卸载时逆序清理。依次调用device_destroy()、class_destroy()、cdev_del()、unregister_chrdev_region()释放所有资源。
三、代码实现
完整驱动代码
// mychardev.c - 字符设备驱动示例(兼容 Linux 5.x/6.x)
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/slab.h>
#define DEVICE_NAME "mychardev"
#define BUF_SIZE 4096
#define CLASS_NAME "mychardev_class"
/* ioctl 命令定义 */
#define MYCHARDEV_MAGIC 'k'
#define MYCHARDEV_RESET _IO(MYCHARDEV_MAGIC, 0)
#define MYCHARDEV_GET_SIZE _IOR(MYCHARDEV_MAGIC, 1, int)
#define MYCHARDEV_SET_SIZE _IOW(MYCHARDEV_MAGIC, 2, int)
/* 设备私有数据结构 */
struct mychar_dev {
struct cdev cdev;
char *buf;
size_t buf_len;
struct mutex lock;
};
static struct mychar_dev mydev;
static dev_t devt;
static struct class *myclass;
static struct device *mydevice;
static int mydev_open(struct inode *inode, struct file *filp)
{
struct mychar_dev *dev;
dev = container_of(inode->i_cdev, struct mychar_dev, cdev);
filp->private_data = dev;
pr_info("mychardev: device opened\n");
return 0;
}
static int mydev_release(struct inode *inode, struct file *filp)
{
pr_info("mychardev: device closed\n");
return 0;
}
static ssize_t mydev_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct mychar_dev *dev = filp->private_data;
ssize_t bytes_read;
mutex_lock(&dev->lock);
if (*ppos >= dev->buf_len) {
mutex_unlock(&dev->lock);
return 0; /* EOF */
}
if (count > dev->buf_len - *ppos)
count = dev->buf_len - *ppos;
if (copy_to_user(buf, dev->buf + *ppos, count)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*ppos += count;
bytes_read = count;
mutex_unlock(&dev->lock);
pr_info("mychardev: read %zu bytes\n", count);
return bytes_read;
}
static ssize_t mydev_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct mychar_dev *dev = filp->private_data;
ssize_t bytes_written;
mutex_lock(&dev->lock);
if (*ppos + count > BUF_SIZE)
count = BUF_SIZE - *ppos;
if (count == 0) {
mutex_unlock(&dev->lock);
return -ENOSPC; /* 缓冲区已满 */
}
if (copy_from_user(dev->buf + *ppos, buf, count)) {
mutex_unlock(&dev->lock);
return -EFAULT;
}
*ppos += count;
if (*ppos > dev->buf_len)
dev->buf_len = *ppos;
bytes_written = count;
mutex_unlock(&dev->lock);
pr_info("mychardev: wrote %zu bytes\n", count);
return bytes_written;
}
static long mydev_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct mychar_dev *dev = filp->private_data;
int ret = 0;
int val;
switch (cmd) {
case MYCHARDEV_RESET:
mutex_lock(&dev->lock);
memset(dev->buf, 0, BUF_SIZE);
dev->buf_len = 0;
*filp->f_pos = 0;
mutex_unlock(&dev->lock);
pr_info("mychardev: buffer reset\n");
break;
case MYCHARDEV_GET_SIZE:
mutex_lock(&dev->lock);
val = (int)dev->buf_len;
mutex_unlock(&dev->lock);
if (copy_to_user((int __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
case MYCHARDEV_SET_SIZE:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
mutex_lock(&dev->lock);
if (val < 0 || val > BUF_SIZE) {
mutex_unlock(&dev->lock);
return -EINVAL;
}
dev->buf_len = val;
mutex_unlock(&dev->lock);
break;
default:
return -ENOTTY;
}
return ret;
}
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_release,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
};
static int __init mydev_init(void)
{
int ret;
/* 1. 分配设备号 */
ret = alloc_chrdev_region(&devt, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("mychardev: failed to alloc device number\n");
return ret;
}
pr_info("mychardev: major=%d, minor=%d\n",
MAJOR(devt), MINOR(devt));
/* 2. 初始化cdev */
cdev_init(&mydev.cdev, &mydev_fops);
mydev.cdev.owner = THIS_MODULE;
/* 3. 分配缓冲区 */
mydev.buf = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!mydev.buf) {
ret = -ENOMEM;
goto err_unregister;
}
mydev.buf_len = 0;
mutex_init(&mydev.lock);
/* 4. 添加cdev到内核 */
ret = cdev_add(&mydev.cdev, devt, 1);
if (ret < 0) {
pr_err("mychardev: cdev_add failed\n");
goto err_free_buf;
}
/* 5. 创建设备类(兼容 Linux 6.4+) */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
myclass = class_create(CLASS_NAME);
#else
myclass = class_create(THIS_MODULE, CLASS_NAME);
#endif
if (IS_ERR(myclass)) {
pr_err("mychardev: class_create failed\n");
ret = PTR_ERR(myclass);
goto err_cdev_del;
}
/* 6. 创建设备节点 */
mydevice = device_create(myclass, NULL, devt, NULL, DEVICE_NAME);
if (IS_ERR(mydevice)) {
pr_err("mychardev: device_create failed\n");
ret = PTR_ERR(mydevice);
goto err_class_destroy;
}
pr_info("mychardev: module loaded successfully\n");
return 0;
err_class_destroy:
class_destroy(myclass);
err_cdev_del:
cdev_del(&mydev.cdev);
err_free_buf:
kfree(mydev.buf);
err_unregister:
unregister_chrdev_region(devt, 1);
return ret;
}
static void __exit mydev_exit(void)
{
device_destroy(myclass, devt);
class_destroy(myclass);
cdev_del(&mydev.cdev);
kfree(mydev.buf);
mutex_destroy(&mydev.lock);
unregister_chrdev_region(devt, 1);
pr_info("mychardev: module unloaded\n");
}
module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("Character Device Driver Example");
Makefile
# Makefile for mychardev driver
obj-m += mychardev.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
用户空间测试程序
// test_mychardev.c - 用户空间测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define DEVICE_PATH "/dev/mychardev"
#define MYCHARDEV_MAGIC 'k'
#define MYCHARDEV_RESET _IO(MYCHARDEV_MAGIC, 0)
#define MYCHARDEV_GET_SIZE _IOR(MYCHARDEV_MAGIC, 1, int)
#define MYCHARDEV_SET_SIZE _IOW(MYCHARDEV_MAGIC, 2, int)
int main(void)
{
int fd;
char write_buf[] = "Hello, char device!";
char read_buf[64] = {0};
int size;
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
/* 写入测试 */
if (write(fd, write_buf, strlen(write_buf)) < 0) {
perror("write failed");
close(fd);
return EXIT_FAILURE;
}
printf("Written: %s\n", write_buf);
/* ioctl: 获取缓冲区大小 */
if (ioctl(fd, MYCHARDEV_GET_SIZE, &size) < 0) {
perror("ioctl GET_SIZE failed");
close(fd);
return EXIT_FAILURE;
}
printf("Buffer size: %d\n", size);
/* 回到文件起始位置读取 */
lseek(fd, 0, SEEK_SET);
if (read(fd, read_buf, sizeof(read_buf) - 1) < 0) {
perror("read failed");
close(fd);
return EXIT_FAILURE;
}
printf("Read: %s\n", read_buf);
/* ioctl: 重置缓冲区 */
if (ioctl(fd, MYCHARDEV_RESET) < 0) {
perror("ioctl RESET failed");
close(fd);
return EXIT_FAILURE;
}
printf("Buffer reset done\n");
close(fd);
return EXIT_SUCCESS;
}
编译测试程序:
gcc -o test_mychardev test_mychardev.c
加载驱动并测试:
# 编译驱动
make
# 加载模块
sudo insmod mychardev.ko
# 查看分配的主设备号
dmesg | tail -5
# 运行测试
sudo ./test_mychardev
# 卸载模块
sudo rmmod mychardev
四、对比表格
新旧API对比
| 维度 | 旧版API | 新版API |
|---|---|---|
| 设备号注册 | register_chrdev() |
alloc_chrdev_region() + cdev_add() |
| 设备号注销 | unregister_chrdev() |
unregister_chrdev_region() + cdev_del() |
| cdev管理 | 内部隐式管理 | 显式cdev_init()/cdev_add()/cdev_del() |
| 次设备号范围 | 固定0~255 | 灵活指定,支持0~1048575 |
| 多设备实例 | 需要多次调用 | 一次注册多个次设备号 |
| 设备类创建 | class_create(THIS_MODULE, name) |
class_create(name)(6.4+) |
| 推荐程度 | 仅兼容旧内核 | 新开发必须使用 |
ioctl命令宏对比
| 宏 | 方向 | 参数 | 典型场景 |
|---|---|---|---|
_IO(type, nr) |
无数据传输 | 魔数+序号 | 重置设备、启动/停止 |
_IOR(type, nr, datatype) |
内核→用户 | +数据类型 | 读取设备状态 |
_IOW(type, nr, datatype) |
用户→内核 | +数据类型 | 设置设备参数 |
_IOWR(type, nr, datatype) |
双向 | +数据类型 | 交换配置数据 |
注意:
_IOR/_IOW/_IOWR的第三个参数是数据类型的名称(如int、struct my_config),不是sizeof()的结果。宏内部会自动计算大小。
五、核心流程图
字符设备驱动注册流程
用户空间系统调用到驱动的分派过程
模块卸载清理流程
六、常见问题解决
Q1:insmod后/dev下没有设备节点?
检查内核是否支持udev/mdev。嵌入式系统需确保CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT已开启,或在启动脚本中手动挂载:mount -t devtmpfs devtmpfs /dev。同时确认device_create()未返回错误指针。
Q2:class_create编译报错"too many arguments"?
Linux 6.4起class_create()移除了THIS_MODULE参数,只接受类名字符串。这是Greg Kroah-Hartman在2023年3月提交的改动,原因是owner参数从未被实际使用。解决方案是用LINUX_VERSION_CODE宏做条件编译,如本文代码所示。
Q3:copy_to_user/copy_from_user返回非零值?
返回值是未能拷贝的字节数,不是已拷贝的字节数。返回0表示全部成功,返回非零应返回-EFAULT给用户空间。常见原因:用户空间指针非法、内核缓冲区越界。
Q4:ioctl命令返回-ENOTTY?
-ENOTTY表示"不合适的ioctl操作",驱动收到无法识别的命令时应返回此值。检查用户空间的命令宏定义是否与驱动一致,特别是魔数字符和序号。
Q5:rmmod提示"Device or resource busy"?
设备文件仍被用户进程打开。cdev.owner = THIS_MODULE确保设备打开期间模块引用计数不为零,防止正在使用时被卸载。先关闭所有打开的文件描述符,再rmmod。
七、总结
字符设备驱动开发的核心是理解"设备号-cdev-file_operations"三者关系:设备号是寻址标识,cdev是内核管理实体,file_operations是功能实现入口。新版API将注册过程拆分为独立的步骤,虽然代码量增加,但带来了更灵活的次设备号管理和更清晰的资源生命周期控制。实际开发中,注意Linux 6.4对class_create()的签名变更,用条件编译保证跨版本兼容。下期讲中断处理与等待队列,实现带阻塞I/O的字符设备驱动。