Linux设备驱动--字符设备(二、新注册接口)

字符设备(一)中已经介绍了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);    // 堆


这里需要注意,若使用自动分配函数alloc_chrdev_region来分配设备号,则绑定file_operations时可以直接绑定而不需要使用cdev_init 。比如:

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内核中很常见。









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值