I/O多路复用

x33g5p2x  于2022-05-16 转载在 其他  
字(5.2k)|赞(0)|评价(0)|浏览(362)

阻塞型IO相对于非阻塞型IO来说,最大的优点就是在设备的资源不可用时,进程主动放弃CPU,让其他的进程运行,而不用不停地轮询,有助于提高整个系统的效率。

但是其缺点也是比较明显的,那就是进程阻塞后,不能做其他的操作,这在一个进程要同时对多个设备进行操作时显得非常不方便。比如一个进程既要读取键盘的数据,又要读取串口的数据,那么如果都是用阻塞的方式进行操作的话,如果因为读取键盘而使进程阻塞,即便串口收到了数据,也不能及时获取。

解决这个问题的方法有多种,比如多进程、多线程和I/O多路复用。

在这里我们来讨论I/O多路复用的实现,首先回顾一下在应用层中,I/O多路复用的相关操作。在应用层,由于历史原因,I/O多路复用有select、poll以及Linux所特有的epoll三种方式。这里以poll为例来进行说明,poll 系统调用的原型及相关的数据类型如下。

poll:

int poll(struct pollfd *fds,nfds_t nfds,int timeout);

struct pollfd{
    int fd;
    short events;
    short revents;
};
POLLIN There is data to read
POLLOUT Writing now will not block
POLLRDNORM Equivalent to POLLIN
POLLWRNORM Equivalent to POLLOUT 

//有数据要读  
现在POLLOUT写入不会被阻塞  
POLLRDNORM相当于POLLIN  
POLLWRNORM相当于POLLOUT

下面分析一下poll结构:

int poll(struct pollfd *fds,nfds_t nfds,int timeout);

fds:要监听的文件描述符集合,类型为struct pollfd的指针

nfds:监听的文件描述符的个数

timeout:超时值(毫秒),负数表示一直监听,一直到被监听的文件描述符集合中的任意一个设备发生了事件才会返回。

struct pollfd{
    int fd;
    short events;
    short revents;
};

struct pollfd成员:

fd:要监听的文件描述符

events:监听的事件

revents:返回的事件

常见的事件有POLLIN、POLLOUT分别表示设备可以无阻塞地读、写。

POLLRDNORM和POLLWRNORM是在_XOPEN_SOURCE宏被定义时所引入的事件,POLLRDNORM通常和POLLIN等价,POLLWRNORM和POLLOUT等价。

示例:

一个既要监听键盘又要监听串口的程序,当用户按下键盘的按键后,将键值转换成字符串后通过串口发送出去,当串口接收到数据后,在屏幕上显示。

#include <stdio.h>
#include <stdlib.h>
#include <string.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>

#include "vser.h"

