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

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

3 字符设备驱动

先学习下虚拟串口设备是啥?

虚拟串口设备

在进一步实现字符设备驱动之前,我们先来讨论一下这本书中用到的一个虚拟串口设备。这个设备是驱动代码虚拟出来的,不能实现真正的串口数据收发,但是它能够接收用户想要发送的数据,并且将该数据原封不动地环回给串口的收端,使用户也能从该串口接收数据。也就是说,该虚拟串口设备是一个功能弱化之后的只具备内环回作用的串口,如图3.3所示。
在这里插入图片描述

这一功能的实现

主要是在驱动中实现了一个FIFO,驱动接收用户层传来的数据,然后将之放入FIFO,当应用层要获取数据时,驱动将FIFO中的数据读出,然后复制给应用层。
一个更贴近实际的形式应该是在驱动中有两个FIFO,一个用于发送,一个用于接收,但是这并不是实现这个简单的虛拟串口设备驱动的关键,所以为了简单起见,这里只用了一个FIFO。

内核中已经有了一个关于FIFO的数据结构struct kfifo,相关的操作宏或函数的声明、
定义都在“include/inux/kfifo.h"头文件中,下面将最常用的宏罗列如下。

DEFINE_KFIFO(fifo,type,size)
kfifo_from_user(fifo,from,len,copied)
kfifo_to_user(fifo,to,len,copied)

DEFINE_ KFIFO用于定义并初始化一个FIFO,这个变量的名字由fifo参数决定, type是FIFO中成员的类型size 则指定这个FIFO有多少个元素但是元素的个数必须是2的幂

kfifo_from_user 是将用户空间的数据(from) 放入FIFO中,元素个数由len来指定,实际放入的元素个数由copied返回

kfifo_to_user 则是将FIFO中的数据取出,复制到用户空间(to)。 len 和copied的含义同kfifo_ from_ user 中对应的参数。

虚拟串口设备驱动

字符设备驱动除了前面搭建好的框架外,接下来最重要的是实现设备的操作方法。

#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("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_ALIAS("virtual-serial");

新的代码驱动在代码DEFINE_KFIFO(vsfifo, char, 32);定义并初始化了一个名叫vsfifo的structu kfifo对象,每个对象的类型为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;
}

代码实现了设备的打开和关闭函数,分别对应于file_operations 内的openrelease方法。
因为是虚拟设备,所以这里并没有需要特别处理的操作,仅仅返回0表示成功。
这两个函数都有两个相同的形参,

  1. 第一个形参是要打开或关闭文件的inode,
  2. 第二个形参则是打开对应文件后由内核构造并初始化好的file 结构

在前面的章节中我们已经较深入地分析了这两个对象的作用。
在这里之所以叫release而不叫close是因为一个文件可以被打开多次,那么vser_ open函数相应地会被调用多次,但是关闭文件只有到最后一个close操作才会导致vser_ release函数被调用,所以用release更贴切。

代码

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;
}

是read系统调用的驱动实现,这里主要把FIFO中的数据返回给用户层,使用了kfifo_to_user这个宏。返回给用户层。
read 系统调用要求用户返回实际读取的字节数,而copied变量的值正好符合这一要求。

代码第36行到第43行是对应的write系统、

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;
}
  

调用的驱动实现,同read系统调用一样,只是数据流向相反而已。

读和写函数引入了3个新的形参,分别是buf, count 和pos,根据上面的代码,已经不难发现它们的含义。buf 代表的是用户空间的内存起始地址; count 表示用户想要读写多少个字节的数据:而pos是文件的位置指针,在虚拟串口这个不支持随机访问的设备中,该参数无用。_user是提醒驱动代码编写者,这个内存空间属于用户空间。

代码

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

这里是将file_operations 的函数指针分别指向上面定义的函数
你看 .read指向vser_read函数

这样在驱动这样在应用层发生相应的系统调用后,在驱动里面的函数就会被相应地调用

上面这个示例实现了一个功能非常简单,但是基本可用的虚拟串口驱动程序。

按照下面的步骤可以进行验证。

先创建设备号

sudo mknod /dev/vser0 c 256 0 

在这里插入图片描述

然后编译运行
make
make modules_install
sudo modpeobe vser
在这里插入图片描述

然后 往这个设备中dev/vser0写入数据
echo "vser deriver test" > /dev/vser0
然后查看
cat /dev/vser0
最后
会出现
vser deriver test

通过实验结果可以看到,对/dev/vser0 写入什么数据,就可以从这个设备读到什么数据,和一个具备内环回功能的串口是一致的。

为了方便读者对照查阅,特将file_operations 结构类型的定义代码列出。从中我们可以看到,还有很多接口函数还没有实现,在后面的章节中,我们会陆续再实现一些接口。

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*mremap)(struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **, void **);
    long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
    void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
};

显然,一个驱动对下面的接口的实现越多,它对用户提供的功能就越多,但这也不是说我们必须要实现下面的所有函数接口。比如串口不支持随机访问,那么lseek函数接口自然就不用实现。

Logo

更多推荐