前面内容:
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
#include
#include
#include
#include
#include
#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 " );
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
内的open
和release
方法。
因为是虚拟设备,所以这里并没有需要特别处理的操作,仅仅返回0表示成功。
这两个函数都有两个相同的形参,
在前面的章节中我们已经较深入地分析了这两个对象的作用。
在这里之所以叫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函数接口自然就不用实现。