『Linux』I/O多路转接之epoll模型

在看epoll模型之前,我们先来看一下poll模型

poll函数

// 头文件:poll.h
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
*	参数:
*		fds:是一个poll函数监听的结构列表。每一个元素中,包含了三部分内容:
*			文件描述符、监听的事件集合、返回的事件集合;
*		nfds:表示fds数组的长度;
*		timeout:超时时间,单位ms。
*	返回值:
*		出错:返回值小于0;
*		poll函数等待超时:返回值等于0;
*		有监听的描述符就绪:返回值大于0。
*/

使用下面命令打开poll.h头文件来看一下struct pollfd的结构

[sss@aliyun ~]$ vim /usr/include/sys/poll.h

『Linux』I/O多路转接之epoll模型_第1张图片

  • fd:文件描述符
  • events:监听的事件集合
  • revents:返回的事件集合

events和revents的取值如下

事件 描述 是否可作为输入 是否可作为输出
POLLIN 数据(包括普通数据和优先数据)可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(Linux不支持)
POLLPRI 高优先级数据可读,比如TCP带外数据
POLLOUT 数据(包括普通数据和优先数据)可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作。由GNU引入
POLLERR 错误
POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNVAL 文件描述符没有打开

poll执行过程

  1. 用户定义事件数组对描述符可以添加关心的事件,进行监控
  2. poll实现监控的原理也是将数据拷贝到内核进行轮询遍历监控。性能随着描述符的增多下降
  3. 用户根据返回的revents判断哪一个事件就绪,只是告诉了用户有就绪事件,还是需要用户遍历查找

poll示例,使用poll监控标准输入

#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

int main(){
	struct pollfd poll_fd;
	// 监控0号文件描述符
	poll_fd.fd = 0;
	// 监听的事件
	poll_fd.events = POLLIN;

	while(1){
		std::cout << "> ";
		fflush(stdout);

		// 监控标准输入
		int ret = poll(&poll_fd, 1, 5000);
		if(ret < 0){
			// 监控出错
			perror("poll error");
			continue;
		}
		else if(ret == 0){
			// 监控超时
			std::cout << "poll timeout!\n";
		}

		// 就绪的事件
		if(poll_fd.revents == POLLIN){
			char buf[BUF_SIZE] = {0};
			// 读取标准输入
			read(0, buf, sizeof(buf) - 1);
			// 打印读取到的标准输入
			std::cout << "stdin: " << buf;
		}
	}

	return 0;
}

编译运行程序效果如下
『Linux』I/O多路转接之epoll模型_第2张图片


poll优缺点分析
优点

  • pollfd结构包含了要监视的event和发生的event,不要再使用select"参数-值"传递的方式。接口使用比select更方便
  • poll没有最大数量限制(但是数量过大后性能也是会下降)

缺点
当poll中监听的文件描述符增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核态
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符的数量的增长,其效率也会线性下降
  • 不能跨平台,只能在Linux下使用。

epoll

我们先使用man手册来看一下epoll
『Linux』I/O多路转接之epoll模型_第3张图片
从man手册,我们可以看到,epoll是为处理大量描述符而作了改进的poll

epoll相关接口

epoll有3个相关的系统调用

// 头文件:sys/epoll.h
// 功能:创建一个eventpoll结构体,用完之后必须调用close()关闭。
int epoll_create(int size);
/*
*	参数:
*		size:能监控的描述符上限,Linux2.6.8之后被忽略了,只要大于0就可以。
*	返回值:
*		文件描述符(非负整数),epoll的操作句柄。
*/
struct eventpoll{
	...
	// 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
	struct rb_root rbr;
	// 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
	...
}

// 头文件:sys/epoll.h
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
*	参数:
*		epfd:epoll的句柄;
*		op:动作,用三个宏表示;
*			EPOLL_CTL_ADD:注册新的fd到epfd中;
*			EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
*			EPOLL_CTL_DEL:从epfd中删除一个fd。
*		fd:需要监听的fd;
*		event:需要监听的事件。
*	返回值:
*		成功,返回0;失败,返回值小于0。
*/

我们使用下面命令,打开epoll.h看一下epoll_event的结构

[sss@aliyun ~]$ vim /usr/include/sys/epoll.h

『Linux』I/O多路转接之epoll模型_第4张图片
data是事件对应的数据描述符就绪后就会返回事件结构,用户可以获得这个数据
events可以是以下几个宏的集合

EPOLLIN 表示对应的文件描述符可以读(包括对端socket正常关闭)
EPOLLOUT 表示对应的文件描述符可以写
EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP 表示对应的文件描述符被挂断
EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的haul,需要再次把这个socket加入到epoll队列里。

// 头文件:sys/epoll.h
// 功能:开始监控
int epoll_wait(
	int epfd, struct epoll_event *events, 
	int maxevents, int timeout
);
/*
*	参数:
*		epfd:epoll操作句柄;
*		events:事件结构体数组,用于保存就绪的描述符对应事件;
*		maxevents:用于确定一次最多获取的就绪事件个数,防止events数组溢出;
*		timeout:超时等待时间,单位ms。
*	返回值:
*		出错,小于0;超时,等于0;就绪的事件个数,大于0。
*/

epoll工作原理

『Linux』I/O多路转接之epoll模型_第5张图片

  • 当进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体重有两个成员一棵红黑树(保存所有要监控的事件)一个双向链表(保存将要通过epoll_wait返回给用户的满足条件的事件)
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
  • 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入效率是log(N),其中N为树高)。
  • 所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体
  • 调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
  • 如果rdlist不为空,则将发生的事件复制到用户态,同时将事件数量返回给用户。这个操作的时间复杂度是O(1)

epitem结构体如下

struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

简单来说,epoll的使用过程如下

  1. 调用epoll_create创建一个epoll对象
  2. 调用epoll_ctl,将要监控的文件描述符进行注册
  3. 调用epoll_wait,等待文件描述符就绪

epoll的优点

  • 接口使用方便:虽然拆分成三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开
  • 数据拷贝轻量只在合适的时候调用EPOLL_CTL_ADD将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
  • 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作的时间复杂度是 O ( 1 ) O(1) O(1)。即使文件描述符数目很多,效率也不会受到影响。
  • 没有数量限制:文件描述符数目无上限。

epoll工作方式

水平触发(LT)

  • 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理。或者只处理一部分
  • 如过socket一段被写入2K的数据,我们只读取1K数据,缓冲区中还剩1K数据,在第二次调用epoll_wait时,epoll_wait仍然会立刻返回并通知socket读事件就绪。
  • 直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回。
  • 支持阻塞读写和非阻塞读写。

边缘触发(ET)

  • 当epoll检测到socket上事件就绪时,必须立刻处理
  • 如上面的例子,虽然只读了1K的数据,缓冲区还剩1K数据,在第二次调用epoll_wait的时候,epoll_wait不会再返回了
  • 也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
  • ET的性能比LT性能更高(epoll_wait返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
  • 只支持非阻塞读写。

你可能感兴趣的:(『Linux』,Linux,I/O多路转接,I/O多路复用,epoll模型)