LV10-02-字符设备驱动-01-基础知识

本文主要是字符设备驱动基础的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、字符设备驱动简介

1. 字符设备?

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的LED灯、按键、 IICSPILCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

2. 如何调用驱动?

image-20220831152808171

驱动加载成功以后会在/dev目录下生成一个相应的文件,应用程序通过对这个名为/dev/xxx(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。

比如现在有个叫做/dev/led的驱动文件,此文件是led灯的驱动文件。应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led这个文件。 openclose就是打开和关闭 led驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数。如果要获取led灯的状态,就用 read函数从驱动中读取相应的状态。

应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做系统调用的方法来实现从用户空间进入到内核空间,这样才能实现对底层驱动的操作。 openclosewriteread 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。

image-20220831153510754

如上图所示,在应用中调用了open函数,然后对应一个关于open的系统调用,在这个系统调用中,再调用我们编写的驱动函数,应用程序中怎么找到这个系统调用,以及如何进入内核空间的,暂时不用关心,我们需要关心的是这个open系统调用怎么知道要调用驱动中的myopen呢?

3. struct file_operations

每一个系统调用,在驱动中都有与之对应的一个驱动函数,他们之间是通过一个结构体联系起来的,在Linux内核文件 include/linux/fs.h中有一个名为file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

点击查看 struct file_operations 结构体成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

【常用成员说明】

  • owner:填THIS_MODULE,表示该结构体对象从属于哪个内核模块。
  • llseekllseek函数用于修改文件当前的读写位置。
  • readread 函数用于读取设备文件,与应用程序中的 read() 函数对应。
  • writewrite 函数用于向设备文件写入(发送)数据,与应用程序中的 write() 函数对应。
  • pollpoll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写,实现多路复用的支持。
  • unlocked_ioctlunlocked_ioctl 函数提供对于设备的控制功能,如读写设备参数,读设备状态等,与应用程序中的 ioctl() 函数对应。
  • compat_ioctlunlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行32位的应用程序调用的是unlocked_ioctl
  • mmap:用于将将设备的内存映射到进程空间中(也就是用户空间,简单来说就是映射内核空间到用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
  • open:用于打开设备文件,与应用程序中的open()函数对应。
  • release:用于释放(关闭)设备文件,与应用程序中的close()函数对应。
  • fasync:用于常用于异步通知,如信号驱动。

一般我们会定义一个struct file_operations类型的全局变量并用自己实现的各种操作函数名对其进行初始化,如myopen()。初始化后,由于该结构体对象各个函数指针成员都对应相应的系统调用函数应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数,这样当我们在应用程序中调用open的时候,系统调用就会在内部调用相应的myopen驱动函数了。

二、字符设备加载与卸载

1. 加载与卸载模板

这是驱动开发的最基本的步骤,模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和
卸载注册函数如下:

1
2
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用insmod命令加载驱动的时候,xxx_init这个函数就会被调用。module_exit()函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用rmmod命令卸载具体驱动的时候xxx_exit函数就会被调用。

  • 字符设备加载与卸载模板
点击查看详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 模块入口函数 */
int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 模块出口函数 */
void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init); /* 注册模块加载函数 */
module_exit(xxx_exit); /* 注册模块卸载函数 */

2. 添加LICENSE

前边我们已经了解过模块信息宏的相关概念,模块信息中这个LICENSE是必须要添加的,还可以添加上作者名字,一般为:

1
2
3
/* 模块信息(通过 modinfo dev_name 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink"); /* 字符串常量内容为模块作者说明 */

三、设备号分配

1. 设备号的组成

Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备,设备号主要是用于区分内核中同类设备。 当应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备

Linux 提供了一个名为dev_t的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:

1
2
3
typedef __u32 __kernel_dev_t;
... ;
typedef __kernel_dev_t dev_t;

dev_t__u32 类型的,而__u32定义在文件include/uapi/asm-generic/int-ll64.h里面,定义如下:

1
typedef unsigned int __u32;

所以dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。 这32位包含了主设备号和次设备号:

  • 主设备号:占高12位,用来表示驱动程序相同的一类设备,因此Linux系统中主设备号范围为0~4095
  • 次设备号:占低20位,用来表示被操作的哪个具体设备。

2. 相关函数

对于设备号,linux内核中的include/linux/kdev_t.h文件中为我们提供了如下几个函数(本质是宏)来操作设备号:

1
2
3
4
5
6
#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))
  • MINORBITS:表示次设备号位数,一共是20位。
  • MINORMASK:表示次设备号掩码。
  • MAJOR: 用于从dev_t中获取主设备号,将dev_t右移20位即可。例如,
1
2
dev_t devno = MKDEV(999,1);
int major = MAJOR(devno);
  • MINOR:用于从dev_t中获取次设备号,取dev_t的低20位的值即可。例如,
1
2
dev_t devno = MKDEV(999,1);
int minor = MINOR(devno);
  • MKDEV:用于将给定的主设备号和次设备号的值组合成dev_t 类型的设备号。 例如,
1
2
3
4
dev_t devno;
int major = 999;/* 主设备号 */
int minor = 0; /* 次设备号 */
devno = MKDEV(major,minor);

3. 设备号的分配

设备号的分配有两种方式,一种是静态分配,一种是动态分配。

3.1 静态分配设备号

设备号可以是驱动开发者静态的指定一个设备号,比如选择200这个主设备号。有一些常用的设备号已经被Linux内核开发者给分配掉了,具体分配的内容可以查看文档Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号。

3.1.1 查看已使用的主设备号

我们可以使用下边的命令查看当前系统中所有已经使用了的主设备号:

1
cat /proc/devices

我们想要查询我们加载的模块的设备号的话,可以加上grep搜索:

1
cat /proc/devices | grep 申请设备号时用的名字

3.1.2 注册静态分配的设备号

当我们给定了主设备号和次设备号的时候需要进行设备号的注册,我们可以使用register_chrdev_region函数进行静态分配的设备号的注册,我们使用以下命令查询一下函数所在头文件:

1
grep register_chrdev_region -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/fs.h>

/* 函数声明 */
int register_chrdev_region(dev_t from, unsigned count, const char *name);

【函数说明】该函数用于注册静态分配的设备号,先验证设备号是否被占用,如果没有则申请占用该设备号,分配成功后可以在/proc/devices查看到申请到主设备号和对应的设备名。

【函数参数】

  • fromdev_t类型,要申请的设备号,也就是给定的设备号。
  • countunsigned类型,需要申请的设备数量。
  • namechar *类型,传入的是一个字符串,表示/proc/devices文件中与该设备对应的名字(就是设备名称),方便用户层查询主设备号。

【返回值】int类型,成功返回0,失败返回一个负数,负数的绝对值表示错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
/* 需要包含的头文件 */
#include <linux/fs.h>
#include <linux/kdev_t.h>
/* 至少应该有的语句 */
int major = 11; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备 */
int minor = 0; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
int dev_num = 1; /* 申请的设备数量 */

dev_t devno = MKDEV(major, minor); /* MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号 */
register_chrdev_region(devno, dev_num, "dev_name");

【注意事项】none

3.2 动态分配设备号

3.2.1 为什么要动态分配

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题, Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给我们一个没有被使用的设备号,这样就避免了冲突,卸载驱动的时候释放掉这个设备号即可。

3.2.2 动态分配函数

我们可以使用alloc_chrdev_region函数进行静态分配的设备号的注册,我们使用以下命令查询一下函数所在头文件:

1
grep alloc_chrdev_region -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/fs.h>

/* 函数声明 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

【函数说明】该函数用于动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号,分配成功后可以在/proc/devices查看到申请到主设备号和对应的设备名。

【函数参数】

  • devdev_t *类型,传入的是一个地址,保存的是申请到的设备号。
  • baseminorunsigned类型,次设备号起始地址, 该函数可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor为起始地址地址开始递增。一般baseminor0,也就是说次设备号从0开始。
  • countunsigned类型,要申请的设备号数量。
  • namechar *类型,传入的是一个字符串,表示/proc/devices文件中与该设备对应的名字(就是设备名称),方便用户层查询主设备号。

【返回值】int类型,成功返回0,失败返回一个负数,负数的绝对值表示错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
/* 需要包含的头文件 */
#include <linux/fs.h>
#include <linux/kdev_t.h>
/* 至少应该有的语句 */
// int major = 11; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备 */
int minor = 0; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
int dev_num = 1; /* 申请的设备数量 */

dev_t devno;
alloc_chrdev_region(&devno, minor, dev_num, "dev_name");

【注意事项】none

4. 设备号释放

4.1 相关函数

我们申请的设备号不再使用的时候就需要释放掉这个设备号,以便于其他设备使用,静态分配或者是动态分配的设备号我们统一使用函数unregister_chrdev_region完成,我们使用以下命令查询一下函数所在头文件:

1
grep unregister_chrdev_region -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/fs.h>

/* 函数声明 */
void unregister_chrdev_region(dev_t from, unsigned count);

【函数说明】该函数用于释放设备号,释放成功后/proc/devices文件对应的记录的主设备号和设备名称消失。

【函数参数】

  • fromdev_t类型,已成功分配的设备号,就是我们需要释放掉的设备号。
  • countunsigned类型,申请成功的设备数量。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
/* 需要包含的头文件 */
#include <linux/fs.h>
/* 至少应该有的语句 */
unregister_chrdev_region(devno, 1);

【注意事项】none

四、设备注册

1. 字符设备结构

Linux 中使用 cdev 结构体表示一个字符设备, cdev 结构体在 include/linux/cdev.h 文件中的定义如下 :

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

【成员介绍】

  • kobjstruct kobject类型,表示该类型实体是一种内核对象。
  • ownerstruct module *类型,一般填THIS_MODULE,表示该字符设备从属于哪个内核模块。
  • opsstruct file_operations *类型,指向空间存放着针对该设备的各种操作函数地址,这样建立起来了设备驱动的函数与系统调用的对应关系,如应用程序调用open()函数的时候,对应的系统调用就会找到我们驱动程序中实现的myopen()函数。
  • liststruct list_head类型,表示链表指针域。
  • devdev_t类型,表示设备号。
  • countunsigned int类型,表示设备数量。

编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,如下所示:

1
struct cdev mydev;

其实我们还有一种方式定义一个设备,就是动态申请(暂时没用过,先写在这里,知道有这么一个函数):

1
struct  cdev * cdev_alloc();

【说明】目前按我自己的理解,就是这个结构体对象是用于负责建立系统调用与设备驱动中函数集关系,并且向内核注册这个字符设备。

2. 字符设备操作函数集

我们需要给设备指定操作函数集,这样内核在进行系统调用的时候才知道去调用驱动中的哪一个函数,所以我们需要使用struct file_operations定义一个操作函数集,之后再赋值给cdev变量,我们常用的成员有下边这几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct file_operations 
{
struct module *owner; //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
int (*open) (struct inode *, struct file *); //打开设备
int (*release) (struct inode *, struct file *); //关闭设备
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //读设备
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备
loff_t (*llseek) (struct file *, loff_t, int); //定位
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //读写设备参数,读设备状态、控制设备
unsigned int (*poll) (struct file *, struct poll_table_struct *); //POLL机制,实现多路复用的支持
int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
int (*fasync) (int, struct file *, int);  //信号驱动
//......
};

所以我们定义的函数集如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* 操作函数集定义 */
struct file_operations myops = {
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_close,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
.poll = mydev_poll,
.fasync = mydev_fasync,
// ... ...
};

3. 初始化字符设备变量

定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化 ,其实主要为它设置操作函数集。

3.1 owner成员

初始化的时候,该成员也需要初始化,一般设置为THIS_MODULE

1
mydev.owner = THIS_MODULE;

3.2 cdev_init()

我们使用以下命令查询一下函数所在头文件:

1
grep cdev_init -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/cdev.h>

/* 函数声明 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops);

【函数说明】该函数用于初始化cdev 变量,主要是为该设备添加操作函数集。

【函数参数】

  • cdevstruct cdev *类型,要初始化的 cdev 结构体变量。
  • fopsstruct file_operations *类型,字符设备文件操作函数集合。

【返回值】none类型。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
/* 需要包含的头文件 */
#include <linux/cdev.h>
/* 至少应该有的语句 */
cdev_init(&mydev, &myops);

【注意事项】none

3.3 cdev_add()

我们使用以下命令查询一下函数所在头文件:

1
grep cdev_add -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/cdev.h>

/* 函数声明 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

【函数说明】该函数用于将指定字符设备添加到Linux内核,添加成功后会在 /proc/devices文件中创建一个包含主设备号和对应的设备名称记录。

【函数参数】

  • pstruct cdev *类型,指向要添加的字符设备(cdev 结构体变量) 。
  • devdev_t类型,设备所使用的设备号。
  • countunsigned类型,要添加的设备数量,一般填1

【返回值】int类型,添加成功返回 0,失败返回错误码。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
/* 需要包含的头文件 */
#include <linux/cdev.h>
/* 至少应该有的语句 */
cdev_add(&mydev, devno, 1);

【注意事项】none

3.4 cdev_del()

我们使用以下命令查询一下函数所在头文件:

1
grep cdev_del -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/cdev.h>

/* 函数声明 */
void cdev_del(struct cdev *p);

【函数说明】该函数用于将指定字符设备从Linux内核移除,移除成功后会删除在 /proc/devices文件中创建的包含主设备号和对应设备名称的记录。

【函数参数】

  • pstruct cdev *类型,指向要移除的字符设备(cdev 结构体变量) 。

【返回值】none类型。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
/* 需要包含的头文件 */
#include <linux/cdev.h>
/* 至少应该有的语句 */
cdev_del(&mydev);

【注意事项】none

五、设备节点创建

前边的介绍中,我们了解到加载完驱动后,我们后边操作的是/dev/下的设备文件,而这个文件则需要我们来进行创建,所以在申请完设备号之后,我们还需要在/dev目录下创建一个与之对应的设备节点文件, 应用程序将会通过操作这个设备节点文件来完成对具体设备的操作

1. 手动创建设备节点

我们可以通过mknod命令完成设备节点的创建,使用格式如下:

1
mknod /dev/设备文件名 设备种类(c为字符设备,b为块设备) 主设备号 次设备号 # ubuntu下需加sudo执行

2. 应用程序创建设备节点

2.1 mknod()

我们还可以通过应用程序来创建设备节点,我们在应用程序中使用mknod系统调用函数来完成设备节点的创建,我们可以使用man 2 mknode来查看函数的帮助手册:

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

/* 函数声明 */
int mknod(const char *pathname, mode_t mode, dev_t dev);

【函数说明】该函数用于创建设备节点,详情可以查看帮助手册,这个函数我没怎么用过,这里做一个简单的笔记。

【函数参数】

  • pathnamechar *类型,带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下。
  • modemode_t类型,文件权限。
  • devdev_t类型,32位设备号。

【返回值】int类型,成功返回0,失败返回-1

【使用格式】none

【注意事项】none

3. 自动创建设备节点

前边两种不免比较麻烦,增加了应用程序开发人员的负担,我们其实是可以在加载驱动模块成功的时候自动在/dev目录下
创建对应的设备文件。

3.1 mdev机制

udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用insmod命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。 使用busybox构建根文件系统的时候,busybox会创建一个 udev的简化版本——mdev

内核中定义了struct class结构体,一个struct class结构体类型变量对应一个类, 内核同时提供了class_create()函数,可以用它来创建一个类,这个类存放于/sys/class下面,一旦创建好了这个类,再调用device_create()函数来在/dev目录下创建相应的设备节点。这样,加载模块的时候,用户空间中的udev会自动响应device_create()函数,去/sys/class下寻找对应的类从而创建设备节点,需要注意的是,当只有一个主设备号,多个次设备号的时候,这些次设备的类只有一个

所以在嵌入式 Linux 中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理,在/etc/init.d/rcS文件中添加如下语句:

1
echo /sbin/mdev > /proc/sys/kernel/hotplug # 命令设置热插拔事件由 mdev 来管理

3.2 创建和删除类

3.2.1 class_create()

我们使用以下命令查询一下函数所在头文件:

1
grep class_create -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 需包含的头文件 */
#include <linux/device.h>

/* 函数声明 */
extern struct class * __must_check __class_create(struct module *owner,
const char *name,
struct lock_class_key *key);
/* ... ... */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

【函数说明】该函数用于创建一个类,在/sys/class生成一个目录,目录名由name指定(会得到一个/sys/class/name目录)。它其实是个宏定义,展开后内容如下:

1
struct class *class_create(struct module *owner, const char *name);

【函数参数】

  • ownerstruct module *类型,一般为THIS_MODULE
  • namechar *类型,类的名字。

【返回值】struct class *类型,是一个指向结构体class的指针,也就是创建的类,失败的时候返回NULL

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <linux/device.h>
/* 至少应该有的语句 */
struct class *class; /* 类 */
class = class_create(THIS_MODULE, "dev_name");
if (IS_ERR(class))
{
return PTR_ERR(class);
}

【注意事项】

(1)辅助接口:可以定义一个struct class的指针变量class来接受返回值,然后通过IS_ERR(class)判断是否失败;

1
2
IS_ERR(device);/* 成功-->0,失败-->非0 */
PTR_ERR(cls); /* 获得失败的返回错误码 */

(2)主设备号相同,次设备号不同的几个次设备,他们属于同一个类,只需要创建一个类即可。

3.2.2 class_destroy()

我们使用以下命令查询一下函数所在头文件:

1
grep class_destroy -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/device.h>

/* 函数声明 */
void class_destroy(struct class *cls);

【函数说明】该函数用于删除一个类,会删除创建类时在/sys/class生成的那个目录(会删除/sys/class/name目录)。

【函数参数】

  • clsstruct class *类型,要删除的类。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <linux/device.h>
/* 至少应该有的语句 */
struct class *class; /* 创建类时定义 */

class_destroy(class); /* 删除类 */

【注意事项】

(1)主设备号相同,次设备号不同的几个次设备,他们属于同一个类,只需要创建一个类,删除的时候也只需要删除创建的那一个类。

3.3创建和删除设备

创建完了类之后,我们还需要在/dev/下创建我们的设备节点。

3.3.1 device_create()

我们使用以下命令查询一下函数所在头文件:

1
grep device_create -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
6
7
/* 需包含的头文件 */
#include <linux/device.h>

/* 函数声明 */
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);

