深入探讨Linux驱动开发:字符设备驱动开发与测试_linux 驱动开发设备号(1)

	- [⑦删除设备节点](#_146)

一、字符设备驱动介绍

1.设备驱动介绍

Linux内核将设备按照访问特性一般分为三类:字符设备、块设备、网络设备:
在这里插入图片描述
详细的学习字符设备驱动框架之前,我们先来简单的了解一下Linux下的应用程序是如何调用驱动程序的,Linux应用程序对驱动的调用如图如所示:
在这里插入图片描述
应用程序运行在用户空间,Linux驱动属于内核的一部分,运行于内核空间,要是用户想要实现对内核的操作,那么他必须使用系统调用来实现从用户空间到内核空间的操作。

二、设备号

1.设备号介绍

设备号(device number)是用于唯一标识Linux系统中的一个设备的数字,包括主设备号(major number)和次设备号(minor number)。主设备号用于标识设备驱动程序,次设备号用于标识同一驱动程序下的不同设备实例。

设备节点(device node)是用于在文件系统中表示设备的特殊文件。设备节点通常位于/dev目录下,其文件名由设备类型和设备号组成,通过设备节点,应用程序可以与设备进行交互,如打开、读取、写入等操作。

在/dev路径下可以通过ls -l查看设备节点:
在这里插入图片描述
其中可以举例:

crw-rw----  1 root        video    29,   0 4月  16 10:30 fb0

  • c: 表示这是一个字符设备文件。
  • rw-rw----: 文件权限,代表文件所有者和所属组有读写权限,其他用户没有任何权限。
  • 1: 确定这个设备文件的硬链接数。
  • root: 文件所有者为root。
  • video: 文件所属video组。
  • 29, 0: 设备号,代表这是主设备号为 29,次设备号为 0 的设备。
  • 4月 16 10:30: 文件的最后修改时间。
  • fb0: 设备文件名,代表此文件是第一个帧缓冲设备的字符设备文件。

2.分配与释放设备编号

①dev_t类型

在内核编程中,dev_t 是一个在 Linux 内核中用来表示设备号的数据类型。它实际上是一个 32 位整数,其中高 12 位是主设备号,低 20 位是次设备号。主设备号用于指示设备类型,而次设备号用于区分同一类型下的不同设备。在编码时,我们不应该管哪些位是主设备号,哪些位是次设备号。而是应当利用在<linux/kdev_t.h>中的一套宏定义来获取一个dev_t的主、次编号:

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) 
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

  • 宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
  • 宏 MINORBITS 表示次设备号位数,一共是 20 位。
  • 宏 MINORMASK 表示次设备号掩码。
  • 宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
  • 宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
②静态分配设备号

静态分配设备号是指在代码中手动指定设备号的方法。开发者在注册字符设备时,需要指定主设备号和次设备号,如果开发者已经确定了设备号,可以使用静态分配设备号的方式。当然对于设备号我们自己随机选择的话,会跟Linux内核其他的驱动冲突,这时我们可以先查看当前系统已经使用了哪些主设备号,然后我们选择一个没有使用的作为我们新的驱动使用。Linux系统中正在使用的主设备号会保存在/proc/devices文件中:
在这里插入图片描述
上面列出来的是Linux内核里所有驱动使用的主设备号,我们在编写设备时就可以选定一个未用的主设备号。

静态分配设备号需要开发者手动指定设备号,保证每个设备号都唯一。在代码中通过调用 register_chrdev_region() 函数进行注册。这个函数的第一个参数是主设备号,第二个参数是设备数量,第三个参数是设备名,在/proc/devices和sysfs中。 register_chrdev_region() 函数成功返回0,失败返回一个负数:

dev\_t       devno;
int         result;
int         major = 251;
​
devno = MKDEV(major, 0);
​
result = register\_chrdev\_region(devno, 4, "chrdev");//静态的申请和注册设备号
if(result < 0)
{
    printk(KERN_ERR "chrdev can't use major %d\n", major);
    return -ENODEV;
}

当驱动的主、次设备号申请成功后,/proc/devices里将会出现该设备,但是/dev路径下并不会创建该设备文件,需要我们去创建设备节点。

