Linux驱动---sys文件系统自动添加设备节点、添加调试节点以及MISC设备驱动的解释

目录

[一. sys 文件系统](#一. sys 文件系统)

[1.1 sys 文件系统介绍](#1.1 sys 文件系统介绍)

[1.2 sys 文件系统的作用](#1.2 sys 文件系统的作用)

[二. 自动创建设备节点](#二. 自动创建设备节点)

[2.1 创建和删除类](#2.1 创建和删除类)

[2.1.1 创建类](#2.1.1 创建类)

[2.1.2 几个检查错误的函数](#2.1.2 几个检查错误的函数)

1、ERR_PTR

2、PTR_ERR

3、IS_ERR

[2.1.3 删除类](#2.1.3 删除类)

[2.2 创建与删除设备节点](#2.2 创建与删除设备节点)

[2.2.1 创建设备节点](#2.2.1 创建设备节点)

[2.2.2 删除设备节点](#2.2.2 删除设备节点)

[三. 加入调试节点](#三. 加入调试节点)

[3.1 创建设备类与设备节点](#3.1 创建设备类与设备节点)

[3.2 调用 device_create_file 函数,创建设备的属性](#3.2 调用 device_create_file 函数,创建设备的属性)

[3.3 使用 __ATTR 填充属性结构体](#3.3 使用 __ATTR 填充属性结构体)

[3.4 回收调试节点](#3.4 回收调试节点)

[四. MISC 设备驱动](#四. MISC 设备驱动)

[4.1 设备类型的概念](#4.1 设备类型的概念)

[4.2 MISC 设备驱动介绍](#4.2 MISC 设备驱动介绍)

[4.3 struct miscdevice结构体及其注册](#4.3 struct miscdevice结构体及其注册)

[4.4 优势分析](#4.4 优势分析)

[五. 注意事项](#五. 注意事项)

[5.1 区分 cdev 与 device](#5.1 区分 cdev 与 device)

[5.2 内核层与用户层数据传递的三种方法](#5.2 内核层与用户层数据传递的三种方法)

[5.3 Makefile](#5.3 Makefile)


一. sys 文件系统

在之前的 LED 字符设备驱动中,需使用命令**"mknod /dev/led0 c 248 0"**手动增加字符设备节点,本节介绍如何自动创建设备节点

1.1 sys 文件系统介绍

sys目录: 系统启动以后此目录作为 sysfs 文件系统 的挂载点,sysfs 是一个基于 ram 的文件系统,也就是说它没有实际的存储设备。此目录是系统设备管理的重要目录,通过一定的组织结构向用户提供详细的内核数据结构信息

sysfs 是Linux内核中一种特殊虚拟文件系统 ,用来向用户空间提供内核设备和驱动程序的信息 。它提供了一种统一的接口,用于查看和操作设备、驱动程序、文件系统等内核对象。sysfs 存放在内存中,挂载在 /sys 目录下,文件系统中的文件不对应硬盘上任何文件

sys 目录中的文件如下所示:

|------------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| /sys/devices | 表示系统中的物理设备,每个子目录对应一个物理设备 | 1.系统设备:如CPU、系统内存等 2.总线设备:如PCI、 USB、 SCSI等设备 3.虚拟设备:如虚拟网络设备 |
| /sys/bus | 表示系统总线类型(如PCI,USB等),每个子目录对应一个总线 | 1.devices:列出所有连接到该总 线的设备 2.drivers:列出与该总线相关 的所有驱动 程序 3.drivers_autoprobe和 drivers_probe: 用于自动或手 动驱动程序绑定 4.uevent:用于触发uevent事件 |
| /sys/class | 表示系统中的设备类型,子目录按照设备类型分类 | 1.net:表示所有网络接口 2.block:表示所有块设备 3.tty:表示所有终端设备 |
| /sys/kernel | 表示内核参数和信息,如调度器参数,内核模块 | |
| /sys/module | 表示加载的内核模块,每个子目录对应一个模块 | |
| /sys/block | 表示块设备(如硬盘、 USB存储设备等) | |

1.2 sys 文件系统的作用

sysfs在驱动中的作用通常包括:

1. 设备信息显示

将内核对象(设备、驱动、总线等)的属性以文件形式展示在/sys目录下,例如设备
状态、配置参数、硬件拓扑
等。用户可通过 cat 或者echo 命令直接读取/修改这些文件,无需编写专用工具。

2. 动态设备管理

实时反映系统设备变化(如热插拔),目录结构随设备增删动态更新支持设备分类

3. 驱动调试与配置

提供寄存器访问接口,允许直接读取/写入硬件寄存器

4. 自动创建设备节点

函数 class_create、函数 class_destroy、函数 device_create、函数 device_destroy

首先介绍自动创建设备节点这个作用

二. 自动创建设备节点

2.1 创建和删除类

2.1.1 创建类

自动创建设备节点的工作是在驱动程序的入口函数 中完成的,一般在 cdev_add 函数 后面添 加自动创建设备节点相关代码。首先要创建一个 class 类 ,class 是个结构体,定义在文件 inclu de/linux/device.h 里面。class_create 是类创建函数,class_create 是个宏定义,内容如下:

cpp 复制代码
#define class_create(owner, name)            \ 
({                                           \ 
    static struct lock_class_key __key;      \ 
    __class_create(owner, name, &__key);     \ 
}) 

struct class *__class_create(struct module *owner, const char *name, 
struct lock_class_key *key) 

根据上述代码,将宏 class_create 展开以后内容如下:

**作用:**创建一个类

参数: 参数 owner 一般为 THIS_MODULE

参数 name 是类名字

返回值: 是个指向结构体 class 的指针,也就是创建的类

使用示例:

注意:

要对返回值进行检查

函数头文件为:device.h

cpp 复制代码
    //指向创建的类
    static struct class *pclass = NULL;    
    //创建类
    pclass = class_create(THIS_MODULE, "LED");
    if(IS_ERR(pclass))
    {
        ret = PTR_ERR(pclass);
        pr_err("class_create error, errno:%d\n", ret);
        return ret;
    }

2.1.2 几个检查错误的函数

1、ERR_PTR

作用:将负数错误码强制转换成指针类型

2、PTR_ERR

作用:把错误指针,还原成原本的负数错误码

3、IS_ERR

作用:判断一个指针是不是错误封装指针

返回 true:指针并非有效内存地址

返回 false:正常合法指针,可以安全解引用使用

cpp 复制代码
static inline void * __must_check ERR_PTR(long error)
{
	return (void *) error;
}

static inline long __must_check PTR_ERR(__force const void *ptr)
{
	return (long) ptr;
}

static inline bool __must_check IS_ERR(__force const void *ptr)
{
	return IS_ERR_VALUE((unsigned long)ptr);
}

2.1.3 删除类

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下

参数 cls 就是要删除的类

使用示例:

cpp 复制代码
    //回收类
    class_destroy(pclass);

2.2 创建与删除设备节点

2.2.1 创建设备节点

上一小节创建好类以后还不能实现自动创建设备节点,还需要在这个类下创建一个设备节点。使用device_create 函数在类下面创建设备节点,device_create 函数原型如下:

device_create 是个可变参数函数,

参数 class就是设备要创建哪个类下面

参数 parent是父设备,一般为 NULL,也就是没有父设备

参数 devt是设备号

参数 drvdata 是设备可能会使用的一些数据,一般为 NULL

参数 fmt是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/x xx 这个设备文件

返回值就是创建好的设备

使用示例:

注意:

参数四的名称会在 /dev/xxx 这个目录下出现,也就是创建的设备节点的名称

返回值需要判断

cpp 复制代码
    //指向创建的设备节点
    static struct device * pdevice = NULL;

    //创建设备节点:/dev/xxx
    pdevice = device_create(pclass, NULL, dev_no, NULL, "myled0");
    if(IS_ERR(pdevice))
    {   
        ret = PTR_ERR(pdevice);
        pr_err("device_create error, errno: %d\n", ret);
        return ret;
    }

这样就无需手动创建设备节点,insmod 之后会自动创建设备节点到 /dev/xxx 下

2.2.2 删除设备节点

卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

参数 class 是要删除的设备所处的类

参数 devt是要删除的设备号。

使用示例:

cpp 复制代码
    //回收设备节点
    device_destroy(pclass, dev_no);

三. 加入调试节点

通过sys文件系统,加入调试节点

**目的:**不需要通过编程,查看sys下的文件,就可以获得,灯,蜂鸣器的状态信息。同样,也可以通过写入来改变灯,蜂鸣器等的状态

具体步骤如下:

3.1 创建设备类与设备节点

cpp 复制代码
    //指向创建的类
    static struct class *pclass = NULL;
    //指向创建的设备节点
    static struct device * pdevice = NULL;

    //创建类
    pclass = class_create(THIS_MODULE, "LED");
    if(IS_ERR(pclass))
    {
        ret = PTR_ERR(pclass);
        pr_err("class_create error, errno:%d\n", ret);
        return ret;
    }
    //创建设备节点:/dev/xxx
    pdevice = device_create(pclass, NULL, dev_no, NULL, "myled0");
    if(IS_ERR(pdevice))
    {   
        ret = PTR_ERR(pdevice);
        pr_err("device_create error, errno: %d\n", ret);
        return ret;
    }

3.2 调用 device_create_file 函数,创建设备的属性

device_create_file 函数原型如下:

参数dev为指向设备的指针

参数attr为指向设备属性结构体的指针

返回值0为正,非零为错

cpp 复制代码
/**
 * device_create_file - create sysfs attribute file for device.
 * @dev: device.
 * @attr: device attribute descriptor.
 */
int device_create_file(struct device *dev,
		       const struct device_attribute *attr)
{
	int error = 0;

	if (dev) {
		WARN(((attr->attr.mode & S_IWUGO) && !attr->store),
			"Attribute %s: write permission without 'store'\n",
			attr->attr.name);
		WARN(((attr->attr.mode & S_IRUGO) && !attr->show),
			"Attribute %s: read permission without 'show'\n",
			attr->attr.name);
		error = sysfs_create_file(&dev->kobj, &attr->attr);
	}

	return error;
}

struct device_attribute 结构体声明如下:

cpp 复制代码
struct attribute {
	    const char		*name;
	    umode_t			mode;
    #ifdef CONFIG_DEBUG_LOCK_ALLOC
	    bool			ignore_lockdep:1;
	    struct lock_class_key	*key;
	    struct lock_class_key	skey;
    #endif
};


struct device_attribute {
	struct attribute	attr;
	ssize_t (*show)(struct device *dev, struct device_attribute *attr,
			char *buf);
	ssize_t (*store)(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count);
};

3.3 使用 __ATTR 填充属性结构体

使用带参宏 _ATTR 填充属性结构体。属性的名字(直接写str,不用引号),读写权限,读函

数,写函数

宏的定义如下:

注意:

1、内核权限不能是 0666,只能是 0664

2、show 与 store 函数 需要自己实现,这两个函数的返回值参数都需要注意,下面的使用示例有详细说明

show的返回值: 需要返回值输出字符串的长度。

store 的返回值: 要返回该函数形参的最后一个参数 count,否则会阻塞

cpp 复制代码
#define __ATTR(_name, _mode, _show, _store) {			
	.attr = {.name = __stringify(_name),				
		 .mode = VERIFY_OCTAL_PERMISSIONS(_mode) },		
	.show	= _show,						            
	.store	= _store,						            
}

3.4 回收调试节点

使用函数 device_remove_file

cpp 复制代码
void device_remove_file(struct device *dev, const struct device_attribute *attr)

使用示例:

cpp 复制代码
static ssize_t myshow(struct device *dev, struct device_attribute *attr,char *buf);
static ssize_t mystore(struct device *dev, struct device_attribute *attr,const char *buf, size_t count);


//设备属性结构体
static struct device_attribute attr = __ATTR(led_status, 0664, myshow, mystore);


/**
 * @brief 在命令行对设备属性进行读取时候调用此函数-读属性
 * 
 * @param dev 本设备
 * @param attr 本设备的属性结构体,暂时不使用
 * @param buf 返回给用户层命令行的字符串信息
 * @return ssize_t buf的字符串长度
 */
static ssize_t myshow(struct device *dev, struct device_attribute *attr,char *buf)
{
    ssize_t len = 0;
    if(led_status == LED_ON)
    {
        strcpy(buf, "LED_ON\n");
    }
    else if(led_status == LED_OFF)
    {
        strcpy(buf, "LED_OFF\n");
    }
    len = strlen(buf);
    return len;
}

/**
 * @brief 当命令行向该属性写入指令时,会调用此函数-写属性
 * 
 * @param dev 
 * @param attr 
 * @param buf 命令行写入本属性的指令字符串
 * @param count 命令行的长度
 * @return ssize_t 返回count 
 */
static ssize_t mystore(struct device *dev, struct device_attribute *attr,const char *buf, size_t count)
{
    u32 v = 0;
    v = readl(pdata);
    if(0 == strcmp(buf, "1\n")) //1:led on
    {
        v &= ~(1 << 3);
        writel(v, pdata);
        led_status = LED_ON;
    }
    else if(0 == strcmp(buf, "0\n")) //0:led off
    {
        v |= (1 << 3);
        writel(v, pdata);
        led_status = LED_OFF;
    }
    return count;
}

这样,在 insmod 之后不仅会自动在 /dev/xxx 下创建设备节点,而且在 /sys/devices/virtual/LED/myled0 会生成一个调试节点,可以使用命令行指令对设备的状态进行读取和改变,如下所示:

四. MISC 设备驱动

4.1 设备类型的概念

在Linux系统中通过 cat /proc/devices查看系统中所有加载的驱动

字符设备驱动如下:

块设备驱动如下:

4.2 MISC 设备驱动介绍

所有的 MISC 设备驱动的主设备号都为 10 ,不同的设备使用不同的从设备号

随着 Linux 字符设备驱动的不断增加,设备号变得越来越紧张,尤其是主设备号,MISC 设备驱动就用于解决此问题。MISC 设备会自动创建 cdev,不需要手动创建,同样也会自动创建设备节点,不需要 mknod,因此采用 MISC 设备驱动可以简化字符设备驱动的编写

4.3 struct miscdevice结构体及其注册

需要向 Linux 注册一个 miscdevice 设备,miscdev ice 是一个结构体,定义在文件 include/linux/miscdevice.h 中,内容如下:

cpp 复制代码
struct miscdevice  {
	int minor;                            //子设备号
	const char *name;                     //设备名称
	const struct file_operations *fops;   //驱动文件操作函数集合
	struct list_head list;
	struct device *parent;
	struct device *this_device;
	const struct attribute_group **groups;
	const char *nodename;
	umode_t mode;
};

定义一个 MISC 设备(miscdevice 类型)以后需要设置 minor、name 和 fops这三个成员变量。

minor 表示子设备号 ,MISC 设备的主设备号为 10,这个是固定的,需要用户指定子设备号,一般使用宏 MISC_DYNAMIC_MINOR 来定义

name 就是此 MISC 设备名字 ,当此设备注册成功以后就会在**/dev 目录**下生成一个名为 nam e 的设备文件。

fops 就是字符设备的操作集合,MISC 设备驱动最终是需要使用用户提供的 fops 操作集合

初始化miscdevice 类型示例:

cpp 复制代码
#define DEV_NAME "myled0"

//杂项设备结构体
static struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,    //这个宏为255,但是次设备号不一定是255;自动获取次设备号
    .name = DEV_NAME,               //设备节点名 和 /sys/class/misc/xxx 两者名称相同
    .fops = &fops                   //杂项设备文件操作函数集合
};

当设置好 miscdevice 以后就需要使用 misc_register 函数向系统中注册一个 MISC 设备,此 函数原型如下:

函数参数和返回值含义如下:

**misc:**要注册的 MISC 设备。

**返回值:**负数,失败;0,成功。

使用示例:

cpp 复制代码
    //注册杂项设备
    ret = misc_register(&misc_dev);
    if(ret)
    {
        pr_err("misc_register error, errno:%d\n", ret);
        return ret;
    }

当卸载设备驱动模块的时候需要调用 misc_deregister 函数来注销掉 MISC 设备,函数原型如下:

函数参数和返回值含义如下:

**misc:**要注销的 MISC 设备。

**返回值:**负数,失败;0,成功

4.4 优势分析

以前需要调用一堆的函数去创建设备,比如在以前的字符设备驱动中会使用如下几个函数完成设备创建过程:

cpp 复制代码
alloc_chrdev_region();   /* 申请设备号 */ 
cdev_init();             /* 初始化 cdev */ 
cdev_add();              /* 添加 cdev */ 
class_create();          /* 创建类 */ 
device_create();         /* 创建设备 */ 

以前注销设备驱动的时候,需要调用一堆的函数去删除此前创建的 cdev、设备等等内容,如下所示:

cpp 复制代码
cdev_del();                 /* 删除 cdev */ 
unregister_chrdev_region(); /* 注销设备号 */ 
device_destroy();           /* 删除设备 */ 
class_destroy();            /* 删除类 */ 

而现在只需要misc_register 函数与misc_deregister 函数这两个函数即可完成

设备查看

五. 注意事项

5.1 区分 cdev 与 device

5.2 内核层与用户层数据传递的三种方法

1、mmap 内存映射

2、ioctnl 控制交互

3、copy_to_user 与 copy_from_user 数据传递

5.3 Makefile

:= :直接赋值,不会再变化了,不会追加

= :延时赋值

$(MAKE) :带参数的make,可以进行参数传递

-C: 进入相应的目录

进入源码目录

**M=:**表示模块在等号后面的路径存放

相关推荐
刘某的Cloud2 小时前
Galera Cluster mariadb 生产环境常见问题排查与运维指南
linux·运维·数据库·mariadb·集群高可用
70asunflower2 小时前
Linux 性能排查分析完全教程
linux·运维
dok124 小时前
Johannes 《Linux内核模块与设备驱动开发:编写Linux驱动程序》 (11)kmalloc 动态内存分配和struct file中的私有数据
linux·驱动开发·算法
无足鸟ICT4 小时前
【RHCA+】if判断
linux
大魔王爱学习4 小时前
终端bash&zsh中实现基于前缀的历史命令搜索
linux·bash
2401_858286114 小时前
OS81.【Linux】基于环形队列的多生产者-多消费者模型
linux·运维·服务器·环形队列
赵广陆4 小时前
Ubuntu Anaconda安装
linux·运维·ubuntu
海天鹰5 小时前
Cosmic桌面:Chrome视频全屏后自动退出
linux
行者-全栈开发6 小时前
【Linux内核】CVE-2026-46242:Bad Epoll Linux 内核 epoll UAF 漏洞修复指南(99%可靠root的云安全噩梦)
linux·运维·服务器·cve-2026-46242·linux内核漏洞·use-after-free·云多租户安全