I/O多路复用详解


I/O分类解释

通常我们将阻塞式的文件描述符称为阻塞式I/O,而非阻塞即为非阻塞I/O。
而二者的主要区别在于:

针对阻塞I/O的系统调用可能因为无法立即完成而被操作系统挂起,直至等待的事件发生。
常见的阻塞式I/O有我们常用的Socket编程中的:accept,send,recv和connect等。

针对非阻塞I/O的系统调用则总是立即返回,而不管事件是否发生,如果事件没有发生,一般情况下返回值为-1。
上述阻塞式I/O我们同样可以在Socket系统调用中进行参数设置,实其变成非阻塞模式。

从上述解释可以看到,通常情况下使用阻塞式I/O会影响程序运行效率,因为进程或线程会长时间等待事件发生,I/O可用。
但是使用非阻塞I/O虽然可以立即返回,但是我们并不知道什么时刻事件发生或I/O可用,此时便可以和我们I/O通知机制,比如I/O复用和SIGIO信号同时使用,便可以提高我们程序的运行效率。

(同时还有异步I/O模型,其与上述主要区别可以看作是由内核完成I/O操作,本文不作过多介绍)


I/O复用

I/O复用便是我们最常使用的I/O通知机制。

它主要指的是,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。

但我们需要知道的是,I/O复用函数本身是阻塞的,它能提高应用程序效率的关键是,它能同时检测监听多个I/O事件,及时通知应用程序进行操作。
而我们平时最常使用复用函数的便是Linux下的select,poll,epoll(Windows下没有epoll函数)。


文件就绪条件

在上文中我们多次提到文件描述符就绪可读可写,下面对这些情况进行详细说明(以网络Socket编程为例):

可读:
1.Socket内核接收缓存区中的字节数大于等于其低水位标准(SO_RCVLOWAT),可以简单理解为,接收数量大于某一标准。
2.socket通信对方关闭连接。
3.listen下由新的连接请求。
4.socket上有未处理错误。

可写:
1.内核发送缓存区可用字节数大于等于其低水位标准(SO_SNDLOWAT),可以简单理解为可用字节数大于某一标准。
2.写操作被关闭。
3.使用非阻塞connect连接成功或失败。
4.有未处理错误。

异常情况:
主要是接收带外(紧急数据)数据。


select系统调用

用途

在一段指定时间内,监听用户感兴趣的文件描述符上的可读,可写和异常等事件。
主要监听方式:轮询

函数原型

#include <sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);

nds:指定被监听的文件描述符的总数。
readfds,writefds,exceptfds:分别指向可读,可写,异常事件对应的文件描述符集合。
fd_set:结构体包含一个整数数组,该数组每个元素的每一位标记一个文件描述符,其能容纳的文件描述符数量是有限的。
timeout:设置select函数的超时时间。若给其传递NULL,则select一直阻塞直到某个描述符就绪。

返回值

select成功时返回(可读,可写,异常)文件描述符总数。
若在timeout时间内无就绪文件描述符,则返回0;


poll系统调用

用途

与select类似,都是轮询,测试是否有就绪者。

函数原型

#include <poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout)

fds:pollfd结构类型的数组,原型如下:

struct pollfd
{
int fd;            //文件描述符
short events;      //注册的事件
short revents;     //实际发生的事件,由内核填充
};

poll支持的事件类型如下:
类型类型
nfds:指定被监听事件集合fds的大小
timeout:与select类似。

返回值

与select类似。


epoll系统调用

用途

首先epoll是linux系统特有的I/O复用函数,其次其使用与select,poll有较大区别,epoll需要一组函数来完成任务,而不是select与poll的一个,并且其需要一个额外的文件描述符,来标识内核中的事件表,以便将用户关心的文件描述符上的事件放在其中。

函数原型

创建
#include <sys/epoll.h>
int epoll_create(int size)

这是我们epoll使用的第一个函数,size提示内核事件表大小,而返回的文件描述符将用作其他所有epoll系统调用的第一个参数,即在用途中提到的额外文件描述符

修改
#include <sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)

此函数用来操作epoll内核事件表。

epfd:即上文的create返回值。
op:指定操作类型,如修改删除,注册事件。
event:指定事件,其结构如下:

struct epoll_event{
_uint32_t events;   //epoll事件
epoll_data_t data;  //用户数据
}

events:描述事件类型,其类型与poll类似,但是名称是在poll类型前加’E’。
data结构如下:

typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;

值得注意的是其为一个联合体,四个成员中使用最多是fd,指定事件所从属的目标文件描述符。

调用
#include <sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout)

该函数成功时,返回就绪文件描述符的个数。
epfd:即create返回值。
events:存放就绪事件,从epfd复制。
maxevents:监听多少事件。
timeout:与poll类似。

与poll区别

下面将用一个简单例子体现出来:

	int ret = poll(fds, MAX_EVENTS_NUMBER, -1);
	for (int i = 0; i < MAX_EVENTS_NUMBER; ++i)
	{
		if (fds[i].revents & POLLIN)
		{
			int sockfd = fds[i].fd;
			//之后进行一系列处理
		}
	}

	int ret = epoll_wait(epollfd, events, MAX_EVENTS_NUMBER, -1);
	for (int i = 0; i < ret; ++i)
	{
		int sockfd = events[i].data.fd;
		//之后进行一系列处理
	}

从这个简单的伪代码片可以看到:
poll调用结束之后,我们需要在此对fds事件表中的事件进行轮询
epoll调用结束之后,events中即为已经就绪的文件描述符我们直接进行处理即可。

两种模式

epoll有两种工作模式,分别为LT(电平触发)模式ET(边沿触发)模式
LT是epoll默认工作模式,ET为其高效工作模式。

二者的主要区别在于:

对于采用LT工作模式的文件描述符,当epoll检测到其上有事件发生时,应用程序可以不立即处理该事件。下次调用时,epoll还会向应用程序通告此事件,直至该事件被处理。
对于ET工作模式的文件描述符,epoll检测到后,应用程序必须立即处理,此后的调用不会再向应用程序通知此事件
由此可见,ET模式降低了事件被重复触发通知的次数,因此效率比LT更高。

而事件采用LT或ET模式,可以由epoll_events中的events标志位进行改变。

三者区别/总结

总结如下图:
总结I/O 复用还有多种高级应用方法,若有时间,之后会继续进行更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值