阻塞型I/O相对于非阻塞型I/O来说,最大的有点就是再设备的资源不可用时,进程主动放弃CPU,让其他进程运行,而不用不停的轮询,有助于提高整个系统的效率。但是其缺点也是比较明显的,那就是进程阻塞后,不能做其他的操作,这在一个进程中要同时对多个设备进行操作时显得非常不方便。比如一个进程既要读取键盘的数据,又要读取串口的数据,那么如果都是用阻塞方式进行操作的话,如果因为读取键盘而使进程阻塞,即便串口收到了数据,也不能及时获取。解决这个问题的方法有很多种,比如多进程,多线程和I/O多路复用。在这里我们来讨论下I/O多路复用的实现。
在应用层,由于历史原因,I/O多路复用有select,poll以及linux特有的epoll三种方式,这里我们已poll为例来进行说明。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll的第一个参数是监听的文件描述符集合,类型为指向struct pollfd的指针,struct pollfd有三个成员,fd是监听的文件描述符,events是监听的事件,revents是返回的事件。常见的事件有POLLIN、POLLOUT,分别表示设备可以无阻塞的读、写。POLLRDNORM和POLLWRNORM是在_XOPEN_SOURCE宏被定义时引入的事件,第二个参数是要监听的文件描述符的个数,第三个参数是毫秒的超时值,负数表示一直监听,知道监听的文件描述符集合中的任意一个设备发生了事件才会返回。如果有一个程序既要监听键盘,又要监听串口,当用户按下键盘后,将键值转换成字符串后通过串口发送出去,当串口接收到数值后,在屏幕上显示,那么可以用下面的应用程序来实现。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
#include <linux/input.h>
int main(int argc, char *argv[])
{
int ret;
struct pollfd fds[2];
char rbuf[32];
char wbuf[32];
struct input_event key; //定义按键监测的key value
fds[0].fd = open("/dev/vser", O_RDWR | O_NONBLOCK);
if (fds[0].fd == -1)
goto fail;
fds[0].events = POLLIN; //初始化关心事件,串口读数据
fds[0].revents = 0;
fds[1].fd = open("/dev/input/event1", O_RDWR | O_NONBLOCK);
if (fds[1].fd == -1)
goto fail;
fds[1].events = POLLIN; //初始化关心事件,keyboard读数据
fds[1].revents = 0;
while (1)
{
ret = poll(fds, 2, -1);
if (ret == -1)
goto fail;
if(fds[0].revents & POLLIN) //当我们往串口写入数据后,就表示串口此时可读,revents就会返回pollin
{
ret = read(fds[0].fd, rbuf, sizeof(rbuf));
if(ret < 0)
goto fail;
puts(rbuf);
}
if(fds[1].revents & POLLIN) //键盘收到可读数据
{
ret = read(fds[1].fd, &key, sizeof(key));
if (ret < 0)
goto fail;
if(key.type == EV_KEY)
{
sprintf(wbuf, "0x%x\n", key.code);
printf("0x%x\n", key.code);
ret = write(fds[0].fd, wbuf, strlen(wbuf)+1);
if (ret < 0)
goto fail;
}
}
}
fail:
perror("poll test");
exit(EXIT_FAILURE);
}
内核中我们只要把poll的方法实现即可
/*************************************************************************
> File Name: blockio.c
> Author: longway.bai
> Mail: 953821672@qq.com
> Created Time: 2021年12月19日 星期日 14时12分50秒
************************************************************************/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kfifo.h>
#include <linux/poll.h>
#define CHRDEV_MAJOR 256
#define CHRDEV_MINOR 0
#define CHRDEV_CNT 1
#define CHRDEV_NAME "vser"
//static struct cdev vsdev;
struct vser_dev {
struct cdev cdev;
wait_queue_head_t rwqh;
wait_queue_head_t wwqh;
struct kfifo *fifo;
};
static struct vser_dev vsdev;
DEFINE_KFIFO(vsfifo, char, 32);
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)
{
int ret;
unsigned int copied;
struct vser_dev *dev = filp->private_data;
if(kfifo_is_empty(dev->fifo))
{
/*用户层已非阻塞的方式读,且kfifo已经empty的话则返回error*/
if(filp->f_flags & O_NONBLOCK)
return -EAGAIN;
/*fifo为空的情况下进程休眠,知道fifo不为空或者接收到信号才被唤醒,如果是被信号唤醒,则返回-ERESTARTSYS*/
if(wait_event_interruptible(dev->rwqh, !kfifo_is_empty(dev->fifo)))
return -ERESTARTSYS;
}
ret = kfifo_to_user(dev->fifo, buf, count, &copied);
/*当fifo不满的时候唤醒所有等待的写进程*/
if (!kfifo_is_full(dev->fifo))
wake_up_interruptible(&dev->wwqh);
return ret == 0 ? copied : ret;
}
static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
int ret;
unsigned int copied;
struct vser_dev *dev = filp->private_data;
if(kfifo_is_full(dev->fifo))
{
/*用户层已非阻塞的方式写,且kfifo已经full的话则返回error*/
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
/*fifo为满的情况下进程休眠,知道fifo不满或者接收到信号才被唤醒,如果是被信号唤醒,则返回-ERESTARTSYS*/
if(wait_event_interruptible(dev->wwqh, !kfifo_is_full(dev->fifo)))
return -ERESTARTSYS;
}
ret = kfifo_from_user(dev->fifo, buf, count, &copied);
/*当fifo不空的时候唤醒所有等待的读进程*/
if (!kfifo_is_empty(dev->fifo))
wake_up_interruptible(&dev->rwqh);
return ret == 0 ? copied : ret;
}
unsigned int vser_poll(struct file *filp, struct poll_table_struct *p)
{
int mask = 0;
struct vser_dev *dev = filp->private_data;
/*将系统调用中构造的队列节点加入到相应的等待队列中*/
poll_wait(filp, &dev->rwqh, p);
poll_wait(filp, &dev->wwqh, p);
/*根据资源的情况返回设置mask的值并返回*/
if(!kfifo_is_empty(dev->fifo))
mask |= POLLIN | POLLRDNORM;
if(!kfifo_is_full(dev->fifo))
mask |= POLLOUT | POLLWRNORM;
return mask;
}
static struct file_operations vser_ops = {
.owner = THIS_MODULE,
.open = vser_open,
.release = vser_release,
.read = vser_read,
.write = vser_write,
.poll = vser_poll,
};
static int __init vser_init(void)
{
int ret;
dev_t dev;
dev = MKDEV(CHRDEV_MAJOR, CHRDEV_MINOR);
ret = register_chrdev_region(dev, CHRDEV_CNT, CHRDEV_NAME);
if (ret)
goto reg_err;
cdev_init(&vsdev.cdev, &vser_ops);
vsdev.cdev.owner = THIS_MODULE;
ret = cdev_add(&vsdev.cdev, dev, CHRDEV_CNT);
if (ret)
goto add_err;
vsdev.fifo = (struct kfifo*)&vsfifo;
/*初始化一个等待列头*/
init_waitqueue_head(&vsdev.rwqh);
init_waitqueue_head(&vsdev.wwqh);
return 0;
add_err:
unregister_chrdev_region(dev, CHRDEV_CNT);
reg_err:
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev;
dev = MKDEV(CHRDEV_MAJOR, CHRDEV_MINOR);
cdev_del(&vsdev.cdev);
unregister_chrdev_region(dev, CHRDEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("longway<longway.bai@outlook.com>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
poll的驱动代码看起来虽然非常简单,但是代码背后的机制却是比较复杂的。
poll系统调用在内核中对应的函数是sys_poll,该函数调用do_sys_poll来完成具体工作,在do_sys_poll函数中有一个for循环,这个循环将会构造一个poll_list结构,其主要作用是把用户层传递过来的struct pollfd复制到poll_list中,并记录监听的文件个数(包括文件描述符和关心的事件),之后调用poll_initwait函数,该函数构造一个poll_wqueues结构,并初始化其中部分成员,包括将pt指针指向一个poll_table的结构,poll_table结构中有一个函数指针指向__poll_wait;接下来调用do_poll函数,do_poll函数内有两层for循环,内层的for循环将会遍历poll_list中每一个struct pollfd结构,并对应初始化poll_wqueues中的每一个poll_table_entry(关键是要构造一个等待列队节点,然后指定唤醒该节点后调用的函数为poll_wake),接下来根据fd找到对应的file结构,从而调用启动中poll接口函数,驱动中的poll接口函数将会调用poll_wait辅助函数,该函数有会调用之前在初始化poll_wqueues时指定的__poll_wait函数,__poll_wait函数的主要作用是将刚才构造好的等待队列节点加入到驱动等待队列中:接下来驱动的poll接口函数判断资源是否可用,并返回状态给mask;如果内核循环所有调用的每一个驱动的poll函数接口都返回,没有相应的事件发生,那么就会调用poll_schedule_timeout将poll系统调用休眠;当设备可用后,通常会产生一个中断(或由另外一个进程中的某个操作使资源可用),在对应的中断处理函数中,将会调用wake_up函数,将该驱动对应资源的等待列队上的进程唤醒,这是也会把刚才因为poll系统调用所加入的节点出队,并调用相应的函数,即poll_wait函数,该函数负责唤醒因调用poll_schedule_timeout函数而休眠的poll系统调用,poll系统调用唤醒后,回到外层的for循环继续执行,这次执行再次遍历所有的驱动中的poll函数接口后,会发现至少一个关心的事件,于是将该事件记录在struct pollfd的revents成员中,然后跳出外层的for循环,将内核的struct pollfd复制到用户层,poll系统调用最终返回,并会饭多少个被监听的文件有关心的事件产生。
sys_poll();
do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,struct timespec *end_time);
poll_initwait(&table);
init_poll_funcptr(&pwq->pt, __pollwait);>table->qproc =__pollwait;
do_poll(nfds, head, &table, end_time);
for (;;) {
if (do_pollfd(pfd, pt)) { //mask = file->f_op->poll(file, pwait);return mask ----调用驱动中的poll函数
count++;//如果驱动的poll函数返回非零,那么count++
pt = NULL;
}
//break条件:count非零,超时,有信号等待处理
if (count || ! timed_out||signal_pending(current))
break;
//休眠__timeout,期间没事发生,则timed_out减为零,再次循环,break
//休眠期间被中断唤醒等待队列,则再次循环,执行驱动中的poll,返回非零mask, 则break
__timeout=schedule_timeout(__timeout);
}
上面的过程比较复杂,而poll系统调用又可以随时添加新的要监听的文件描述符,所以在内核中,相应的数组还可能动态扩充,从而整个过程更复杂一些。但是,其宗旨只有一个,那就是遍历所有被监听的设备驱动中poll接口函数,如果没有关心的事件发生,那么poll系统就会休眠,直到至少有一个驱动唤醒它为止。
综上,再结合vser_poll里面的注释就比较容易理解了