Linux中的IO模型(下):poll、epoll模型

110 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

poll模型: 【poll采用了一个描述符事件结构的方式对描述符所关心的事件进行监控,相对于select不再需要创建多个事件集合的遍历方式了】

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

监控实现原理:

  1. 用户定义事件数组,对描述符可以添加用户关心的事件到结构体数组中,进行监控。 (POLLIN读事件、POLLOUT写事件)
struct pollfd{
	int fd;		//用户关心监控的 文件描述符
	short events;	//保存用户 关心 的事件
	short revents;	//保存当前 就绪 的事件
};
  • ==内核==:
  • a. poll实现监控的原理也是将数据拷贝到内核,然后进行轮询遍历监控,若有描述符就绪,则返回。其性能随着描述符的增多而下降。
  • b. 若有描述符就绪,则修改这个相应描述符事件结构中的实际就绪事件。
  1. poll也不会直接告诉用户哪一个描述符事件就绪,只是告诉用户有就绪事件,需要用户遍历查找。根据返回的revents判断哪一个事件就绪,查看实际发生的事件是否是自己关心的事件,来查找就绪,进而进行操作。
  • 优点:

a. 采用事件结构的方式对描述符进行监控,简化了多个事件集合的监控方式

b. 描述符的具体监控无上限

  • 缺点:

a. 不能跨平台。所以poll已经逐渐在历史舞台上淡出。

b. poll采用轮询遍历判断就绪,性能随着描述符增多而性能下降。

b. poll也不会告诉用户具体就绪的描述符,需要用户进行轮询判断。性能也会随着描述符增多而下降,代码复杂度较高。


epoll模型: Linux性能最高IO多路转接模型,也是采用事件结构的形式对描述符进行监控。

头文件<sys/epoll.h>

  • 就绪:

对于可读事件来说,有数据到来就是读就绪。接收缓冲区中的数据大小大于低水位标记(1Bit),就会触发可读事件。可读事件就绪。

对于可写事件来说,缓冲区有空闲空间就是写就绪。发送缓冲区中的空闲空间大小大于低水位标记(1Bit),就会触发可写事件。可写事件就绪

创建epoll接口:

int epoll_create(int size);		//创建epoll
  • size:自从Linux 2.6.8之后,size参数就被忽略了,但必须大于0。以后的版本以一种动态增长的方式进行变化。
  • 返回值:成功返回非负的文件描述符,当然文件描述符也是一个非负整数,他就是epoll的操作句柄。

功能:在内核中创建一个eventpoll结构体,其中包括{

struct eventpoll{
	rbr		//(红黑树)
	rdlist	//(双向链表)
};

接口:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	
//通过这个接口告诉操作系统:在哪一个epoll中,添加/移除/修改,哪一个描述符,所对应的监控事件
//通过参数解释:向内核中 epfd 对应的epoll结构,进行添加删除修改,一个 fd 描述符,对应关心的 event 事件
  • epfdepollcreate()接口返回的epoll操作句柄。
  • op:用户要进行的操作。
	EPOLL_CTL_ADD		向内核的eventpoll中添加要监控的事件结构
	EPOLL_CTL_DEL		从内核的eventpoll中移除要监控的事件结构
	EPOLL_CTL_MOD		修改内核中监控的事件结构
  • fd:用户所要监控的描述符
  • event:描述符对应所监控的事件
  • 返回值:成功返回0,失败返回-1

这里的event参数就是下面的epoll_event结构体:

typedef union epoll_data{
	void *ptr;		//给定一个指针就可以包含任意大小与类型的数据了
	
	int fd;			//事件结构中携带的这个描述符数据,当事件在内核中就绪时,OS会返回给用户事件结体
					//用户可以获得这个数据,若这个数据就是关心的描述符,就可以直接操作了
					//不再需要 遍历 了
					//但也有可能不是用户关心的描述符。
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

struct epoll_event{
	uint32_t events;		// Epoll events 用户对描述符关心的事件 其中包括:
							//可读 EPOLLIN
							//可写 EPOLLOUT
							//边缘触发 EPOLLET
							//水平触发 EPOLLLT
							
	epoll_data_t data;		// User data variable 事件对应的数据
};

epoll在内核中保存的事件结构不需要用户每次重新拷贝,只拷贝一次即可。

功能:向内核的eventpoll结构中添加 / 移除 / 修改所监控的事件结构。


接口:

int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);	
  • epfdepoll操作句柄
  • event:事件结构体数组,用于保存就绪的描述符对应事件
  • maxevent:告诉内核数组这个数组的大小,最多一次性获取就绪事件个数,防止event数组溢出。
  • timeout:超时时间,精度为毫秒。
  • 返回值: <0 :出错 == 0 :超时等待 >0:就绪的事件个数

流程:

  1. 接口告诉内核要开始对描述符进行监控了。(异步)
    1. 操作系统:对描述符进行监控,采用的是事件触发方式进行监控,为每一个要监控的描述符都定义了一个事件,并且对这个事件定义一个事件回调函数。ep_poll_callback();
    1. 这个事件回调函数做的事情为:将就绪的这个描述符所对应的epoll_event事件结构添加到双向链表rdlist
  1. epoll_wait并没有立即返回,而是每隔一段时间检查一次,内核中的eventpoll中双向链表是否为空。进而判断是否有描述符就绪。
  2. 若链表不为空,表示有描述符就绪,则epoll_wait即将返回,返回之前将就绪的描述符对应事件结构向用户态结构体数组拷贝一份(这个结构体数组就是epoll_wait的第二个参数) (链表中保存的都是就绪的描述符对应的事件结构)
  3. 将就绪的描述符对应事件拷贝一份到用户态,直接告诉用户有哪些描述符就绪,用户可以直接操作就绪的描述符。
struct eventpoll{
	rbr;	//红黑树:保存用户添加的事件结构结点。
	rdlist;	//双向链表
};

使用红黑树是因为它的特性:红黑树去重快,可以添加重复结点快速检测出。


  • 阻塞:为了完成功能发起调用,若当前不具备完成条件,则一直等待直到完成后调用返回。
  • 非阻塞:为了完成功能发起调用,若当前不具备完成条件,直接报错返回,通常需要循环处理。

阻塞与非阻塞区别:发起调用后是否==立即返回==。

  • 同步:为了完成功能发起调用,若当前不具备完成条件,则自己等待完成功能后返回。同步通常都是阻-塞的。
  • 异步:为了完成功能发起调用,但功能由别人完成。包含异步阻塞 / 异步非阻塞。
  1. 异步阻塞操作:操作由操作系统完成,但是自己等待别人(OS)完成条件。
  2. 异步非阻塞操作:操作由操作系统完成,但是不等待别人完成操作,立即返回。操作系统完成操作后通过信号通知进程。

同步与异步区别:功能是否由自己完成

同步与异步的优缺点对比: 同步流程控制简单,但效率较低。 异步流程控制较难,消耗资源少、效率较高。

Linux下的异步IO:aio(用于大量文件读写)