目录
虚拟串口设备驱动
一个驱动支持多个设备
习题
虚拟串口设备驱动
字符设备驱动除了前面搭建好代码的框架外,接下来最重要的就是要实现特定于设备的操作方法,这是驱动的核心和关键所在,是一个驱动区别于其他驱动的本质所在,是整个驱动代码中最灵活的代码所在。了解了虚拟串口设备的工作方式后,接下来就可以针对性的编写驱动程序,代码如下:
#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 1
#define VSER_DEV_NAME "vser"
static struct cdev vsdev;
DEFINE_KFIFO(vsfifo, char, 32);
static int vser_open(struct inode *inode, struct file *filp)
{
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;
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;
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("name <E-mail>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
新的驱动在代码第15行定义并初始化了一个名叫 vsfifo 的 struct kfifo 对象,每个元素的数据类型为char,共有32个元素的空间。代码第17行到第25行设备打开和关闭函数,分别对应于file_operations 内的open和release 方法。因为是虚拟设备,所以这里并没有需要特别处理的操作,仅仅返回 0表示成功。这两个函数都有两个相同的形参,第一个形参是要打开或关闭文件的inode,第二个形参则是打开对应件后由内核构造并初始化好的file结构,在前面我们已经较深入地分析了这两个对象的作用。这里之所以叫release而不叫close是因为一个文件可以被打开多次,那么vser_open函数相应地会被调用多次,但是关闭文件只有到最后一个close操作才会导致vser_release函数被调用,所以用 release 更贴切。
代码第27第34行是read系统调用驱动实现,这里主要是把FIFO中的数据返回给用户层,使用了kfifo_to_user 这个宏。read系统调用要求用户返回实际读取的字节数,而copied变量的值正好符合这一要求。代码36到第43对应的write系统调用的驱动实现,同read系统调用一样,只是数据流向相反而已。
读和写函数引入了3个新的形参,分别是buf,count和pos,根据上面的代码,已经不难发现它们的含义。buf代表的是用户空间的内存起始地址;count表示用户想要读写多少个字节的数据:而pos是文件的位置指针,在虚拟串口这个不支持随机访问的设备中,该参数无用。_user是提醒驱动代码编写者,这个内存空间属于用户空间。
代码第 47 行到第 50 行是将file_operations中的函数指针分别指向上面定义的函数这样在应用层发生相应的系统调用后,在驱动里面的函数就会被相应地调用。上面这个示例实现了一个功能非常简单,但是基本可用的虚拟串口驱动程序。按照下面的步骤可以进行验证。
通过实验结果可以看到,对/dev/vser0写入什么数据,就可以从这个设备读到什么数据,和一个具备内环回功能的串口是一致的。
为了方便读者对照查阅,特将file_operations结构类型的定义代码列出。从中我们可以看到,还有很多接口函数还没有实现,在后面的章节中,我们会陆续再实现一些接口。显然,一个驱动对下面的接口的实现越多,它对用户提供的功能就越多,但这也不是说我们必须要实现下面的所有函数接口。比如串口不支持随机访问,那么llseek函数接口自然就不用实现。
1525 struct file_operations {
1526 struct module *owner;
1527 loff_t (*llseek) (struct file *, loff t, int);
1528 ssize_t (*read) (struct file *, char__user *, size t, loff t *);
1529 ssize_t (*write) (struct file *, const char _user *, size t, loff t*);
1530 ssize t (*alo read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1531 ssize t (*aio write) (struct kiocb *, const struct iovec *, unsigned long, loff t);1532 int (iterite) latruct tile ', atruet dir_context *);
1533 unsigned int (*poll) (struct file *, strunt poll_table_struct *);
1534 long (unlocked ioctl) (struct file *, unsigned int, unsigned long);1535 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1536 int (*mmap) (struct file *, struct vm_area_struct *);
1537 int (*open) (struct inode *, struct file *);
1538 int (*flush) (struct file *, f1_owner_t id);
1539 int (*release) (struct inode *, struct file *);
1540 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1541 int (*aio_fsync) (struct kiocb *, int datasync);
1542 int (*fasync) (int, struct file *, int);
1543 int (*lock) (struct file *, int, struct file_lock *);
1544 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,int);
1545 unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1546 int (*check_flags)(int);
1547 int (*flock) (struct file *, int, struct file_lock *);
1548 ssize_t (*splice_write) (struct pipe_inode_info *, struct file *, lofft *, size_t, unsigned int);
1549 ssize_t (*splice_read) (struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
1550 int (*setlease) (struct file *, long, struct file_lock **);
1551 long (*fallocate)(struct file *file, int mode, loff_t offset,
1552 loff_t len);
1553 int (*show_fdinfo)(struct seq_file *m, struct file *f);
1554 };
一个驱动支持多个设备
如果一类设备有多个个体(比如系统上有两个串口),那么我们就应该写一个驱动来支持这几个设备,而不是每一个设备都写一个驱动。对于多个设备所引入的变化是什么呢?首先我们应向内核注册多个设备号,其次就是在添加 cdev对象时指明该cdev对象管理了多个设备;或者添加多个 cdev 对象,每个cdev对象管理一个设备。接下来最麻烦的部分在于读写操作,因为设备是多个,那么设备对应的资源也应该是多个(比如虚拟串口驱动中的FIFO)。在读写操作时,怎么来区分究竟应该对哪个设备进行操作呢(对于虚拟串口驱动而言,就是要确定对哪个FIFO 进行操作)?观察读和写函数,没有发现能够区别设备的形参。再观察open 接口,我们会发现有一个inode形参,通过前面的内容我们知道,inode里面包含了对应设备的设备号以及所对应的cdev对象的地址。因此,我们可以在open接口函数中取出这些信息,并存放在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("name <E-mail>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
上面的代码针对前一示例做的修改是:将VSER_DEV_CNT定义为2,表示支持两个设备;用DEFINE_KFIFO,分别是vsfifo0和vsfifo1(很显然,这里动态分配FIFO要优于静态定义,但是这会涉及后面章节中内核内存分配的相关知识,故此使用静态的方法);在open接口函数中根据次设备号的值来确定保存哪个FIFO结构体的地址到file结构中的private_data成员中,file结构中的private_data是一个void *类型的指针,内核保证不会使用该指针,所以正如其名一样,是驱动私有的;在读写接口函数中则是先从file结构中取出private_data的值,即FIFO结构的地址,然后再进一步操作。
接下来演示如何将每一个edev对象对应到一个设备来实现一个驱动对多个设备的支持
#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("name <E-mail>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
代码第17行至第20行新定义了一个结构类型vser_dev,代表一种具体的设备类,通常和设备相关的内容都应该和cdev定义在一个结构中。如果用面向对的思想理解这种做法将会变得很容易。cdev是所有字符设备的一个抽象,是一个基类,而一个具体类型的设备应该是由该基类派生出来的一个子类,子类包含了特定设备所特有的强性,比如vser_dev中的fifo,这样子类就更能刻画好一类具体的设备。代码第22行创建了两个vser_dev类型的对象,和C++不同的是,创建这两个对象仅仅是为其分配了内存并没有调用构造函数来初始化这两个对象,但在代码的第 74行到第77行完成了这个作。查看内核源码,会发现这种面向对象的思想处处可见,只能说因为语言的特性,并没有把这种形式体现得很明显而已。代码的第 74行到第82行通过两次循环完成了两个cdev对象的初始化和添加工作,并且初始化了fifo成员的指向。这里需要说明的是,用 DEFINE_KFIFO 定义的FIFO,每定义一个FIFO就会新定义一种数据类型,所以严格来说 vsfifo0和vsfifo1是两种不同类型的对象,但好在这里能和struct kfifo类型兼容。
代码第26行用到了一个container_of宏,这是在Linux内核中设计得非常巧妙的一个宏,在整个Linux内核源码中几乎随处可见。它的作用就是根据结构成员的地址来反向得到结构的起始地址。在代码中,inode->i_cdev给出了struct vser_dev结构类型中cdev成员的地址(见图3.2),通过container_of宏就得到了包含该 cdev的结构地址。
使用上面两种方式都可以实现一个驱动对多个同类型设备的支持。使用下面的命令可以测试这两个驱动程序。
make ARCH=arm
./lazy
上面再ubuntu中下面再开发板中
mknod /dev/vser0 c 256 0
mknod /dev/vser1 c 256 1depmod
modprobe vser
echo "11111" > /dev/vser0
echo "22222" > /dev/vser1
cat /dev/vser0
cat /dev/vser1
这俩就会分别打印出来。没带开发板,但是现象绝对没问题。
习题
1.字符设备和块设备的区别不包括( B)。
[A]字符设备按字节流进行访问,块设备按块大小进行访问
[B]字符设备只能处理可打印字符,块设备可以处理二进制数据
[C]多数字符设备不能随机访问,而块设备一定能随机访问
[D] 字符设备通常没有页高速缓存,而块设备有
2.在3.14.25 版本的内核中,主设备号占(C )位,次设备号占(D )位。
[A]8 [B]16 [C] 12 [D] 20
3.用于分配主次设备号的函数是(C )。
[A]register_chrdev_region [B] MKDEV
[C]alloc_chrdev_region [D] MAJOR
4.在字符设备驱动中,struct file_operations 结构中的函数指针成员不包含( B)。
[A]open [B]close [C] read [D] show_fdinfo