ESC
输入关键词搜索文章标题和内容

字符设备驱动:从cdev注册到miscdevice的完整实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

字符设备驱动:从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。其中主设备。由内核保留用于动态分配置

三、驱动开发流程

flowchart TB A(["开始:模块加载"]) --> B["定义 file_operations<br/>绑定回调函数"] B --> C["实现 open/read/write/ioctl<br/>等回调函数"] C --> D["alloc_chrdev_region()<br/>动态分配设备号"] D --> E{"分配成功?"} E -->|"| F(["返回错误"]) E -->|"| G["cdev_init()<br/>初始化cdev + 绑定fops"] G --> H["cdev.owner = THIS_MODULE"] H --> I["cdev_add()<br/>将cdev注册到内存"] I --> J{"注册成功?"} J -->|"| K["unregister_chrdev_region()"] K --> F J -->|"| L["class_create()<br/>创建设备"] L --> M["device_create()<br/>。dev下创建节"] M --> N{"创建成功?"} N -->|"| O["逆序清理"] O --> F N -->|"| P(["驱动就绪<br/>/dev/xxx 可访"]) style A fill:#E8F5E9 style P fill:#E8F5E9 style F fill:#FFEBEE style E fill:#FFF8E1 style J fill:#FFF8E1 style N fill:#FFF8E1 style B fill:#E3F2FD style C fill:#E3F2FD style D fill:#E3F2FD style G fill:#E3F2FD style H fill:#E3F2FD style I fill:#E3F2FD style L fill:#E3F2FD style M fill:#E3F2FD style K fill:#FFEBEE style O fill:#FFEBEE

每步对应的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实现要点

  1. 魔数字选唯一:避免与其他驱动冲突,推荐用ASCII字符(如'k'、'm'),不要用0或常见字符。2. 校验命令合法:先检查_IOC_TYPE(cmd)是否匹配,不匹配返回-ENOTTY
  2. 校验用户指针:_IOR命令用copy_to_user,_IOW命令用copy_from_user,不要直接解引用用户空间指针
  3. 第三个参数是类型:_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。

来自 linuxros.cn · linuxROS

七、设备节点管理

设备节点是用户空间访问驱动的入口,存在于/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,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页