Linux 驱动设备号
对于 Linux 系统,为了识别和管理设备,每个设备便使用一个唯一的编号来标记设备,每个注册到内核的设备都需要一个编号,这个编号就是设备号,为了细分设备号分为主设备号和次设备号。
由于 Linux 的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在 /dev
目录下,所以我们查看文件的详细信息就可以看到设备的设备号。
bash
crw-rw---- 1 root uucp 4, 70 04-14 18:16 ttyS6
crw-rw---- 1 root uucp 4, 71 04-14 18:16 ttyS7
crw-rw---- 1 root tty 7, 0 08-08 18:58 vcs
crw-rw---- 1 root tty 7, 1 08-08 18:58 vcs1
可以看到设备文件权限不再像普通文件那样为 rwx
了,而是变成了 crw
第一个字符为 c
的表示字符设备。同时多了两个数字并且使用逗号隔开,这两个数字对应的就是设备的主设备号和次设备号,如上 4,7 分别是主设备号,70,71,0,1 都是次设备号。
主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备(主设备号用来标记设备的类型,次设备号用来区分在这类设备中具体的个体设备)。
1. 设备号表示
主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备,设备号在 Linux 内核内部表示被定义为 u32 类型的一个数值,最终使用的就是 dev_t这个类型,如下(在内核源码 include/linux/types.h 中)。
c
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
而 u32 在 Linux 内核源码中被定义为 unsigned int,如下。
c
typedef unsigned int __u32;
typedef __u32 u32;
所以 dev_t 本质属于 unsigned int 类型(即 32 位的数据类型),dev_t 类型为了可以同时表达主设备号和次设备号,用 32 位的数据高 12 位表示主设备号,低 20 位为次设备号。所以主设备号最多可以有 2^12=40dev_t96 个(0-4095),次设备号最多可以有 2^20=1048576 个(0-1048575)。
c
dev_t 32 bit
-------------------------------------------------------------------------
| 31 .. MAJOR ... 20 | 19 ................. MINOR ................... 0 |
-------------------------------------------------------------------------
主设备号较少使用时不能超过 4095 而次设备号一般可以随意使用,次设备号一般足够使用,虽然这样但还是要做好驱动设备号的分配,不要随意浪费使用。
2. 设备号操作宏
主设备号和次设备号共同保存在一个 32 位变量中,为了方便提取或设置主/次设备号相应的位就提供了一些宏定义,这些宏定义在编写设备驱动时会用到,如下。
c
#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 中获取主设备号,MINOR()
用于从 dev_t 中获取次设备号,MKDEV()
用于将主设备号和次设备号组合成 dev_t 类型的设备号。
3. 分配设备号
设备号分配,类似 IP 地址可以静态分配也可以动态分配,静态分配 IP 就是由我们自己指定一个 IP 地址,但是不能用已经被其他设备使用的 IP,动态 IP 分配就由路由器给我们分配一个未被使用的 IP,驱动设备号分配也是这样的规则,动态分配就是向 Linux 内核申请一个设备号。
静态分配设备号要注意不能用已经被其他设备使用的设备号,所以实际上我们一般使用动态设备号分配。
4. 静态分配
(1) 要静态分配设备号很简单只需要在编写驱动时指定一个主设备号以及一个次设备号,然后使用设备号操作宏 MKDEV() 构造出一个设备号,最后调用内核提供的注册接口 register_chrdev_region() 即可将构造出的设备号注册给驱动,如下。
c
dev_t dev_id;
dev_id = MKDEV(200, 0);
register_chrdev_region(dev_id, 1, "DRIVER_NAME");
注意静态分配时不能用已经被其他设备使用的主设备号和次设备号,因为这样会构造出和其他设备相同的设备号,这是不允许的。
(2) 还有一种静态分配设备设备号的方法是只提供一个主设备号即可,例如使用字符设备注册函数 register_chrdev 注册设备时,如下。
c
file_operations dev_fops;
register_chrdev(200, "DRIVER_NAME", &dev_fops);
但是这种有个大问题,会将一个主设备号下的所有次设备号都使用掉,比如设置 LED 这个主设备号为 200,那么 0-1048575 这个区间的次设备号会全部都被 LED 一个设备占用(这样太浪费次设备号!一个 LED 设备肯定只能有一个主设备号,一个次设备号),为什么呢?看代码就知道了。
找到 register_chrdev() 函数的定义,发现调用了 __register_chrdev() 函数,并且形参传递了我们静态指定的主设备号,强制指定次设备号为 0,以及设备号注册数量为 256 个。
c
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
再看 __register_chrdev() 函数的定义,调用了 __register_chrdev_region() 函数去根据我们设置的静态主设备号,根据强制指定的次设备号 0 和注册数量 256,来强制注册 256 个设备号,如下。
c
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
...
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
...
return err;
}
通过阅读上方代码就可以知道为什么这种静态分配方式会将一个主设备号下的所有次设备号都使用掉了。
5. 动态分配
静态分配设备号需要我们事先去 Linux 根文件系统中查看设备文件的属性确定好哪些设备号没有使用,再选择一个设备号注册给我们的驱动。
解决这个问题最好的方法就是在使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核给你的驱动分配可以使用的设备号。
c
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
dev 用于接收申请到的设备号,baseminor 指定次设备号,count 指定要申请的设备号个数。
c
dev_t dev_id;
alloc_chrdev_region(&dev_id, 0, 1, "DRIVER_NAME");
申请到设备号之后使用设备号操作宏 MAJOR() 和 MINOR() 从设备号中分离出主设备号和次设备号用于其他用途。
c
int major = MAJOR(dev_id);
int minor = MINOR(dev_id);
6. 设备节点文件
在 Linux 系统中应用程序通过访问设备节点文件来访问设备(驱动)的,访问设备节点文件的操作如何映射到具体的驱动呢?答案就是我们给设备分配的设备号。
由于我们给驱动分配了设备号,所以只要将设备节点文件映射到设备号就相当于映射到了驱动,这就是我们给驱动分配设备号的作用。
7. 手动创建设备节点
如何将设备节点文件映射到对应的设备号呢?第一种办法是在使用 mknod
命令手动创建设备节点文件时把设备号作为参数传递给 mknod
命令,如下。
bash
mknod /dev/leddrv c 200 0
这里设备节点文件名为 "/dev/leddrv",主设备号为 200
,次设备号为 0
,这样设备节点文件就映射到设备号相关的驱动了,最终应用程序就可以通过设备节点文件访问到相应驱动了。
c
fd = open("/dev/leddrv", O_RDWR);
8. 自动创建设备节点
手动创建设备节点有一个条件是要求我们知道具体的设备号,并且在创建设备节点时将设备号作为参数传递给设备节点文件。
设备号采用静态分配时这没有问题,但是设备号采用动态分配(向内核申请)时我们无法事先知道具体的设备号。此时处于不确定的设备映射状态,特别是那些动态设备,比如 USB 设备,设备节点文件到实际设备(驱动)的映射并不确定。
此时就需要设备(驱动)在初始化时利用分配到的设备号自行创建设备节点文件。
8.1 认识 udev 和 mdev
udev 是一个用户程序,在 Linux 通过 udev 可实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,并根据硬件设备状态来创建或者删除设备文件。
比如使用 modprobe
命令成功加载驱动模块后 udev 就自动在 rootFS 的 /dev
目录下创建对应的设备节点文件,使用 rmmod
命令卸载驱动模块后就自动删除 /dev
目录下对应的设备节点文件,而 mdev 是 udev 的简化版本,用于少资源的嵌入式平台。
如果需要使用自动创建设备节点这个功能需要在内核 menuconfig 中把 mdev 打开。
8.2 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后添加自动创建设备节点相关代码。
第一步,使用 class_create 函数创建一个 class 类。
c
struct class * _class = class_create(THIS_MODULE, "DRIVER_NAME");
在文件 include/linux/device.h 中可以看到 class_create 的定义,可以看到是一个宏定义函数,如下。
c
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); \
})
形参 owner 一般为 THIS_MODULE,name 是类名字,返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序时需要同时使用 class_destroy() 函数删除掉类 ,参数 cls 指定要删除的类,函数定义如下。
c
void class_destroy(struct class *cls);
8.3 创建设备
第二步,类创建完成后还需要在这个类下创建一个设备,创建设备使用 device_create 函数。
c
struct device * _device = device_create(_class, NULL, dev_id, NULL,
"DRIVER_NAME"");
在文件 include/linux/device.h 中可以看到 device_create 的定义,可以看到是一个可变参数函数,如下。
c
struct device *device_create(const struct class *class,
struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);
形参 class 指定在哪个类创建设备,parent 是父设备,一般为 NULL(即没有父设备)。devt 是设备号(动态分配的设备号),drvdata 指定设备可能会使用的私有数据,一般为 NULL。fmt 指定设备节点文件名称(设置 fmt=xxx 的话就会生成 /dev/xxx 这个设备节点文件)。返回值就是创建好的设备。
卸载驱动程序时需要同时使用 device_destroy() 函数删除设备 ,函数定义如下。参数 class 是要删除的设备所处的类,参数 devt 指定要删除的设备号。
c
void device_destroy(const struct class *class, dev_t devt);