【函数说明】该函数用于在/sys/class目录下class_create生成目录再生成一个子目录与该设备相对应uevent让应用程序udevd创建设备文件,最终会创建/dev/fmt作为我们的设备节点。该函数最终创建的文件有:

1
2
/sys/class/class_name/fmt # 目录
/dev/fmt # 设备节点文件

【函数参数】

  • clsstruct class *类型,创建的类名,表示设备要创建哪个类下面。
  • parentstruct device *类型,表示父设备,一般为NULL,也就是没有父设备。
  • devtdev_t类型,已经分配成功的设备号。
  • drvdatavoid *类型,驱动私有数据,一般为NULL
  • fmtchar *类型,表示设备名字 ,是一个格式化字符串,类似printf,可以是一个固定的字符串,也可以是格式化字符串,若fmt=xxx ,就会生成/dev/xxx这个设备文件。 若为格式化字符串%s%d,则需要后边的vargs参数。
  • ...:不定参数列表,前边的fmt若为"%s%d",这里就需要一个字符串和一个整型变量,就类似于printf

【返回值】struct device *类型,是一个指向结构体device 的指针,也就是表示创建好的设备,失败的时候返回NULL

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <linux/device.h>
/* 至少应该有的语句 */
struct device *device; /* 设备 */
device = device_create(class, NULL, devno, NULL, "%s%d", "dev_name", num);
if (IS_ERR(device))
{
return PTR_ERR(device);
}

