Linux驱动—实现一个驱动支持多个设备

x33g5p2x  于2022-05-11 转载在 Linux  
字(6.4k)|赞(0)|评价(0)|浏览(518)

前面内容:
前面内容:
1 Linux驱动—内核模块基本使用

2 Linux驱动—内核模块参数,依赖(进一步讨论)

3 字符设备驱动

4 虚拟串口设备驱动

每个设备都写一个驱动太麻烦了,所以要Linux驱动—实现一个驱动支持多个设备。

对于多设备引入的变化:
我们首先要向 内核注册多个设备号
其次就是在添加cdev对象时指明改cdev对象管理了多个设备;
或者添加多个cdev对象,每个cdev对象管理一个设备

接下来最麻烦的部分在于读写操作,因为设备是多个,那么设备对应的资源也应该是多个(比如虚拟串口驱动中的FIFO)。 在读写操作时,怎么来区分究竟应该对哪个设备进行操作呢(对于虚拟串口驱动而言,就是要确定对哪个FIFO进行操作) ?

观察读和写函数,没有发现能够区别设备的形参。
再观察open接口,我们会发现有一个inode 形参,通过前面的内容我们知道,inode里面包含了对应设备的设备号以及所对应的cdev对象的地址。

因此,我们可以在open接口函数中取出这些信息(inode里面包含了对应设备的设备号以及所对应的cdev对象的地址),并存放在file结构对象的某个成员中,再在读写的接口函数中获取该file结构的成员,从而可以区分出对哪个设备进行操作。

下面首先展示用一个cdev实现对多个设备的支持

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kfifo.h>

#define VSER_MAJOR	256
#define VSER_MINOR	0
#define VSER_DEV_CNT	2
#define VSER_DEV_NAME	"vser"

static struct cdev vsdev;
static DEFINE_KFIFO(vsfifo0, char, 32);
static DEFINE_KFIFO(vsfifo1, char, 32);

static int vser_open(struct inode *inode, struct file *filp)
{
	switch (MINOR(inode->i_rdev)) {
	default:
	case 0:
		filp->private_data = &vsfifo0;
		break;
	case 1:
		filp->private_data = &vsfifo1;
		break;
	}
	return 0;
}

static int vser_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	unsigned int copied = 0;
	struct kfifo *vsfifo = filp->private_data;

	kfifo_to_user(vsfifo, buf, count, &copied);

	return copied;
}

static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
	unsigned int copied = 0;
	struct kfifo *vsfifo = filp->private_data;

	kfifo_from_user(vsfifo, buf, count, &copied);

	return copied;
}

static struct file_operations vser_ops = {
	.owner = THIS_MODULE,
	.open = vser_open,
	.release = vser_release,
	.read = vser_read,
	.write = vser_write,
};

static int __init vser_init(void)
{
	int ret;
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);
	ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
	if (ret)
		goto reg_err;

	cdev_init(&vsdev, &vser_ops);
	vsdev.owner = THIS_MODULE;

	ret = cdev_add(&vsdev, dev, VSER_DEV_CNT);
	if (ret)
		goto add_err;

	return 0;

add_err:
	unregister_chrdev_region(dev, VSER_DEV_CNT);
reg_err:
	return ret;
}

static void __exit vser_exit(void)
{
	
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);

	cdev_del(&vsdev);
	unregister_chrdev_region(dev, VSER_DEV_CNT);
}

module_init(vser_init);
module_exit(vser_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");

来分析下代码:

这里把CNT改成2 说明支持2个设备

static struct cdev vsdev;
static DEFINE_KFIFO(vsfifo0, char, 32);
static DEFINE_KFIFO(vsfifo1, char, 32);

定义了二个FIFO,vsfifo0和1 (这里用的是动态分配fifo要更好,但是后面会涉及内存分配的知识,所以先用静态的)

在open接口函数中根据次设备号的值来确定保存哪个FIFO结构的(vsfifo0还是1)地址到file结构中的private_data 的值,即FIFO结构的地址

static int vser_open(struct inode *inode, struct file *filp)
{
	switch (MINOR(inode->i_rdev)) {
	default:
	case 0:
		filp->private_data = &vsfifo0;
		break;
	case 1:
		filp->private_data = &vsfifo1;
		break;
	}
	return 0;
}

次设备号 0 选择存 vsfifo0
次设备号 1 选择存 vsfifo1

接下来演示如何将每一个cdev对象对应到一个设备来实现一个驱动对多个设备的支持

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kfifo.h>

#define VSER_MAJOR	256
#define VSER_MINOR	0
#define VSER_DEV_CNT	2
#define VSER_DEV_NAME	"vser"

static DEFINE_KFIFO(vsfifo0, char, 32);
static DEFINE_KFIFO(vsfifo1, char, 32);

struct vser_dev {
	struct kfifo *fifo;
	struct cdev cdev;
};

static struct vser_dev vsdev[2];

static int vser_open(struct inode *inode, struct file *filp)
{
	filp->private_data = container_of(inode->i_cdev, struct vser_dev, cdev);
	return 0;
}

static int vser_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
	unsigned int copied = 0;
	struct vser_dev *dev = filp->private_data;

	kfifo_to_user(dev->fifo, buf, count, &copied);

	return copied;
}

