我们知道“Linux下一切皆文件”(当然由于历史原因,网络设备除外,它是通过socket进行操作的),我们操作设备都要通过文件进行操作也就是所所谓的操作设备文件节点,但是在Linux内核中是使用设备号来唯一的识别和管理设备,就相当于公民的省份证号码一样(其实吧,计算机还是喜欢数字的像标识进程使用进程的PID,管理用户使用UID,管理磁盘上的文件使用的inode号,管理网络中的计算机使用IP地址等等)。
那么设备号到底是什么东西?在Linux系统中如何才能保证每个设备的设备号是唯一的?下面我们来看一下:
1.设备号的构成
Linux系统中一个设备号由主设备号和次设备号构成,Linux内核用主设备号来定位对应的设备驱动程序(即是主设备找驱动),而次设备号用来标识它同个驱动所管理的若干的设备(次设备号找设备)。因此,从这个角度来看,设备号作为系统资源,必须要进行仔细的管理,以防止设备号与驱动程序的错误对应所带来的管理设备的混乱。
下图为一个嵌入式设备上的串口的设备文件节点信息:
可以看到他们的主设备号都是252,次设备号0-5,即是这种串口驱动管理了6个不同的设备。
Linux系统中,使用dev_t类型来标识一个设备号,他是一个32位的无符号整数:
<include linux/types.h>
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
其中,12为主设备号,20为次设备如下图。
随着内核版本的演变,上述的主次设备号的构成也许会发生变化,所以设备驱动开发者应该避免直接使用主次设备号锁占的位宽来获得对应的主设备号或者次设备号。内核为了保证在主次设备号位宽发生变化时,现在的程序依然可以工作,内核提供了如下的几个宏:
<include /linux/kdev_t.h>
#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))
MAJOR宏是用来从一个dev_t 类型的设备号中提取出主设备号,MINOR用来提取次设备号。MKDEV则是将主设备号ma和次设备号mi合成一个dev_t类型的设备号。实现原理都是通过未操作,不在赘述。在上述的宏定义中,MINORBITS在3.14.0内核中定义为20,
如果之后的内核对主次设备号所占的位宽进行调整,例如将MINORBITS改为8,只要驱动程序坚持使用MAJOR,MINOR,MKDEV来操作设备号,那么这部分的代码无需修改就可以用在新的内核中运行。
在实际的驱动开发中,我们经常已知inode,那么我们可以通过inode来获得主次设备号:
<include /linux/fs.h>
static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}
iminor用于根据inode获得次设备号,imajor用于根据inode获得主设备号。
2.设备号的分配和管理
在内核源码中,进行设备号的分配与管理的函数有一下两个:register_chrdev_region,alloc_chrdev_region
1)register_chrdev_region函数
此函数用于静态注册设备号,优点是可以在注册的时候就知道其设备号,缺点是可能会与系统中已经注册的设备号冲突导致注册失败。