定义

udev与mdev

udev 是一种工具,它能够根据系统中的硬件设备的状态动态更新设备文件,包括设备文件的创建,删除等。

  • 设备文件通常放在/dev 目录下。
  • 使用 udev 后,在/dev 目录下就只包含系统中真正存在的设备。

mdevudev 的简化版本,是 busybox 中所带的程序,最适合用在嵌入式系统

  • udev 一般用在 PC 上的 linux中,相对 mdev 来说要复杂些
  • 所以在嵌入式 Linux 中使用 mdev 来实现设备节点文件的自动创建和删除。

自动创建节点步骤

  • 使用class_create函数创建一个类
  • 使用device_create函数在创建的类中创建对应的设备

相关函数

创建和删除类函数

struct class 结构体用于表示一个设备类。设备类是一组具有相同属性或行为的设备的集合

  • struct class定义
#include<linux/device.h>
/**
 * @brief 表示一个设备类的内核结构体
 *
 * 结构体 class 用于定义和管理一个设备类。设备类可以包含多个设备,这些设备
 * 通常具有相似的属性或行为。例如,所有的字符设备可以属于一个名为"char"的设备类。
 * 每个设备类都有一个与之关联的类名,并且可以有一个与之关联的设备模型。
 *
 * @note 设备类通常在驱动程序初始化时创建,并在驱动程序卸载时销毁。
 */
struct class {
    const char *name;                  ///< 类名,用于标识设备类
    struct module *owner;               ///< 拥有此类的模块,用于模块归属和引用计数
    struct class_attribute *class_attrs; ///< 类属性,用于定义类级别的属性
 
    struct device *dev;                ///< 类的设备实例,指向第一个设备
    struct kobject kobj;                ///< 内核对象,用于内核对象管理
 
    int (*dev_release)(struct device *dev); ///< 设备释放函数,用于释放设备资源
 
    struct attribute_group **groups;    ///< 属性组,用于定义类和设备属性的集合
 
    // 其他成员变量和函数指针...
};

内核同时提供了class_create用来创建一个类,这个类存放于sysfs下面

  • 一旦创建好了这个类,再调用device_create/dev目录下创建相应的设备节点。
  • 加载模块的时候,用户空间中的udev会自动响应device_create,去/sysfs下寻找对应的类从而创建设备节点。
  • 在 Linux 驱动程序中一般通过class_createclass_destroy来完成设备节点的创建和删除。
  • class_create
/**
 * @brief 创建一个新的设备类
 *
 * 该函数用于在内核中创建一个新的设备类。设备类是具有相同特性或用途的设备的集合。
 * 创建成功后,返回一个指向新创建的设备类的指针,该指针可以用于后续的设备管理操作。
 *
 * @param owner 指向创建该类的模块,通常为THIS_MODULE,表示该类所属的模块
 * @param name 类的名称,用于唯一标识一个设备类
 * @return 返回指向新创建的设备类的指针,如果创建失败则返回NULL
 */
struct class *class_create(struct module *owner, const char *name);
  • class_destroy
/**
 * @brief 删除一个设备类
 *
 * 该函数用于删除一个已经存在的设备类。在驱动程序卸载时,应当使用此函数来清理
 * 之前创建的设备类,释放相关资源。
 *
 * @param cls 指向要删除的设备类的指针,该指针通常由class_create函数返回
 */
void class_destroy(struct class *cls);

创建和删除设备函数

  • device_create
/**
 * @brief 在指定的设备类中创建一个新的设备实例
 *
 * 该函数用于在指定的设备类中创建一个新的设备实例,并将其添加到内核的设备树中。
 * 创建的设备实例会与提供的设备号(dev_t)关联,并可以包含一个指向驱动数据的指针。
 * 此外,还可以为设备指定一个父设备,以及一个格式化的设备名称。
 *
 * @param class   指向设备类的指针,该类是新设备所属的类别
 * @param parent  指向父设备的指针,可以为NULL,表示没有父设备
 * @param devt    设备号,用于唯一标识设备
 * @param drvdata 指向驱动私有数据的指针,可以为NULL
 * @param fmt     设备名称的格式化字符串
 * @param ...     格式化字符串的参数
 * @return 返回指向新创建的设备实例的指针,如果创建失败则返回NULL
 */
