字符设备(一)中已经介绍了2.6及之前注册设备的旧接口,为了与之前版本的兼容,Linux新版本的驱动也可以使用它来完成字符设备的注册。
回顾一下字符设备注册的旧接口:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
其中,major -- 主设备号,name -- 字符设备名,fops -- file_operations类型的结构体。对应的注销方法为 unregister_chrdev
使用 register_chrdev 一个函数即可完成驱动设备号的申请以及设备的注册,这是旧接口的一个优点。但是,当同一类型的外设有很多个时,比如有多个LED,比如磁盘驱动等,旧接口无法为它们指定次设备号,如果都使用register_chrdev注册的话,无疑会造成主设备号的大量使用,随着设备增多,很可能占满主设备号。此时就需要能够指定次设备号的新接口。
一、基础
新接口中有一个特别重要的结构体——cdev,它定义在<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;
};
它用来表示一种字符设备。其中, *owner 表示该字符设备的拥有者,通常设置为 THIS_MODULE;ops 为file_operations类型的结构体;
dev是dev_t(也就是unsigned long)类型,它是主设备号和次设备号的集合,这里再引入三个宏,分别是MKDEV、MAJOR、MINOR,它们在<linux/Kdev_t.h>中定义如下:
#define MAJOR(dev) ((dev)>>8)
#define MINOR(dev) ((dev) & 0xff)
#define MKDEV(ma,mi) ((ma)<<8 | (mi))
这里表示dev_t的bit0~7表示次设备号,bit8~15表示主设备号(具体多少位可能不同体系定义不同)。
在知道设备号dev的情况下,主设备号为 MAJOR(dev) ,次设备号为 MINOR(dev) ;
在知道主(ma) / 次(mi)设备号的情况下,设备号(dev_t)为 MKDEV(ma, mi)
二、新接口
新接口与老接口最大的不同在于把register_chrdev函数中一次完成的事情(申请设备号、注册设备号、注册设备)拆分出来各自处理。
1、申请并注册设备号
申请设备号的方法有两个,分别是手动指派和由系统分配。
1) 手动指派
这种方法需要知道系统中尚未使用的主设备号,可以使用
% cat /proc/devices
查看系统中已经占有的字符设备主设备号,选择一个未占有的主设备号ma,次设备号通常从0开始分配,dev = MKDEV(ma, 0)
然后使用以下函数注册设备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
其中,from 即为前文得到dev,count表示注册的设备个数(假如有5个LED,count为5,次设备号依次排开为0~4),name字符名。
例如:
dev = MKDEV(250, 0);
register_chrdev_region(dev , 1, "test");
2) 自动分配
自动分配无需指定主次设备号,只需要定义一个存放它们的变量,比如,dev_t mydev; 即可。分配函数为:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
其中,dev就是mydev; baseminor为次设备号的起始号,通常设为0; count和name意义同上。
例如:
int retval;
dev_t mydev; // 因为其它函数也会调用,这个变量通常定义为全局
retval = alloc_chrdev_region(&mydev, 0, 1, "test"); // 分配主次设备号
if(retval) {
printk(KERN_ERR "alloc_chrdev_region failed.\n");
goto alloc_region_err; // 见第三部分、倒影式编程错误处理
}
printk(KERN_INFO "major = %d, minor = %d\n", MAJOR(mydev), MINOR(mydev));
2、注册设备
1) 申请cdev结构体的内存空间
这里通常也有两种方法,即分别在堆或者栈上申请。
栈上申请,直接定义一个cdev结构体变量:
struct cdev mycdev;
堆上申请,先声明一个cdev结构体指针,再通过函数 cdev_alloc 申请内存:
struct cdev *pcdev;
pcdev = cdev_alloc();
2) 将cdev和file_operations绑定
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
比如:
cdev_init(&mycdev, &myfops); // 栈(以下同理)
cdev_init(pcdev, &myfops); // 堆
pcdev->owner = THIS_MODULE;
pcdev->ops = &myfops;
3) 注册设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
比如:
cdev_add(pcdev, mydev, 1);
稍显完整的例程如下(位于module_init链接的函数体内):
// 以下定义为全局变量
static dev_t mydev;
static struct cdev *pcdev;
static const struct file_operations myfops = {
.owner = THIS_MODULE,
// 操作函数
};
// 局部变量
int retval;
// 第1步:分配主次设备号
retval = alloc_chrdev_region(&mydev, 0, 0, "test");
if (retval < 0)
{
printk(KERN_ERR "Unable to alloc minors for %s\n", "test");
goto flag1;
}
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
// 第2步:申请cdev结构体
pcdev = cdev_alloc(); // 给pcdev分配内存,指针实例化
if(NULL == pcdev) {
printk(KERN_ERR "cdev_alloc failed.\n");
goto flag2;
}
// 第3步:将cdev和file_operations绑定
//cdev_init(pcdev, &myfops);
pcdev->owner = THIS_MODULE;
pcdev->ops = &myfops;
// 第4步:注册设备
retval = cdev_add(pcdev, mydev, 1);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag3;
}
// 其它处理省略
return 0;
flag3:
cdev_del(pcdev);
flag2:
unregister_chrdev_region(mydev, 1);
flag1:
return -EINVAL;
三、倒影式编程错误处理
无论新老接口,关于字符设备的注册通常都有一些通用的流程。对于新接口而言,在module_init链接的模块安装函数中,一般需要
第1步:分配主次设备号
第2步:申请cdev结构体
第3步:将cdev和file_operations绑定
第4步:注册设备
对应的,在module_exit链接的模块卸载函数中,则需要
第1步:注销设备
...
第2步:释放cdev结构体
第3步:注销申请的设备号
这就是所谓的倒影式编程。
cdev_del完成了注销设备和释放cdev结构体两步的工作:
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
而unregister_chrdev_region则完成注销申请的设备号的这个工作:
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}
但是在模块安装函数中的四步走不能保证每一步都成功,所以需要引入错误处理,其实现使用了C中不被提倡的goto语句,这在Linux内核中很常见。