目录
前置知识
前言
用户态与内核态
上下文切换
文件描述符
打开文件表
文件描述符与打开文件表
注意
Netcat软件的基本使用
Linux strace命令追踪系统调用链路
编辑
select
poll
epoll
代码举例
epoll原理图解举例
编辑
建立过程细节
Epoll函数解释
Epoll解决的问题
事件通知机制
水平触发和边缘触发
EPOLL ET触发模式的意义
总结
前置知识
阅读本文要求需要了解:
操作系统的内存空间的用户态、内核态。操作系统的非阻塞 nonblock支持CPU 上下文切换中断(硬中断、软中断)系统调用system call阻塞、非阻塞。同步、异步。文件描述符FD,文件句柄套接字socket相关内容,如"四元组":一个TCP连接的套接字对(socket pair)是一个定义该连接的两个端点的四元组:源IP地址、源TCP端口号、目的IP地址、目的TCP端口号。套接字对唯一标识一个网络上的TCP连接水平触发、边缘触发...
前言
I/O多路复用是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll都是IO多路复用的机制。但select,poll,epoll本质上都是同步I/O。而linux 内核支持通过socket中的fcntl函数
设置其fd使其变为non-blocking 非阻塞,这样通过调用socket的accept/read/write过程则为非阻塞的。
fcntl
函数提供了对文件描述符的各种控制操作:
#include <fcntl.h>//若成功则取决于cmd,否则返回-1int fcntl(int sockfd, int cmd, ...);
fcntl
函数支持的常用操作及其参数说明:
int flags = fcntl(sockfd, F_GETFL, 0);if (flags < 0) {printf("F_GETFL error\n");return -1;}//设置套接字成非阻塞if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {printf("F_SETFL error\n");return -1;}//关闭非阻塞flags &= ~O_NONBLOCK;if (fcntl(sockfd, F_SETFL, flags) == -1) {printf("F_SETFL error\n");return -1;}
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。select/poll/epoll
是I/O多路复用的三种实现。以select
为例,它的工作流程如下:
当用户进程调用select
,那么整个进程会被阻塞,同时,内核会监视所有select
负责的socket,当任何一个socket中的数据准备好了,select
就会返回,这个时候用户进程再调用read
操作,将数据从内核拷贝到用户进程。
相较于阻塞I/O,多路复用I/O并不显得有什么优势,事实上还要差一些,因为这里需要使用两个系统调用(select
和read
),而阻塞I/O只用了一个系统调用(read
)。但使用select
以后最大的优势是用户可以在一个线程内同时处理多个socket的I/O请求。而在阻塞I/O中,必须通过多线程/多进程的方式才能达到目的。(注意:如果处理的连接数不是很高的话,使用多路复用I/O不一定比使用阻塞I/O性能更好,可能延迟还更大。)
在多路复用模型中,对于每一个socket一般都设置成非阻塞(fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK)
),但是整个用户的进程其实是一直被select
阻塞的。
注:本文主要介绍Epoll 模式,其解决的select/poll模式的缺点、弊端自行了解。如select模式下socket文件描述符的限制、poll的轮询所有fd事件等。
用户态与内核态
后续补充
上下文切换
后续补充
文件描述符
Linux中,一切皆是文件,也就是意味着任何一个进程以及线程也都有一个唯一标识,这个标识称之为为文件描述符(file descriptor)。
举一些例子,例如建立Socket连接,会产生一个文件,文件的描述符标记为fd1,socket监听也会产生一个文件,也会产生文件描述符fd2,文件描述符在形式上是一个非负整数,是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
具体来说文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。
程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
打开文件表
每一个文件描述符会与一个打开文件相对应,同时不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开,也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符。这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。
进程级的文件描述符表
系统级的打开文件描述符表
文件系统的i-node表
进程级的描述符表的每一条目记录了单个文件描述符的相关信息。
控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志)
对打开文件句柄的引用
内核对所有打开的文件的文件维护有一个系统级的描述符表格(open file description table)。有时,也称之为打开文件表(open file table),并将表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
打开文件时所使用的状态标识(即,open()的flags参数)
文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
与信号驱动相关的设置
对该文件i-node对象的引用
文件类型(例如:常规文件、套接字或FIFO)和访问权限
一个指针,指向该文件所持有的锁列表
文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
文件描述符与打开文件表
注意
由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件
两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由调用read()、write()或lseek()所致),那么从另一个描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。
要获取和修改打开的文件标志(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。
文件描述符标志(即,close-on-exec)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符
Netcat软件的基本使用
Netcat(简写nc)是一个强大的网络命令工具,能够在linux中执行与TCP、UDP相关的操作,例如端口扫描,端口重定向、端口监听甚至远程连接。
在这里,我们使用 nc 来模拟一台接收message的服务器,和一台发送message的客户端。
1、安装 nc 软件
sudo yum install -y nc
2、使用 nc 创建一台监听9999端口的服务器
nc -l -p 9999# -l表示listening,监听
启动成功后 nc 进行阻塞
3、新建一个bash console控制台,使用 nc 创建一个发送message的客户端
nc localhost 9999
在控制台上输入要发送的信息,查看服务端是否接收到。
4、查看上面的nc进程中的文件描述符
可以看到这个进程下有一个socket,这就是nc的客户端和服务端之间创建的一个socket
Linux strace命令追踪系统调用链路
strace软件说明:它是一个可以追踪系统调用和信号的软件,通过它我们来了解BIO
环境说明:这里演示的都是基于老版本的linux,因为新版本的linux都不用BIO了,演示不出来
1、使用strace来追踪系统调用
sudo yum install -y strace # 安装strace软件mkdir ~/strace # 新建一个目录,存放追踪的信息cd ~/strace # 进入到这个目录strace -ff -o out nc -l -p 8080 # 使用strace追踪后边的命令进行的系统调用# -ff 表示追踪后面命令创建的进程及子进程的所有系统调用,# 并根据进程id号分开输出到文件# -o 表示追踪到的信息输出到指定名称的文件,这里是out
2、查看服务端创建的系统调用
在上一步进入的目录下,出现了一个out.pid
文件里的内容都是nc -l -p 8080
这个命令执行后的系统调用过程,使用vim
命令来查看
vim out.92459 # nc进程id为92459
如上图可以看到,这里accept()
方法进行了阻塞,它要等待其他socket对它进行连接
3、客户端连接,查看系统调用
退出vim
,使用tail
来进行查看
tail -f out.92459
-f 参数:当文件有追加的内容,可以实时地打印在控制台,这样就能很方便来查看客户端连接后进行的系统调用。
建立bash console客户端连接:
nc localhost 8080
查看系统调用
这里客户端连接后,accept() 方法获取到客户端连接并返回文件描述符4,这个4就是服务端新创建的socket,用于和这个客户端进行通信之后使用多路复用器poll来监听服务端上文件描述符4和0,0是标准输入文件描述符,哪个有事件发生就读取哪个文件描述符,如果都没有事件发生就进行堵塞
4、客户端发送message,查看系统调用
如在bash console客户端在已执行nc localhost 8080建立连接后,发送消息 如发送"hello".
客户端向服务端发送数据,服务端就能从socket中监听到有事件发生,就能进行相应的处理,处理完继续堵塞,等待下一个事件发生。
5、服务端发送数据到客户端,查看系统调用
服务端发数据,肯定从键盘输入,也就是标准输入0,从0中读取到数据发送给socket 4
select
select函数定义:
int select(int maxfd1,// 最大文件描述符个数,传输的时候需要+1fd_set *readset,// 读描述符集合fd_set *writeset,// 写描述符集合fd_set *exceptset,// 异常描述符集合const struct timeval *timeout);// 超时时间// 现在很多Liunx版本使用pselect函数,最新版本(5.6.2)的select已经弃用// 其定义如下int pselect(int maxfd1, // 最大文件描述符个数,传输的时候需要+1fd_set *readset,// 读描述符集合fd_set *writeset,// 写描述符集合fd_set *exceptset,// 异常描述符集合const struct timespec *timeout, // 超时时间const struct sigset_t *sigmask); // 信号掩码指针
poll
poll技术与select技术实现逻辑基本一致,重要区别在于其使用链表的方式存储描述符fd,不受数组大小影响,对此,现对poll技术进行分析如下:
// poll已经被弃用int poll(struct pollfd *fds, // fd的文件集合改成自定义结构体,不再是数组的方式,不受限于FD_SIZEunsigned long nfds,// 最大描述符个数int timeout);// 超时时间struct pollfd {int fd;// fd索引值short events;// 输入事件short revents;// 结果输出事件};// 当前查看的linux版本(5.6.2)使用ppoll方式,与pselect差不多,其他细节不多关注int ppoll(struct pollfd *fds, // fd的文件集合改成自定义结构体,不再是数组的方式,不受限于FD_SIZEunsigned long nfds, // 最大描述符个数struct timespec timeout, // 超时时间,与pselect一样const struct sigset_t sigmask, // 信号指针掩码struct size_t sigsetsize); // 信号大小
poll技术使用链表结构的方式来存储fdset的集合,相比select而言,链表不受限于FD_SIZE的个数限制,但是对于select存在的性能并没有解决,即一个是存在大内存数据拷贝的问题,一个是轮询遍历整个等待队列的每个节点并逐个通过回调函数来实现读取任务的唤醒。
epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现。IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
epoll有两种工作方式, LT-水平触发 和ET-边缘触发(默认工作方式),主要区别是:
LT,内核通知你fd是否就绪,如果没有处理,则会持续通知。而ET,内核只通知一次。
什么是I/O?
输入输出(input/output)的对象可以是文件(file), 网络(socket),进程之间的管道(pipe)。在linux系统中,都用文件描述符(fd)来表示。
什么是事件?
IO中涉及到的行为,建立连接、读操作、写操作等抽象出一个概念,就是事件,在jdk中用类SelectionKey.java来表示,例如:可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件(可读:内核缓冲区非空,有数据可以读取);可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件(可写:内核缓冲区不满,有空闲空间可以写入)。
什么是通知机制?
通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。
代码举例
epoll原理图解举例
上图流程解释:
JAVA默认会调用Epoll作为多路复用选择器,可以设置参数调整。
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
当JAVA端调用Selector后,创建一个套接字Socket,描述符是如图中的fd4,绑定顿口bind开始监听。这时候在epoll里面会调用单个函数(后面会详细解释),首先调用epoll_create(int size)创建红黑树空间,如图中的fd6,这个空间是需要被监听时间的数目然后执行epoll_ctl,将空间fd6与Socket描述符fd4关联起来,当有连接有数据时,记录IO的编号,并将这个编号存储在一个链表中当epoll调用epoll_wait时,将这个记录有事件IO的链表返回,之后调用程序只需要根据记录的信息读写产生事件的IO即可。
建立过程细节
1.通过调用epoll_create,在epoll文件系统建立了个file节点,并开辟epoll自己的内核高速cache区,建立红黑树,分配好想要的size的内存对象,建立一个list链表,用于存储准备就绪的事件。即首先epoll_create创建epoll实例,它会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄。其实就是在内核开辟一块内存空间,所有与服务器连接的socket都会放到这块空间中,这些socket以红黑树的形式存在,同时还会有一块空间存放就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;
2.通过调用epoll_ctl,把要监听的socket放到对应的红黑树上,给内核中断处理程序注册一个回调函数,通知内核,如果这个句柄的数据到了,就把它放到就绪列表。即epoll_ctl添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。
3.通过调用 epoll_wait,观察就绪列表里面有没有数据,epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。
Epoll函数解释
epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数(给出最大监听的fd+1的值)。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
size参数告诉内核这个epoll对象处理的事件大致数量,而不是能够处理的最大数量。在现在的linux版本中,这个size函数已经被废弃(但是size不要传0,会报invalid argument错误)。内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄,之后针对该epoll的操作需要通过该句柄来标识该epoll对象。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
将被监听的描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改返回值:0表示成功,-1表示错误,根据errno错误码判断错误类型
第一个参数epfd
参数是epoll_create
函数的返回值,第二个参数op表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);EPOLLOUT:表示对应的文件描述符可以写;EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中events: 用来记录被触发的events,其大小应该和maxevents一致maxevents: 返回的events的最大个数参数timeout描述在函数调用中阻塞时间上限,单位是ms:
timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。
Epoll解决的问题
为了解决select&poll技术存在的两个性能问题,对于大内存数据拷贝问题。
epoll通过epoll_create函数创建epoll空间(相当于一个容器管理),在内核中只存储一份数据来维护N个socket事件的变化;
通过epoll_ctl函数来实现对socket事件的增删改操作,并且在内核底层通过利用虚拟内存的管理方式保证用户空间与内核空间对该内存是具备可见性,直接通过指针引用的方式进行操作,避免了大内存数据的拷贝导致的空间切换性能问题,
对于轮询等待事件则是通过epoll_wait的方式来实现对socket事件的监听,将不断轮询等待高频事件wait与低频socket注册事件两个操作分离开,同时会对监听就绪的socket事件添加到就绪队列中,也就保证唤醒轮询的事件都是具备可读的。
现对epoll函数参数分析如下:
// 创建保存epoll文件描述符的空间,该空间也称为“epoll例程”int epoll_create(int size); // 使用链表,现在已经弃用int epoll_create(int flag); // 使用红黑树的数据结构// epoll注册/修改/删除 fd的操作long epoll_ctl(int epfd, // 上述epoll空间的fd索引值int op,// 操作识别,EPOLL_CTL_ADD | EPOLL_CTL_MOD | EPOLL_CTL_DELint fd,// 注册的fdstruct epoll_event *event);// epoll监听事件的变化struct epoll_event {__poll_t events;__u64 data;} EPOLL_PACKED;// epoll等待,与select/poll的逻辑一致epoll_wait(int epfd, // epoll空间struct epoll_event *events, // epoll监听事件的变化int maxevents, // epoll可以保存的最大事件数int timeout);// 超时时间
我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。
而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
事件通知机制
1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中
2、网卡向cpu发起中断,让cpu先处理网卡的事
3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中。
水平触发和边缘触发
Level_triggered(LT水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(ET边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
EPOLL ET触发模式的意义
若用EPOLL LT
,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用epoll_wait
都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。 而采用EPOLLET
,当被监控的文件描述符上有可读写事件发生时,epoll_wait
会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait
时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
总结
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
(3)无论是哪种多路复用器,知道IO状态后都需要自己去读取数据,所以都是同步而非异步模型。