【注意事项】

(1)辅助接口:可以定义一个struct device的指针变量device来接受返回值,然后通过IS_ERR(device)判断是否失败;

1
2
IS_ERR(device);/* 成功-->0,失败-->非0 */
PTR_ERR(device); /* 获得失败的返回错误码 */

(2)主设备号相同,次设备号不同的几个次设备,他们属于同一个类,只需要创建一个类,需要创建多个设备对应不同的次设备。

3.2.2 device_destroy()

我们使用以下命令查询一下函数所在头文件:

1
grep device_destroy -r -n ~/5linux/linux-3.14/include

经过查找,我们可以得到如下信息:

1
2
3
4
5
/* 需包含的头文件 */
#include <linux/device.h>

/* 函数声明 */
extern void device_destroy(struct class *cls, dev_t devt);

【函数说明】该函数用于删除一个设备,会删除创建设备时在/sys/class生成的那个目录。就是说下边目录和文件会被删除:

1
2
/sys/class/class_name/fmt # 目录
/dev/fmt # 设备节点文件

【函数参数】

  • clsstruct class *类型,要删除的设备所处的类。
  • devtdev_t类型,表示要删除的设备号。

【返回值】none

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
/* 需要包含的头文件 */
#include <linux/device.h>
/* 至少应该有的语句 */
device_destroy(class, devno);

【注意事项】

(1)主设备号相同,次设备号不同的几个次设备,他们属于同一个类,只需要创建一个类,需要创建多个设备对应不同的次设备,删除的时候就需要将这些设备逐个删除。

(2)删除的时候可以先删除设备,再删除类,按理说可以直接删除类就完事了,毕竟类目录都没了,子目录应该也就一起删除了。