静态分配设备号的优点是使用方便,可以确保设备号的唯一性。缺点是如果设备数量不够,就会浪费设备号资源,而如果设备数量过多,又会导致设备号的耗尽。因此,在静态分配设备号时,需要谨慎考虑设备数量,以及设备号的合理使用。

③动态分配设备号

我们通过静态分配设备号很容易带来冲突问题,如果以后我们Linux内核升级需要使能其他的设备驱动时,如果需要的驱动和我们所用的主设备号冲突,那么我们就不得不对这个主设备号进行调整,如果产品已经部署了,这种召回升级是十分致命的,所以我们写驱动的之后应该由Linux内核根据当前主设备号使用情况动态分配一个未用的给我们的设备驱动,这样就不会发生冲突的情况了。

int alloc\_chrdev\_region(dev\_t \*dev, unsigned baseminor, unsigned count, const char \*name)

参数说明:

  • dev:用于返回分配的设备号。
  • firstminor:要分配的第一个次设备号,它常常是0。
  • count:要分配的设备号数量。
  • name:设备名字。

动态分配的缺点是你无法提前创建设备节点,因为分配给你的主设备号会发生变化,对于驱动的正常使用这不是问题,但是一旦编号分配了,只能通过 查看 /proc/devices文件才能知道它的值,然后再创建设备节点。

④释放主次设备号

注销字符设备后要释放设备号,设备号释放函数如下:

void unregister\_chrdev\_region(dev\_t from, unsigned count)

使用该函数可以将之前通过 register_chrdev_region() 函数注册的设备号从系统中取消,以便于其他设备可以使用这些设备号。通常在模块的卸载函数xxx__exit中调用该函数,释放模块使用的设备号资源。

⑤手动创建设备节点

如果我们需要创建该文件,则需要使用mknod命令创建。当然我们也可以在驱动里调用相应的函数,来通知应用程序空间自动创建该设备文件。

mknod /dev/chrdevbase c 251 0

  • “mknod”是创建节点命令
  • “/dev/chrdevbase”是要创建的节点文件
  • “c”表示这是个字符设备
  • “251”是设备的主设备号
  • “0”是设备的次设备号

例如 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase进行读写操作即可。相当于/dev/chrdevbase这个文件是 chrdevbase 设备在用户空间中的实现。

⑥自动创建设备节点

在Linux中,可以通过udev(userspace dev)来实现自动创建设备节点,当然前提条件是用户空间移植了udev。udev是Linux系统中的一个动态设备管理机制,它负责在系统启动时检测硬件设备的插拔并创建相应的设备节点。

当udev检测到新设备插入时,它会执行一系列规则(rules)来确定设备应该如何创建,这些规则定义了设备节点的名称、权限等信息,并使用mknod系统调用来创建设备节点。这些规则通常存储在/etc/udev/rules.d/目录中的文件中,每个文件包含一组规则。

class_create():在调用device_create()前要先用class_create()创建一个类(设备依附的类)。类这个概念在Linux中被抽象成一种设备的集合。类在/sys/class目录中。class_create()这个函数使用非常简单,在内核中是一个宏定义,定义在/include/linux/device.h中:

#define class\_create(owner, name) \
({ \
 static struct lock\_class\_key \_\_key; \
 \_\_class\_create(owner, name, &\_\_key); \
})

此函数有两个参数:

  • owner:struct module结构体类型的指针,一般赋值为THIS_MODULE。
  • name:char类型的指针,类名。

device_create():用于创建设备节点,其定义在/include/linux/device.h中,函数原型如下:

struct device \*device\_create(struct class \*class, struct device \*parent, dev\_t devt, void \*drvdata, const char \*fmt, ...)
{
    va_list vargs;
    struct device \*dev;
    
    va\_start(vargs, fmt);
    dev = device\_create\_vargs(class, parent, devt, drvdata, fmt, vargs);
    va\_end(vargs);
return dev;
}

  • class:该设备依附的类
  • parent:父设备
  • devt:设备号(此处的设备号为主次设备号)
  • drvdata:私有数据
  • fmt:设备名

device_create能自动创建设备文件是依赖于udev这个应用程序,使用udev后,在/dev目录下就只包含系统中真正存在的设备。

⑦删除设备节点