int main(int argc, char *argv[])
{
	int ret;
	struct pollfd fds[2];
	char rbuf[32];
	char wbuf[32];
	struct input_event key;

	fds[0].fd = open("/dev/vser0", 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;
	fds[1].revents = 0;

	while (1) {
		ret = poll(fds, 2, -1);
		if (ret == -1)
			goto fail;

		if (fds[0].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, "%#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);
}

分析代码:

fds[0].fd = open("/dev/vser0", 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;
	fds[1].revents = 0;

分别以O_NONBLOCK 不阻塞的方式打开二个设备文件,并且初始化了revents = 0;

代码

ret = poll(fds, 2, -1);

调用poll进行监听,-1表示一直监听,如果监听的设备没有一个设备文件可读,那么poll将会一直阻塞,直到键盘或者串口任意一个设备能够读取才返回

代码:

if (fds[0].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, "%#x\n", key.code);
				ret = write(fds[0].fd, wbuf, strlen(wbuf) + 1);
				if (ret < 0)
					goto fail;
			}
		}

判断返回的事件,如果相关的事件发生,则读取数据ret = read(fds[1].fd, &key, sizeof(key));

如果从键盘读取数据了,则判断按键类型,将EV_KEY转换为字符串,通过串口发送。if (key.type == EV_KEY) { sprintf(wbuf, "%#x\n", key.code);

如果不是,是串口读取,则直接标准输出、
ret = write(fds[0].fd, wbuf, strlen(wbuf) + 1);

因为虚拟串口是内环回的,所以发给串口的数据都会返回来。

下面看看驱动如何实现:

unsigned int vser_poll (struct file *filp,struct poll_table_struct *p)
int mask=0;
poll_wait(filp, &vsdev.rwqh, p);
poll_wait (filp, &vsdev.wwqh, p);
if(!kfifo_is_empty(&vsfifo))
mask|= POLLIN | POLLRDNORM;
if(!kfifo_is_full (&vsfifo))
mask |= POLLOUT|POLLWRNORM;
return mask;
}
static struct file_operations vser_ops= {
.......
.poll = vser_poll,
};

vser_poll定义了一个poll接口函数,代码
.poll = vser_poll,让file_operations结构内部的poll指针指向vser_poll函数

看完下面的poll调用过程再来理解驱动中的poll 接口函数的实现就比较简单了,代码

poll_wait(filp, &vsdev.rwqh, p);
poll_wait (filp, &vsdev.wwqh, p);

将系统调用中构造的等待队列节点加入到相应的等待队列中
代码

if(!kfifo_is_empty(&vsfifo))
mask|= POLLIN | POLLRDNORM;
if(!kfifo_is_full (&vsfifo))
mask |= POLLOUT|POLLWRNORM;
return mask;

根据资源的情况返回设置mask的值并返回。
驱动中的poll接口函数是不会休眠的,休眠发生在poll系统调用上,这和前面的阻塞型I/O是不同的

下面是poll系统调用的过程:

poll系统调用在内核中对应的函数是sys_ poll, 该函数调用do_ sys_ poll 来完成具体的工作:在do_ sys_ poll 函数中有一个for循环,这个循环将会构造一个 poll_list 结构,其主要作用是把用户层传递过来的struct polfd复制到poll_list 中,并记录监听的文件个数(图4.1中有两个文件描述符3和4,关心的事件都是POLLIN);

之后调用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 接口函数( 图示中为xxx_ poll), 驱动中的poll接口函数将会调用poll_wait辅助函数,该函数又会调用之前在初始化poll_wqueues时指定的_ poll_ wait 函数,_ poll wait函数的主要作用是将刚才构造好的等待队列节点加入到驱动的等待队列中;接下来驱动的poll接口函数判断资源是否可用,并返回状态给mask;

如果内层循环所调用的每一个驱动的poll接口函数都返回,没有相应的事件发生,那么会调用poll schedule_timeout将poll系统调用休眠;当设备可用后,通常会产生一个中断(或由另外一个进程的某个操作使资源可用),在对应的中断处理函数中( 图示中为xxx_ isr) 将会调用wake_ up 函数(或其变体),将该驱动对应资源的等待队列上的进程唤醒,这时也会把刚才因为poll 系统调用所加入的节点出队,并调用相应的函数,即poll_ wake 函数,该函数负责唤醒因调用poll_ schedule_ timeout 函数而休眠的poll 系统调用,poll 系统调用唤醒后,回到外层的for 循环继续执行,这次执行再次遍历所有驱动中的poll接口函数后,会发现至少有一个关心的事件产生,于是将该事件记录在struct pollfd的revents成员中,然后跳出外层的for 循环,将内核的struct pollfd 复制至用户层,poll 系统调用最终返回,并返回有多少个被监听的文件有关心的事件产生。

最终测试结构流程:

这里0x 是你按下的键值转换为字符串 key.code
通过hexdump /dev/input/eventX方法查看键盘上z键的在Linux内核上传过来的值(以下简称:Linux键值),一看是0x2c(十进制:44)
键值从键盘到Linux内核传输过程分析
windows和linux键值表

第一个链接是自己键盘值转到Linux内核的
第二个是不同系统的键值

相关文章

微信公众号

最新文章

更多