static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
	unsigned int copied = 0;
	struct vser_dev *dev = filp->private_data;

	kfifo_from_user(dev->fifo, buf, count, &copied);

	return copied;
}

static struct file_operations vser_ops = {
	.owner = THIS_MODULE,
	.open = vser_open,
	.release = vser_release,
	.read = vser_read,
	.write = vser_write,
};

static int __init vser_init(void)
{
	int i;
	int ret;
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);
	ret = register_chrdev_region(dev, VSER_DEV_CNT, VSER_DEV_NAME);
	if (ret)
		goto reg_err;

	for (i = 0; i < VSER_DEV_CNT; i++) {
		cdev_init(&vsdev[i].cdev, &vser_ops);
		vsdev[i].cdev.owner = THIS_MODULE;
		vsdev[i].fifo = i == 0 ? (struct kfifo *) &vsfifo0 : (struct kfifo*)&vsfifo1;

		ret = cdev_add(&vsdev[i].cdev, dev + i, 1);
		if (ret)
			goto add_err;
	}

	return 0;

add_err:
	for (--i; i > 0; --i)
		cdev_del(&vsdev[i].cdev);
	unregister_chrdev_region(dev, VSER_DEV_CNT);
reg_err:
	return ret;
}

static void __exit vser_exit(void)
{
	int i;
	dev_t dev;

	dev = MKDEV(VSER_MAJOR, VSER_MINOR);

	for (i = 0; i < VSER_DEV_CNT; i++)
		cdev_del(&vsdev[i].cdev);
	unregister_chrdev_region(dev, VSER_DEV_CNT);
}

module_init(vser_init);
module_exit(vser_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");

代码:

struct vser_dev {
	struct kfifo *fifo;
	struct cdev cdev;
};

这里定义了一个结构类型 vser_dev,代表一种具体的设备类

通常和设备有关的内容都可以跟cdev一起定义到一个结构中这样更容易

cdev 是所有字符设备的一个抽象,是一个基类,而一个具体类型的设备应该是由该基类派生出来的一个子类,子类包含了特定设备所特有的属性,比如vser_ dev 中的fifo,这样子类就更能刻画好一类具体的设备。

代码

static struct vser_dev vsdev[2];

创建了两个vser_dev 类型的对象,和C++不同的是,创建这两个对象仅仅是为其分配了内存,并没有调用构造函数来初始化这两个对象,但在代码的第74行到第77行完成了这个操作。

for (i = 0; i < VSER_DEV_CNT; i++) {
		cdev_init(&vsdev[i].cdev, &vser_ops);
		vsdev[i].cdev.owner = THIS_MODULE;
		vsdev[i].fifo = i == 0 ? (struct kfifo *) &vsfifo0 : (struct kfifo*)&vsfifo1;

你看,初始化init cdev

查看内核源码,会发现这种面向对象的思想处处可见,只能说因为语言的特性,并没有把这种形式体现得很明显而已。

代码的第74行到第82行通过两次循环完成了两个cdev对象的初始化和添加工作,并且初始化了fifo 成员的指向。

for (i = 0; i < VSER_DEV_CNT; i++) {
		cdev_init(&vsdev[i].cdev, &vser_ops);
		vsdev[i].cdev.owner = THIS_MODULE;
		vsdev[i].fifo = i == 0 ? (struct kfifo *) &vsfifo0 : (struct kfifo*)&vsfifo1;

		ret = cdev_add(&vsdev[i].cdev, dev + i, 1);
		if (ret)
			goto add_err;
	}

如果i为0是第一个,指向vsfifo0这个结构的地址
如果i为1是第二个,指向vsfifo1这个结构的地址

这里需要说明的是,用DEFINE_ KFIFO定义的FIFO,每定义一个FIFO就会新定义一种数据类型,所以严格来说vsfifo0和vsfifo1是两种不同类型的对象,但好在这里能和struct kfifo类型兼容。

代码第26行

filp->private_data = container_of(inode->i_cdev, struct vser_dev, cdev);

用到了一个container_ of宏,这是在Linux内核中设计得非常巧妙的一个宏,在整个Linux内核源码中几乎随处可见。

它的作用就是**根据结构成员的地址来反向得到结构的起始地址**

在代码中,inode->i_cdev 给出了struct vser_dev 结构类型中cdev成员的地址(见图3.2),通过container_ of宏就得到了包含该cdev的结构地址

使用上面两种方式都可以实现一个驱动对多个同类型设备的支持。使用下面的命令可以测试这两个驱动程序。

完成

相关文章