字符设备驱动:从cdev注册到miscdevice的完整实战
字符设备是Linux驱动开发的第一道门。本文基于Linux 6.8内核,拆解cdev注册、file_operations回调、ioctl设计、杂项设备、设备节点管理的完整链路,给出可直接编译运行的代码。
一、字符设备是什么?
字符设备(Character Device)按字节流顺序访问,不支持随机寻址。读写操作直接在驱动与用户空间之间传递数据,不经过页缓存。串口、LED、传感器、GPIO都属于字符设备。
Linux三大设备类型对比
| 维度 | 字符设备 | 块设备 | 网络设备 |
|:-----|:---------|:-------|:---------|
| 访问方式 | 字节流顺序读写 | 按扇区/块随机寻址 | 数据包收发 |
| 页缓* | 不经过 | 经过 | 不经过 |
| 设备节点 | /dev下有节点 | /dev下有节点 | 无节点(套接字接口) |
| 典型设备 | 串口、LED、GPIO、传感器 | 硬盘、eMMC、NVMe | eth0、wlan0 |
| 核心结构 | cdev + file_operations | gendisk + block_device_ops | net_device + net_device_ops |
| 注册API** | cdev_add() | add_disk() | register_netdev() |
典型字符设备举例。
- /dev/ttyS0 。串口,按字节收发数据
- /dev/gpiochip0 。GPIO控制器,通过ioctl配置引脚
- /dev/i2c-0 。I2C适配器,用户态直接发送I2C消息
- /dev/input/event0 。输入设备,读取,按触摸事件
- /dev/null、/dev/zero、/dev/random 。内核虚拟字符设备
二、核心数据结构
字符设备驱动围绕三个核心数据结构展开:file_operations、cdev、dev_t。
file_operations:驱动与VFS的桥。
struct file_operations定义了用户空间系统调用到驱动回调的映射关系。VFS层收到系统调用后,通过file->f_op找到对应的回调函数并调用。
常用回调试
| 回调 | 签名 | 触发系统调用 | 说明 |
|:-----|:-----|:------------|:-----|
| .open | int (*open)(struct inode *, struct file *) | open() | 初始化设备,分配资源 |
| .release | int (*release)(struct inode *, struct file *) | close() | 释放资源 |
| .read | ssize_t (*read)(struct file *, char __user *, size_t, loff_t *) | read() | 内核→用户空间数据传递 |
| .write | ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *) | write() | 用户空间→内核数据传递 |
| .unlocked_ioctl | long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long) | ioctl() | 设备控制命令 |
| .llseek | loff_t (*llseek)(struct file *, loff_t, int) | lseek() | 修改文件偏移 |
| .poll | __poll_t (*poll)(struct file *, struct poll_table_struct *) | poll()/select()/epoll() | 非阻塞I/O多路复用 |
| .mmap | int (*mmap)(struct file *, struct vm_area_struct *) | mmap() | 内存映射 |
不需要的回调留NULL即可,VFS会返回默认行为(如read返回-EINVAL)。
cdev:内核内部表示字符设。
struct cdev {
struct kobject kobj; // 内嵌kobject,sysfs展示
struct module *owner; // 模块所有者,通常设THIS_MODULE
const struct file_operations *ops; // 指向file_operations
struct list_head list; // 同主设备号的cdev链表
dev_t dev; // 设备。 unsigned int count; // 连续次设备号数量
};
cdev是内核管理字符设备的内部实体。注册字符设备就是创建cdev实例、绑定file_operations、关联设备号,然后添加到内核的字符设备子系统就绪
dev_t:设备号
dev_t32位无符号整数,高12位为主设备号,低20位为次设备号。
| | 功能 | 用法 |
|:---|:-----|:-----|
| MKDEV(major, minor) | 构造dev_t | dev_t dev = MKDEV(240, 0); |
| MAJOR(dev) | 提取主设备号 | int major = MAJOR(dev); |
| MINOR(dev) | 提取次设备号 | int minor = MINOR(dev); |
主设备号标识驱动程序,次设备号标识同一驱动下的设备实例。比如主设备。40对应mychardev驱动,次设备。。。分别对应该驱动的三个设备实例。
设备号范围:主设备号0~4095,次设备。~1048575。其中主设备。由内核保留用于动态分配置
三、驱动开发流程
每步对应的API函数。
| 步骤 | API | 作用 |
|:-----|:----|:-----|
| 分配设备号 | alloc_chrdev_region(&devt, baseminor, count, name) | 让内核分配未使用的主设备号 |
| 初始化cdev | cdev_init(&cdev, &fops) | 绑定cdev与file_operations |
| 添加cdev | cdev_add(&cdev, devt, count) | 注册到内核字符设备子系统 |
| 创建设备类 | class_create(name) | 创建/sys/class/下的类目录(6.4+不再需要THIS_MODULE|
| 创建设备节点 | device_create(cls, parent, devt, drvdata, fmt, ...) | 触发udev,/dev下创建节点 |
卸载时逆序清理:device_destroy() class_destroy() cdev_del() unregister_chrdev_region()。
四、完整驱动代。
驱动代码(mychardev.c。
// mychardev.c - 字符设备驱动(Linux 6.8,Ubuntu 24.04 LTS。#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/mutex.h>
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mychardev_class"
#define BUF_SIZE 4096
/* ioctl命令定义 */
#define MYCHAR_MAGIC 'k'
#define MYCHAR_RESET _IO(MYCHAR_MAGIC, 0)
#define MYCHAR_GET_SIZE _IOR(MYCHAR_MAGIC, 1, int)
#define MYCHAR_SET_SIZE _IOW(MYCHAR_MAGIC, 2, int)
#define MYCHAR_EXCHANGE _IOWR(MYCHAR_MAGIC, 3, 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 my_open(struct inode *inode, struct file *filp)
{
/* 通过container_of从cdev获取设备私有数据 */
struct mychar_dev *dev;
dev = container_of(inode->i_cdev, struct mychar_dev, cdev);
filp->private_data = dev;
pr_info("mychardev: opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
pr_info("mychardev: closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct mychar_dev *dev = filp->private_data;
ssize_t ret;
mutex_lock(&dev->lock);
/* 偏移超出缓冲区长度,返回EOF */
if (*ppos >= dev->buf_len) {
mutex_unlock(&dev->lock);
return 0;
}
/* 限制读取长度不超过剩余数*/
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;
ret = count;
mutex_unlock(&dev->lock);
pr_info("mychardev: read %zu bytes, pos=%lld\n", count, *ppos);
return ret;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct mychar_dev *dev = filp->private_data;
ssize_t ret;
mutex_lock(&dev->lock);
/* 缓冲区已*/
if (*ppos >= BUF_SIZE) {
mutex_unlock(&dev->lock);
return -ENOSPC;
}
/* 限制写入长度不超过缓冲区剩余空间 */
if (count > BUF_SIZE - *ppos)
count = BUF_SIZE - *ppos;
/* 从用户空间拷贝数*/
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;
ret = count;
mutex_unlock(&dev->lock);
pr_info("mychardev: wrote %zu bytes, pos=%lld\n", count, *ppos);
return ret;
}
static long my_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
struct mychar_dev *dev = filp->private_data;
int val;
/* 校验魔数*/
if (_IOC_TYPE(cmd) != MYCHAR_MAGIC)
return -ENOTTY;
switch (cmd) {
case MYCHAR_RESET:
mutex_lock(&dev->lock);
memset(dev->buf, 0, BUF_SIZE);
dev->buf_len = 0;
mutex_unlock(&dev->lock);
pr_info("mychardev: buffer reset\n");
break;
case MYCHAR_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 MYCHAR_SET_SIZE:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
if (val < 0 || val > BUF_SIZE)
return -EINVAL;
mutex_lock(&dev->lock);
dev->buf_len = val;
mutex_unlock(&dev->lock);
break;
case MYCHAR_EXCHANGE:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
mutex_lock(&dev->lock);
/* 返回当前缓冲区长度,同时设置新*/
{
int old = (int)dev->buf_len;
if (val >= 0 && val <= BUF_SIZE)
dev->buf_len = val;
mutex_unlock(&dev->lock);
if (copy_to_user((int __user *)arg, &old, sizeof(old)))
return -EFAULT;
}
break;
default:
return -ENOTTY;
}
return 0;
}
/* file_operations:驱动与VFS的接*/
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
static int __init my_init(void)
{
int ret;
/* 1. 动态分配设备号 */
ret = alloc_chrdev_region(&devt, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("mychardev: alloc_chrdev_region failed (%d)\n", ret);
return ret;
}
pr_info("mychardev: major=%d minor=%d\n", MAJOR(devt), MINOR(devt));
/* 2. 初始化cdev */
cdev_init(&mydev.cdev, &my_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 (%d)\n", ret);
goto err_free_buf;
}
/* 5. 创建设备类(Linux 6.4+ 不再需要THIS_MODULE参数)*/
myclass = class_create(CLASS_NAME);
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: loaded, /dev/%s ready\n", DEVICE_NAME);
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 my_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: unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Character device driver example for Linux 6.8");
Makefile
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。
// 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 MYCHAR_MAGIC 'k'
#define MYCHAR_RESET _IO(MYCHAR_MAGIC, 0)
#define MYCHAR_GET_SIZE _IOR(MYCHAR_MAGIC, 1, int)
#define MYCHAR_SET_SIZE _IOW(MYCHAR_MAGIC, 2, int)
#define MYCHAR_EXCHANGE _IOWR(MYCHAR_MAGIC, 3, int)
int main(void)
{
int fd;
char wbuf[] = "Hello from user space!";
char rbuf[64] = {0};
int val;
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open");
return EXIT_FAILURE;
}
/* 写入测试 */
if (write(fd, wbuf, strlen(wbuf)) < 0) {
perror("write");
close(fd);
return EXIT_FAILURE;
}
printf("Written: %s\n", wbuf);
/* ioctl: 获取缓冲区数据长*/
if (ioctl(fd, MYCHAR_GET_SIZE, &val) < 0) {
perror("ioctl GET_SIZE");
close(fd);
return EXIT_FAILURE;
}
printf("Buffer data length: %d\n", val);
/* 回到起始位置读取 */
lseek(fd, 0, SEEK_SET);
if (read(fd, rbuf, sizeof(rbuf) - 1) < 0) {
perror("read");
close(fd);
return EXIT_FAILURE;
}
printf("Read back: %s\n", rbuf);
/* ioctl: 交换——传入新长度,返回旧长度 */
val = 10;
if (ioctl(fd, MYCHAR_EXCHANGE, &val) < 0) {
perror("ioctl EXCHANGE");
close(fd);
return EXIT_FAILURE;
}
printf("Exchange: old_len=%d, set new_len=10\n", val);
/* ioctl: 重置缓冲*/
if (ioctl(fd, MYCHAR_RESET) < 0) {
perror("ioctl RESET");
close(fd);
return EXIT_FAILURE;
}
printf("Buffer reset done\n");
close(fd);
return EXIT_SUCCESS;
}
编译与运。
# 编译驱动
make
# 加载模块
sudo insmod mychardev.ko
# 查看分配的设备号
dmesg | grep mychardev | tail -3
# 编译测试程序
gcc -o test_mychardev test_mychardev.c
# 运行测试
sudo ./test_mychardev
# 卸载模块
sudo rmmod mychardev
五、ioctl设计
ioctl是字符设备驱动的控制通道,用于执行read/write无法覆盖的操作:配置参数、获取状态、执行命令。
命令编码。
| 方向 | 参数 | 场景 | |
|---|---|---|---|
_IO(type, nr) |
无数据 | 魔数+序号 | 重置设备、启停操作 |
_IOR(type, nr, datatype) |
内核→用途 | +数据类型 | 读取设备状态 |
_IOW(type, nr, datatype) |
用户→内核 | +数据类型 | 设置设备参数 |
_IOWR(type, nr, datatype) |
双向 | +数据类型 | 交换配置数据 |
命令号编码规。
ioctl命令号是32位整数,编码格式。
| 位段 | 位数 | 含义 |
|:-----|:-----|:-----|
| 31~30 | 2| 方向(_IOC_NONE/_IOC_READ/_IOC_WRITE|
| 29~16 | 14| 数据大小(sizeof(datatype)|
| 15~8 | 8| 魔数字(type),标识驱动 |
| 7~0 | 8| 序号(nr),区分同一驱动的不同命 |
解码宏:
_IOC_TYPE(cmd) // 提取魔数。_IOC_NR(cmd) // 提取序号
_IOC_DIR(cmd) // 提取方向
_IOC_SIZE(cmd) // 提取数据大小
ioctl实现要点
- 魔数字选唯一:避免与其他驱动冲突,推荐用ASCII字符(如
'k'、'm'),不要用0或常见字符。2. 校验命令合法:先检查_IOC_TYPE(cmd)是否匹配,不匹配返回-ENOTTY - 校验用户指针:
_IOR命令用copy_to_user,_IOW命令用copy_from_user,不要直接解引用用户空间指针 - 第三个参数是类型:
_IOR('k', 1, int)中的int是类型名,宏内部自动计算sizeof(int)
六、杂项设备(miscdevice。
杂项设备是字符设备的简化封装,主设备号固定。0,由内核自动分配次设备号。适合功能简单、只需一个设备节点的驱动。
miscdevice结构建
struct miscdevice {
int minor; // 次设备号,MISC_DYNAMIC_MINOR表示自动分配
const char *name; // 设备名,/dev/下的节点。 const struct file_operations *fops; // file_operations
struct list_head list; // 内核内部链表
struct device *parent; // 父设。 struct device *this_device;// 内核创建的device指针
const struct attribute_group **groups;
const char *nodir_name;
};
注册与注销
int misc_register(struct miscdevice *misc); // 注册
void misc_deregister(struct miscdevice *misc); // 注销
misc_register内部完成了字符设备驱动的全部注册工作:分配次设备号、创建cdev、创建设备类、创建设备节点。调用一个函数等于替代了alloc_chrdev_region + cdev_init + cdev_add + class_create + device_create五步。
LED控制misc设备示例
// led_misc.c - LED控制杂项设备驱动(Linux 6.8。#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "myled"
/* 模拟LED状态*/
static int led_state = 0;
static int led_open(struct inode *inode, struct file *filp)
{
pr_info("myled: opened\n");
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
pr_info("myled: closed\n");
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
char state_str[2];
if (*ppos > 0)
return 0; /* 只读一*/
state_str[0] = led_state ? '1' : '0';
state_str[1] = '\n';
if (count < 2)
return -EINVAL;
if (copy_to_user(buf, state_str, 2))
return -EFAULT;
*ppos = 2;
return 2;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
char kbuf[4] = {0};
if (count > sizeof(kbuf) - 1)
count = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
if (kbuf[0] == '1') {
led_state = 1;
pr_info("myled: LED ON\n");
} else if (kbuf[0] == '0') {
led_state = 0;
pr_info("myled: LED OFF\n");
} else {
return -EINVAL;
}
return count;
}
static long led_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)
{
switch (cmd) {
case 0: /* 关灯 */
led_state = 0;
pr_info("myled: ioctl LED OFF\n");
break;
case 1: /* 开销*/
led_state = 1;
pr_info("myled: ioctl LED ON\n");
break;
default:
return -ENOTTY;
}
return 0;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.read = led_read,
.write = led_write,
.unlocked_ioctl = led_ioctl,
};
/* 杂项设备定义 */
static struct miscdevice led_misc = {
.minor = MISC_DYNAMIC_MINOR, /* 自动分配次设备号 */
.name = DEVICE_NAME, /* /dev/myled */
.fops = &led_fops,
};
static int __init led_init(void)
{
int ret;
ret = misc_register(&led_misc);
if (ret < 0) {
pr_err("myled: misc_register failed (%d)\n", ret);
return ret;
}
pr_info("myled: registered, /dev/%s ready\n", DEVICE_NAME);
return 0;
}
static void __exit led_exit(void)
{
misc_deregister(&led_misc);
pr_info("myled: unregistered\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("LED misc device driver example for Linux 6.8");
LED测试。
# 加载驱动
sudo insmod led_misc.ko
# 开销echo 1 | sudo tee /dev/myled
# 关灯
echo 0 | sudo tee /dev/myled
# 读取状态cat /dev/myled
# 卸载
sudo rmmod led_misc
字符设备 vs 杂项设备对比
| 维度 | 字符设备(cdev | 杂项设备(miscdevice |
|---|---|---|
| 主设备号 | 动态分配或静态指 | 固定。0 |
| 次设备号 | 手动管理,范围灵 | 自动分配或指 |
| 注册API | alloc_chrdev_region + cdev_init + cdev_add + class_create + device_create | misc_register一步完 |
| 注销API | device_destroy + class_destroy + cdev_del + unregister_chrdev_region | misc_deregister一步完 |
| *代码 | 多(50~80行注。注销代码 | 少(10~20行) |
| *多设备实 | 支持,一次注册多个次设备 | 不支持,每个miscdevice只有一个节 |
| 适用场景 | 复杂设备、多实例、自定义主设备号 | 简单设备、单实例、快速开销 |
| 典型用户 | I2C适配器、帧缓冲、输入设 | 看门狗、随机数发生器、LED、传感器 |
选择原则:设备逻辑简单、只需一个节点,用miscdevice;需要多实例或自定义主设备号,用cdev。
七、设备节点管理
设备节点是用户空间访问驱动的入口,存在于/dev目录下。创建方式有三种。
udev自动创建(推荐)
udev监听内核uevent,根据class_create和device_create发出的事件自动在/dev下创建节点。这是最常用的方式,也是本文驱动代码使用的方式。
流程。
1. 驱动调用class_create()创建设备类,在/sys/class/下生成目。2. 驱动调用device_create()创建设备,内核发出uevent
3. udev收到uevent,在/dev/下创建对应的设备节点
/* 创建*/
struct class *cls = class_create("my_class");
/* 创建设备节点,自动触发udev */
struct device *dev = device_create(cls, NULL, devt, NULL, "mydev");
/* 销*/
device_destroy(cls, devt);
class_destroy(cls);
手动创建(mknod。
没有udev的嵌入式系统,或调试时需要指定权限,可以用mknod手动创建。
# 格式:mknod 设备路径 c 主设备号 次设备号
sudo mknod /dev/mychardev c 240 0
# 设置权限
sudo chmod 666 /dev/mychardev
# 删除节点
sudo rm /dev/mychardev
手动创建的节点重启后消失,需要写在启动脚本里。
devtmpfs自动节点
内核开启CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT后,内核在cdev_add()时自动在/dev下创建节点。这是内核级自动创建,不依赖udev。
# 查看是否已挂载devtmpfs
mount | grep devtmpfs
# 手动挂载(嵌入式系统启动脚本中)
mount -t devtmpfs devtmpfs /dev
devtmpfs创建的节点权限默认为0600,root用户才能访问。如需修改权限,仍需udev规则或手动chmod。
设备权限管理
udev规则可以精确控制设备节点的权限和属主。
# /etc/udev/rules.d/99-mychardev.rules
# mychardev设备:所有用户可读写
KERNEL=="mychardev", MODE="0666"
# 指定用户。KERNEL=="mychardev", GROUP="dialout", MODE="0660"
应用规则。
sudo udevadm control --reload-rules
sudo udevadm trigger
三种节点创建方式对比
| 方式 | 依赖 | 权限控制 | 重启持久 | 适用场景 |
|:-----|:-----|:---------|:---------|:---------|
| udev自动 | udev守护进程 | udev规则灵活控制 | 持久 | 桌面Linux、有udev的嵌入式 |
| mknod手动 | | chmod/chown | 不持 | 调试、无udev的最小系 |
| devtmpfs | 内核CONFIG | 默认0600,需udev或chmod补充 | 持久 | 嵌入式系统、早期启动阶 |
八、常见问题
Q1:insmod。dev下没有设备节点?
检查class_create()和device_create()是否返回了错误指针。用dmesg查看是否。failed"日志。嵌入式系统还需确认udev正在运行,或devtmpfs已挂载。
*Q2:open返回ENODEV
两个原因:设备号未注册,或设备节点未创建。用cat /proc/devices确认主设备号已注册,用ls -l /dev/xxx确认节点存在且主次设备号匹配置
*Q3:copy_to_user返回EFAULT
用户空间指针无效。常见原因:用户程序传了NULL指针、传了未映射的地址、传了只读区域的指针给copy_from_user。驱动侧无法修复,需要修正用户程序的传参数
*Q4:class_create编译报错"too many arguments"
Linux 6.4起class_create()移除了THIS_MODULE参数。.8内核只需传类名:class_create("my_class")。如果需要兼容旧内核,用LINUX_VERSION_CODE做条件编译。
*Q5:rmmod。Device or resource busy"
设备文件仍被进程打开。cdev.owner = THIS_MODULE会使模块引用计数在设备打开时递增,防止使用中被卸载。先关闭所有打开的fd,再rmmod。用lsof /dev/mychardev查看谁在占用。
*Q6:misc_register返回EBUSY
次设备号冲突。如果指定了固定minor值,可能已被其他misc设备占用。改为MISC_DYNAMIC_MINOR让内核自动分配置
九、总结
字符设备驱动的核心是三个数据结构的协作:dev_t标识设备,cdev管理内核注册,file_operations实现功能接口。开发流程就。分配设备号→初始化cdev→添加到内核→创建节。四步,卸载时逆序清理。
杂项设备是字符设备的快捷方式,misc_register一个调用替代五步注册,适合简单设备。设备节点管理首选udev自动创建,嵌入式最小系统用devtmpfs或mknod。
速查表:
| 操作 | API | 备注 |
|---|---|---|
| 分配设备号 | alloc_chrdev_region(&devt, 0, 1, name) |
动态分配,推荐 |
| 静态指定设备号 | register_chrdev_region(devt, 1, name) |
需确保不冲 |
| 初始化cdev | cdev_init(&cdev, &fops) |
绑定file_operations |
| 注册cdev | cdev_add(&cdev, devt, 1) |
此后设备号可被open |
| 删除cdev | cdev_del(&cdev) |
卸载时调试 |
| 创建设备类 | class_create(name) |
6.4+无需THIS_MODULE |
| 销毁设备类 | class_destroy(cls) |
卸载时调试 |
| 创建设备节点 | device_create(cls, NULL, devt, NULL, name) |
触发udev |
| 销毁设备节 | device_destroy(cls, devt) |
卸载时调试 |
| 释放设备 | unregister_chrdev_region(devt, 1) |
卸载时调试 |
| 注册杂项设备 | misc_register(&miscdev) |
一步完 |
| 注销杂项设备 | misc_deregister(&miscdev) |
一步完 |
| 内核→用途 | copy_to_user(to, from, n) |
返回0成功 |
| 用户→内核 | copy_from_user(to, from, n) |
返回0成功 |
本文首发于linuxros.cn,转载请注明出处。