当进程既要读取鼠标数据,又要读取触摸屏数据时,如果用阻塞方式操作的话,可能在读取鼠标数据时,由于没有数据,阻塞了,即使这时触摸屏有数据,也不能获取,要解决这个问题,我们有多种方案,比如多进程、多线程和这里要介绍的I/O多路复用。同样,我们对《字符设备驱动(6)-阻塞I/O》进行改造,使用I/O多路复用实现读写。由于历史原因,I/O多路复用,有select、poll及epoll三种方式,我们以select为例,进行说明。
如上图,用户态程序调用select、poll、epoll,在系统调用阻塞,内核空间中如果多个设备其中某一个设备的资源可用或者一个设备的某一个资源可用,则会唤醒poll系统调用,返回用户态程序,用户态程序对可用资源进行读或者写操作,完成这次I/O操作。
用户态程序
app.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#define FILE_NAME "/dev/mydev"
#define MAX_LEN 64
int fd;
void *func(void *arg)
{
char buf[MAX_LEN] = {0};
int rlen;
}
int main(void)
{
int ret,wlen,rlen;
fd_set rfd,wfd;
char wbuf[MAX_LEN] = "abcdefg";
char rbuf[MAX_LEN] = {0};
//注意以非阻塞方式打开文件
fd = open(FILE_NAME, O_RDWR);
if (0 > fd)
{
printf("Open failed.\n");
return -1;
}
while (1)
{
FD_ZERO(&rfd);
FD_ZERO(&wfd);
FD_SET(fd, &rfd);
FD_SET(fd, &wfd);
ret = select(fd + 1, &rfd, &wfd, NULL, 0);
if (0 > ret)
{
perror("Select");
return ret;
}
if (FD_ISSET(fd, &rfd))
{
rlen = read(fd, rbuf, MAX_LEN - 1);
if (rlen < 0)
{
perror("Read");
return rlen;
}
printf("Read buf is %s\n", rbuf);
}
if (FD_ISSET(fd,&wfd))
{
wlen = write(fd, wbuf, strlen(wbuf));
if (wlen < 0)
{
perror("Write error");
return wlen;
}
printf("Write buf is %s\n", wbuf);
}
}
close(fd);
return 0;
}
上面的程序,当有一个写资源可用时,会调用写函数,当有一个读资源可用时,会调用读函数。
驱动程序关键点
对三个系统调用,内核态驱动对应的操作都是poll方法:
struct file_operations {
__poll_t (*poll) (struct file *, struct poll_table_struct *);
}
第一个参数是文件指针。
第二个参数是poll_table_struct结构,用于在内核中实现select、poll、epoll系统调用,不必关心内部细节,可以当成一个不透明对象处理。
这个设备方法,分成两步处理:
1、在一个或者多个可指示poll状态变化的等待队列上调用pool_wait。如果当前没有文件描述符可用来执行I/O,则内核将使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。
2、返回一个用来描述操作是否可以立即无阻塞执行的位掩码。
第一步的pool_wait的原型如下:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
第一个参数是文件指针,有驱动透传。
第二个参数是等待队列头指针。
第三个参数是等待队列poll_table就是struct poll_table_struct,如下:
/*
* Do not touch the structure directly, use the access functions
* poll_does_not_wait() and poll_requested_events() instead.
*/
typedef struct poll_table_struct {
poll_queue_proc _qproc;
__poll_t _key;
} poll_table;
第二步返回描述哪个操作可以立即执行的位掩码,对应的位掩码定义在<linux/poll.h>中,通常我们常常使用其中四个,如下:
POLLIN:如果设备可以无阻塞地读取,设置此位。
POLLRDNORM:和POLLIN等价,一般一个可读设备返回POLLIN|POLLRDNORM。
POLLOUT:如果设备可以无阻塞地写取,设置此位。
POLLWRNORM:和POLLOUT等价,一般一个可写设备返回POLLOUT|POLLWRNORM。
驱动对应的poll函数实现如下:
#include <linux/poll.h>
static unsigned int my_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &my_write_queue, wait);//加写等待队列头
poll_wait(filp, &my_read_queue, wait);//加读等待队列头
if (0 != kbuf_len)//可读
{
mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
}
if (MAX_LEN != kbuf_len)//可写
{
mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
}
return mask;
}
struct file_operations cdev_ops = {
.open = my_open,
.release = my_close,
.read = my_read,
.write = my_write,
.poll = my_poll,
};
当第一个select进来时,kbuf_len = 0,my_poll返回POLLOUT | POLLWRNORM,表示可写,那用户态先写入abcdef,kbuf_len = 6,然后返回,再进入select,返回POLLIN | POLLRDNORM|POLLOUT | POLLWRNORM,表示可读可写,先读数据,kbuf_len = 0,然后写数据,kbuf_len=6,返回再进入select,表示可读可写,循环。
完整驱动程序
//head
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/string.h>
#include <linux/wait.h>
#include <linux/poll.h>
#define MAJOR_CHAR 100
#define MINOR_CHAR 0
#define MAX_LEN 64
static int my_open(struct inode *pnode, struct file *pfile)
{
printk("Open cdev.\n");
return 0;
}
static int my_close(struct inode *pnode, struct file *pfile)
{
printk("Close cdev.\n");
return 0;
}
static char kbuf[MAX_LEN] = {0};
static int kbuf_len = 0;
DECLARE_WAIT_QUEUE_HEAD(my_read_queue);
DECLARE_WAIT_QUEUE_HEAD(my_write_queue);
ssize_t my_read(struct file *pf, char __user *ubuf, size_t len, loff_t *pl)
{
int ret = -1;
if (MAX_LEN < len)
{
printk("len is large than %d.\n", MAX_LEN);
return -1;
}
//判断设备缓存是否为空
if (0 == kbuf_len)
{
//如果是非阻塞操作,返回-EAGAIN,用户态轮询
if (pf->f_flags& O_NONBLOCK)
return -EAGAIN;
else
{
//如果是阻塞操作,等待
if (wait_event_interruptible(my_read_queue, 0!= kbuf_len))
return -ERESTARTSYS; //信号唤醒,通知文件系统层做相应处理
}
}
ret = copy_to_user(ubuf, kbuf, len);
if (0 != ret)
{
printk("Copy to user failed.\n");
return -1;
}
//读出后,把对应的设备数据清空,保留未读出数据
memcpy(kbuf, kbuf + len, MAX_LEN - len);
memset(kbuf + MAX_LEN - len, 0, len);
//计算设备缓存长度
if (kbuf_len > len)
kbuf_len -= len;
else
kbuf_len = 0;
//当缓存长度不满时,唤醒写队列
if (MAX_LEN != kbuf_len)
wake_up_interruptible(&my_write_queue);
return len;
}
ssize_t my_write(struct file *pf, const char __user *ubuf, size_t len, loff_t *pl)
{
int ret = -1;
//判断是否会写超过
if (MAX_LEN < len + kbuf_len)
{
printk("len is large than %d.\n", MAX_LEN);
return -1;
}
if (MAX_LEN == kbuf_len)
{
//如果是非阻塞操作,返回-EAGAIN,用户态轮询
if (pf->f_flags& O_NONBLOCK)
return -EAGAIN;
else
{
//如果是阻塞操作,等待
if (wait_event_interruptible(my_write_queue, MAX_LEN != kbuf_len))
return -ERESTARTSYS; //信号唤醒,通知文件系统层做相应处理
}
}
ret = copy_from_user(kbuf + kbuf_len, ubuf, len);
if (0 != ret)
{
printk("Copy from user failed.\n");
return -1;
}
kbuf_len += len;
//当缓存长度不为空时,唤醒读队列
if (0 != kbuf_len)
wake_up_interruptible(&my_read_queue);
return len;
}
static unsigned int my_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
poll_wait(filp, &my_write_queue, wait);//加写等待队列头
poll_wait(filp, &my_read_queue, wait);//加读等待队列头
if (0 != kbuf_len)//可读
{
mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
}
if (MAX_LEN != kbuf_len)//可写
{
mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
}
return mask;
}
struct cdev cdevice;
struct file_operations cdev_ops = {
.open = my_open,
.release = my_close,
.read = my_read,
.write = my_write,
.poll = my_poll,
};
//加载
static int hello_init(void)
{
dev_t devno = MKDEV(MAJOR_CHAR,MINOR_CHAR);
int ret = -1;
printk(KERN_ALERT "Hello World.\n");
//up kernel
//1、注册设备号
ret = register_chrdev_region(devno, 1, "hello");
if (0 != ret)
{
printk("Register char device failed.\n");
return ret;
}
//2、初始化字符设备结构体
cdev_init(&cdevice, &cdev_ops);
cdevice.owner = THIS_MODULE;
//3、添加字符设备结构体给内核
ret = cdev_add(&cdevice,devno , 1);
if (0 != ret)
{
//注意释放设备号
unregister_chrdev_region(devno,1);
printk("Unregister char device.\n");
return ret;
}
printk("Register char device success.\n");
//down hardware
return 0;
}
//卸载函数(必须)
static void hello_exit(void)//返回值是void类型,函数名自定义,参数是void
{
dev_t devno = MKDEV(MAJOR_CHAR, MINOR_CHAR);
printk(KERN_ALERT "Goodbye World.\n");
// down hardware
// up kernel
//1、从内核中删除字符设备结构体
cdev_del(&cdevice);
//2、注销设备号
unregister_chrdev_region(devno, 1);
}
//注册(必须)
module_init(hello_init);
module_exit(hello_exit);
//license(必须)
MODULE_LICENSE("GPL");
//作者与描述(可选)
MODULE_AUTHOR("Ono Zhang");
MODULE_DESCRIPTION("A simple Hello World Module");
运行结果
$ sudo mknod /dev/mydev c 100 0
$ sudo insmod cdev.ko
$ sudo ./a.out
fds number is 1
Write buf is abcdefg
fds number is 2
Read buf is abcdefg
Write buf is abcdefg
fds number is 2
Read buf is abcdefg
Write buf is abcdefg
fds number is 2
Read buf is abcdefg
Write buf is abcdefg
……
tobecontinue
每周三、周六更新