device_destroy()函数用于销毁一个设备节点,并释放相关的资源。定义在/include/linux/device.h中:

void device\_destroy(struct class \*class, dev\_t devt)
{
    struct device \*dev;

    dev = class\_find\_device(class, NULL, &devt, exact_match);
    if (dev)
        device\_destroy(dev);
}

参数说明:

  • class:指向struct class类型的指针,表示设备节点所属的设备类。
  • devt:表示设备号。

函数class_destroy()用于从Linux内核系统中删除设备。此函数执行的效果是删除函数class_create()在/sys/class目录下创建的节点对应的文件夹,定义在/include/linux/device.h中:

void class\_destroy(struct class \*cls)
{
    if ((cls == NULL) || (IS\_ERR(cls)))
    return;
 
    class\_unregister(cls);
}

参数说明:

  • cls:struct class结构体类型的变量,代表通过class_create创建的设备的节点。

自动创建节点示例:

static struct class \*my_class;
static struct device \*my_device;
static int __init my\_driver\_init(void)
{
    /\* 创建设备类 \*/
    my_class = class\_create(THIS_MODULE, "my\_class");
    /\* 创建设备节点 \*/
    my_device = device\_create(my_class, NULL, MKDEV(MAJOR_NUM, MINOR_NUM), NULL, "my\_device");
    return 0;
}
static void __exit my\_driver\_exit(void)
{
    /\* 销毁设备节点 \*/
    device\_destroy(my_class, MKDEV(MAJOR_NUM, MINOR_NUM));
    /\* 销毁设备类 \*/
    class\_destroy(my_class);
}
module\_init(my_driver_init);
module\_exit(my_driver_exit);

三、字符设备注册

1.cdev结构体

内核在内部使用类型struct cdev的结构体来代表字符设备。在内核调用你的设备操作之前,你必须分配一个这样的结构体并注册给linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operation结构体中。该结构体定义在include/linux/cdev.h文件中:

struct cdev {
    struct kobject kobj;
    struct module \*owner;
    const struct file\_operations \*ops;
    struct list\_head list;
    dev\_t dev;
    unsigned int count;
};

  • kobj:kobject结构体,用于表示cdev对象在内核中的内存管理等方面的信息。
  • owner:指向内核模块的指针,表示注册这个cdev结构体的内核模块。
  • ops:指向字符设备驱动的file_operations结构体。
  • list:链表节点,用于将多个cdev结构体链接在一起。
  • dev:字符设备的设备号。
  • count:用于表示同一设备实例的数量,通常为1。

在内核编程中,我们可以使用两种方法获取结构体。
一是运行时想获取一个独立的cdev结构:

struct cdev \*chrtest;
if(NULL == chrtest = cdev\_alloc())
{
    printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
    unregister\_chrdev\_region\_region(devno, dev_vount);
    return -ENOMEM;
}
​
chrtest->ops = &my_fops;

但是,偶尔你会想将cdev结构体嵌入一个你自己的设备特定结构。这样的情况下你需要初始化已经分配的结构体:

cdev\_init(struct cdev \*dev, struct file\_operations \*fops);

struct cdev有一个拥有者成员,应当设置为THIS_MODULE,一旦cdev结构建立,最后的步骤就是告诉内核。

2.注册cdev到内核

分配到cdev结构体后,我们将它初始化,并将对该设备驱动所支持的系统调用函数存放在file_operations结构体添加进来。然后用途cdev_add函数将它注册到内核,从而完成一个完整的Linux设备注册过程。

cdev_add函数原型如下:

int cdev\_add(struct cdev \*p, dev\_t dev, unsigned int count);

其中,参数含义如下:

  • p:指向要添加的字符设备对象的 cdev 结构体指针。
  • dev:指定要添加的设备号。
  • count:指定添加的设备号的数量。

字符设备驱动cdev的分配和注册示例:

static struct file\_operations chrtest_fods =
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .release = chrtest_release,
    .unlocked_ioctl = chrtest_ioctl,
};
​
struct cdev \*chrtest_cdev;
if(NULL == (led_cdev = cdev_alloc))
{
    printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
    unregister\_chdev\_region(devno, dev_count);
    return -ENOMEM;   
}
​
led_cdev->owner = THIS_MODULE;
cdev\_init(led_cdev, &led_fops);
​
result = cdev\_add(led_cdev, devno, dev_count);
​
if(0 != result)
{
    printk(KERN_INFO "S3C %s drive can't register cdev:result = %d\n", DEV_NAME, result);
    goto ERROR;
}