struct device *device_create(struct class *class,
                             struct device *parent,
                             dev_t devt,
                             void *drvdata,
                             const char *fmt, ...);
  • device_destroy
/**
 * @brief 删除指定类中的设备实例
 *
 * 该函数用于删除之前通过device_create()创建的设备实例。当设备不再被使用或者
 * 驱动程序卸载时,应当调用此函数来移除设备实例,并释放相关资源。
 *
 * @param class 指向设备所属类的指针。这个类应该与创建设备时使用的类相同。
 * @param devt  设备的设备号(dev_t),用于唯一标识要删除的设备。
 */
void device_destroy(struct class *class, dev_t devt);

示例

创建类函数

  • 完整代码
/*
 * @Description:字符设备自动创建设备节点步骤一创建类,创建设备
 */
#include <linux/init.h>   //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h>     //包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations
#include <linux/kdev_t.h>
#include <linux/cdev.h>        //对字符设备结构cdev以及一系列的操作函数的定义。//包含了cdev 结构及相关函数的定义。
#define DEVICE_NUMBER 1        //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0  //定义次设备号的起始地址
#include <linux/device.h>      //包含了device、class 等结构的定义
#define DEVICE_CLASS_NAME "chrdev_class"
#define DEVICE_NODE_NAME "chrdev_test" //宏定义设备节点的名字
static int major_num, minor_num;       //定义主设备号和次设备号
 
struct class *class;                   /* 类 */
struct cdev cdev;                      //定义一个cdev结构体
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数minor_num
dev_t dev_num;                         /* 设备号 */
 
/**
 * @description: 打开设备
 * @param {structinode} *inode:传递给驱动的 inode
 * @param {structfile} *file:设备文件,file 结构体有个叫做 private_data 的成员变量,
 *  一般在 open 的时候将 private_data 指向设备结构体。
 * @return: 0 成功;其他 失败 
 */
int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open\n");
    return 0;
}
// 设备操作函数结构体
struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open};
/**
 * @description: 驱动入口函数
 * @param {*}无
 * @return {*} 0 成功;其他 失败
 */
static int hello_init(void)
{
    int ret; //函数返回值
    if (major_num)
    {
        /*静态注册设备号*/
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
 
        dev_num = MKDEV(major_num, minor_num);                              //MKDEV将主设备号和次设备号合并为一个设备号
        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
        if (ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
        printk("register_chrdev_region ok\n"); //静态注册设备号成功
    }
    else
    {
        /*动态注册设备号*/
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
        if (ret < 0)
        {
            printk("alloc_chrdev_region error\n");
        }
        printk("alloc_chrdev_region ok\n"); //动态注册设备号成功
 
        major_num = MAJOR(dev_num);            //将主设备号取出来
        minor_num = MINOR(dev_num);            //将次设备号取出来
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
    }
    // 初始化 cdev
    cdev.owner = THIS_MODULE;
    cdev_init(&cdev, &chrdev_ops);
    // 向系统注册设备
    cdev_add(&cdev, dev_num, DEVICE_NUMBER);
    // 创建 class 类
    class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
    return 0;
}
/**
 * @description: 驱动出口函数
 * @param {*}无
 * @return {*}无
 */
static void hello_exit(void)
{
    //注销设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
    //删除设备
    cdev_del(&cdev);
    //删除类
    class_destroy(class);
    printk("gooodbye! \n");
}
// 将上面两个函数指定为驱动的入口和出口函数
module_init(hello_init);
module_exit(hello_exit);
//  LICENSE 和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
  • makefile
obj-m += chrdev.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
	make -C $(KDIR) M=$(PWD) clean

在创建类成功后,会在/sys/class生成名为chrdev_class的类

  • 使用ls /sys/class查看
  • 使用insmod chrdev.ko ls /sys/class加载驱动并查看设备类

创建设备函数

/*
 * @Description:字符设备自动创建设备节点步骤一创建类,创建设备
 */
#include <linux/init.h>   //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h>     //包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations
#include <linux/kdev_t.h>
#include <linux/cdev.h>        //对字符设备结构cdev以及一系列的操作函数的定义。//包含了cdev 结构及相关函数的定义。
#define DEVICE_NUMBER 1        //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0  //定义次设备号的起始地址
#include <linux/device.h>      //包含了device、class 等结构的定义
#define DEVICE_CLASS_NAME "chrdev_class"
#define DEVICE_NODE_NAME "chrdev_test" //宏定义设备节点的名字
static int major_num, minor_num;       //定义主设备号和次设备号
 
struct class *class;                   /* 类 */
struct device *device;                 /* 设备 */
struct cdev cdev;                      //定义一个cdev结构体
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数minor_num
dev_t dev_num;                         /* 设备号 */
 
/**
 * @description: 打开设备
 * @param {structinode} *inode:传递给驱动的 inode
 * @param {structfile} *file:设备文件,file 结构体有个叫做 private_data 的成员变量,
 *  一般在 open 的时候将 private_data 指向设备结构体。
 * @return: 0 成功;其他 失败 
 */
int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open\n");
    return 0;
}
// 设备操作函数结构体
struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open};
/**
 * @description: 驱动入口函数
 * @param {*}无
 * @return {*} 0 成功;其他 失败
 */
static int hello_init(void)
{
    int ret; //函数返回值
    if (major_num)
    {
        /*静态注册设备号*/
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
 
        dev_num = MKDEV(major_num, minor_num);                              //MKDEV将主设备号和次设备号合并为一个设备号
        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
        if (ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
        printk("register_chrdev_region ok\n"); //静态注册设备号成功
    }
    else
    {
        /*动态注册设备号*/
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
        if (ret < 0)
        {
            printk("alloc_chrdev_region error\n");
        }
        printk("alloc_chrdev_region ok\n"); //动态注册设备号成功
 
        major_num = MAJOR(dev_num);            //将主设备号取出来
        minor_num = MINOR(dev_num);            //将次设备号取出来
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
    }
    // 初始化 cdev
    cdev.owner = THIS_MODULE;
    cdev_init(&cdev, &chrdev_ops);
    // 向系统注册设备
    cdev_add(&cdev, dev_num, DEVICE_NUMBER);
    // 创建 class 类
    class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
    // 在 class 类下创建设备
    device = device_create(class, NULL, dev_num, NULL, DEVICE_NODE_NAME);
    return 0;
}
/**
 * @description: 驱动出口函数
 * @param {*}无
 * @return {*}无
 */
static void hello_exit(void)
{
    //注销设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
    //删除设备
    cdev_del(&cdev);
    //注销设备
    device_destroy(class, dev_num);
    //删除类
    class_destroy(class);
    printk("gooodbye! \n");
}
// 将上面两个函数指定为驱动的入口和出口函数
module_init(hello_init);
module_exit(hello_exit);
//  LICENSE 和作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

添加对应的app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;
    char buf[64] = {0};
    fd = open("/dev/chrdev_test",O_RDWR);  //打开设备节点
    if(fd < 0)
    {
        perror("open error \n");
        return fd;
    }
    //read(fd,buf,sizeof(buf)); //从文件中读取数据放入缓冲区中
    close(fd);
    return 0;
}
  • 首先卸载驱动insmod chrdev.ko
  • 查看是否生成类ls /sys/class/chrdev_class/
  • 查看是否生成设备节点ls /dev/chrdev_test

验证生成的设备节点是否可以使用

  • ./app