三、字符设备驱动开发与测试

1.字符设备驱动

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
 \* Copyright: (C) 2023 Deng Yonghao<dengyonghao2001@163.com>
 \* All rights reserved.
 \*
 \* Filename: chrdevbase.c
 \* Description: This file is character device base
 \* 
 \* Version: 1.0.0(2023年04月10日)
 \* Author: Deng Yonghao <dengyonghao2001@163.com>
 \* ChangeLog: 1, Release initial version on "2023年04月10日 11时30分10秒"
 \* 
 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc\_fs.h>
#include <linux/seq\_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/\*确定主设备号\*/
//#define DEV\_MAJOR 79
#ifndef DEV\_MAJOR
#define DEV\_MAJOR 0
#endif
int dev_major = DEV_MAJOR;//主设备号 


#define DEV\_NAME "chrdev"//设备名称

static struct cdev \*chrtest_cdev;//cdev结构体

//static struct class \*chrdev\_class; //定义一个class用于自动创建类

static char kernel_buf[1024];


#define MIN(a,b) (a < b ? a : b)

/\*实现对应的open/read/write等函数,填入file\_operations结构体 \*/
static ssize\_t chrtest\_drv\_read(struct file \*file, char __user \*buf, size\_t size, loff\_t \*offset)
{
	int err;
	printk("%s %s line %d\n", \_\_FILE\_\_,  __FUNCTION__, \_\_LINE\_\_);
	err = copy\_to\_user(buf, kernel_buf, MIN(1024, size));//内核空间的数据到用户空间的复制 
	return MIN(1024, size);
}

static ssize\_t chrtest\_drv\_write(struct file \*file, const char __user \*buf, size\_t size, loff\_t \*offset)
{
	int err;
	printk("%s %s line %d\n", \_\_FILE\_\_, __FUNCTION__, \_\_LINE\_\_);
	err = copy\_from\_user(kernel_buf, buf, MIN(1024, size));//将用户空间的buf复制到内核空间缓冲区kernel\_buf中,因为用户空间内存不能直接访问内核空间的内存
	return MIN(1024, size);
}

static int chrtest\_drv\_open(struct inode \*node, struct file \*file)
{
	printk("%s %s line %d\n", \_\_FILE\_\_, __FUNCTION__, \_\_LINE\_\_);
	return 0;
}

## 最后

**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。**

**因此收集整理了一份《2024年嵌入式&物联网开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

![img](https://img-blog.csdnimg.cn/img_convert/db868dc94ce4b9a8bf6a675e510cf6fd.png)

![img](https://img-blog.csdnimg.cn/img_convert/5f398263b3f2677690558bf4d96badf7.jpeg)

![img](https://img-blog.csdnimg.cn/img_convert/7d2ff2183f54ea60876d2514aab3c17f.png)

 ![img](https://img-blog.csdnimg.cn/img_convert/e24de95238f7ef7a6b83d7ae973f321f.png)

![img](https://img-blog.csdnimg.cn/img_convert/dbfb0200dd9b67c5cb8ef6649773b767.png)

![img](https://img-blog.csdnimg.cn/img_convert/e563141f4579c046e3ce62f3e4476447.png)

![](https://img-blog.csdnimg.cn/img_convert/ea96b377b4b75a62d5225d4c641ff17e.png)

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!**

[**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618654289)

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**!!


img-kpG36s8X-1715758910505)]

[外链图片转存中...(img-6YCRskwn-1715758910506)]

[外链图片转存中...(img-S9LdobQI-1715758910507)]

 [外链图片转存中...(img-eqn9O3Kq-1715758910508)]

[外链图片转存中...(img-gw6X0hsO-1715758910508)]

[外链图片转存中...(img-ElqbEQaQ-1715758910509)]

[外链图片转存中...(img-TSn4vDVk-1715758910510)]

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!**

[**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618654289)

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**!!


  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值