关于I/O多路复用技术以及这三者是什么,这里我就不做过多的讲解了,如有不懂是什么的读者,请参考:【Linux】select,poll和epoll_linux epoll poll select-CSDN博客
一、select
select函数的作用是检测一组socket中某个或某几个是否有 “ 事件 ” 就绪,这里的“事件”一般分为如下三类:
- 读事件就绪
- socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 recv 或 read 函数可以无阻塞的读该文件描述符, 并且返回值大于 0;
- TCP 连接的对端关闭连接,此时调用 recv 或 read 函数对该 socket 读返回 0 值 ;
- 侦听 socket 上有新的连接请求;
- socket 上有未处理的错误。
- 写事件就绪
- socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于 0;
- socket 的写操作被关闭(调用了 close 或 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
- socket 使⽤非阻塞 connect 连接成功或失败时。
- 异常事件就绪
socket上收到外带数据。
函数签名如下:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
参数说明:
参数 nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数检测事件的 fd 中的最大 fd 值加 1。
参数 readfds,需要监听可读事件的 fd 集合。
参数 writefds,需要监听可写事件的 fd 集合。
参数 exceptfds,需要监听异常事件 fd 集合。
readfds、writefds 和 exceptfds 类型都是 fd_set,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中:
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
/* Some versions of <linux/posix_types.h> define this macros. */
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FD_ELT(d) ((d) / __NFDBITS)
#define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
// 在我的centOS 7.0 系统中的值:
// __FD_SETSIZE = 1024
//__NFDBITS = 64
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE
我们假设未定义宏__USE_XOPEN,将上面的代码整理一下:
typedef struct
{
long int __fds_bits[16];
} fd_set;
将一个 fd 添加到 fd_set 这个集合中需要使用 FD_SET 宏,其定义如下:
void FD_SET(int fd, fd_set *set);
fd_set 数组的定义是:
typedef struct
{
long int __fds_bits[16]; //可以看成是128 bit的数组
} fd_set;
__fds_bits 是 long int 型数组,long int 占 8 个字节,每个字节 8 bit,每个 bit 对应一个 fd 的事件状态,0 表示无事件,1 表示有事件,数组长度是 16,因此一共可以表示 8 * 8 * 16 = 1024 个 fd 的状态, 这是 select 函数支持的最大 fd 数量。
__FD_SET(d, set) 的实际操作是:
__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)
__FD_ELT (d) 确定的是某个 fd(这里用 d 表示)在数组 __fds_bits 中的下标位置,计算方法是计算 fd 与 __NFDBITS 求商(__NFDBITS 值是 64 (8 * 8))。
__FD_MASK 计算对应 fd 在对应 bit 位置上的值,计算方法是先与 __NFDBITS 求余得到 n,然后执行 1 << n,即左移 n 位,然后将值设置到对应的 bit 上( |= 操作)。
举个例子,假设现在 fd 的 值是 57,那么:
FD_SET(57, set)
实际调用:
__fds_bits[__FD_ELT(57)] |= __FD_MASK(57)
即
__fds_bits[57 / 64] |= (1 << (57 % 64))
即
__fds_bits[0] |= (0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000)(二进制)
即数组下标为0的元素(64 bit)中第 57 bit被置为 1(0~63位)
Linux 系统上往 fd_set 集合中添加新的 fd 时决定这个 fd 在 __fds_bits 数组的位置的实现使用的是位图法(bitmap),Windows 系统上添加 fd 至 fd_set 的实现则是依次从数组第 0 个位置开始往后递增。
也就是说 FD_SET 宏的原理本质上是向一个有 1024 个连续 bit (共计 64 个字节)的内存的某个 bit 上设置一个标志。
同理,如果我们需要从 fd_set 上删除一个 fd,即对应的 bit 位置 0,我们可以使用 FD_CLR,其定义如下:
void FD_CLR(int fd, fd_set *set);
如果,我们需要将 fd_set 中所有的 fd 都清掉,即将所有 bit 位置 0,可以使用宏 FD_ZERO:
void FD_ZERO(fd_set *set);
当 select 函数返回时, 我们使用 FD_ISSET 宏来判断某个 fd 是否有我们关心的事件,FD_ISSET 宏的定义如下:
int FD_ISSET(int fd, fd_set *set);
这就是与 select 函数相关的几个宏的原理。示意图如下:
参数 timeout,超时时间,即在这个参数设定的时间内检测这些 fd 的事件,超过这个时间后 select 函数将立即返回。这是一个 timeval 类型结构体,其定义如下:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
/**
* select函数示例,server端, select_server.cpp
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <sys/time.h>
#include <vector>
#include <errno.h>
//自定义代表无效fd的值
#define INVALID_FD -1
int main(int argc, char* argv[])
{
//创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == INVALID_FD)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
//启动侦听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
//存储客户端socket的数组
std::vector<int> clientfds;
int maxfd;
while (true)
{
fd_set readset;
FD_ZERO(&readset);
//将侦听socket加入到待检测的可读事件中去
FD_SET(listenfd, &readset);
maxfd = listenfd;
//将客户端fd加入到待检测的可读事件中去
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)
{
if (clientfds[i] != INVALID_FD)
{
FD_SET(clientfds[i], &readset);
if (maxfd < clientfds[i])
maxfd = clientfds[i];
}
}
timeval tm;
tm.tv_sec = 1;
tm.tv_usec = 0;
//暂且只检测可读事件,不检测可写和异常事件
int ret = select(maxfd + 1, &readset, NULL, NULL, &tm);
if (ret == -1)
{
//出错,退出程序。
if (errno != EINTR)
break;
}
else if (ret == 0)
{
//select 函数超时,下次继续
continue;
}
else
{
//检测到某个socket有事件
if (FD_ISSET(listenfd, &readset))
{
//侦听socket的可读事件,则表明有新的连接到来
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//4. 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd == INVALID_FD)
{
//接受连接出错,退出程序
break;
}
//只接受连接,不调用recv收取任何数据
std:: cout << "accept a client connection, fd: " << clientfd << std::endl;
clientfds.push_back(clientfd);
}
else
{
//假设对端发来的数据长度不超过63个字符
char recvbuf[64];
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)
{
if (clientfds[i] != INVALID_FD && FD_ISSET(clientfds[i], &readset))
{
memset(recvbuf, 0, sizeof(recvbuf));
//非侦听socket,则接收数据
int length = recv(clientfds[i], recvbuf, 64, 0);
if (length <= 0)
{
//收取数据出错了
std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl;
close(clientfds[i]);
//不直接删除该元素,将该位置的元素置位INVALID_FD
clientfds[i] = INVALID_FD;
continue;
}
std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl;
}
}
}
}
}
//关闭所有客户端socket
int clientfdslength = clientfds.size();
for (int i = 0; i < clientfdslength; ++i)
{
if (clientfds[i] != INVALID_FD)
{
close(clientfds[i]);
}
}
//关闭侦听socket
close(listenfd);
return 0;
}
关于上述代码在实际开发中有几个需要注意的事项,这里逐一来说明一下:
-
正如上文介绍 select 函数相关宏所介绍的,select 函数调用前后可能会修改 readfds、writefds 和 exceptfds 这三个集合中的内容,所以如果读者想下次调用 select 函数时复用这些 fd_set 变量,记得在下次调用前使用 FD_ZERO 将 fd_set 清零,然后再调用 FD_SET 将需要检测事件的 fd 重新添加到 fd_set。
-
select 函数也会修改 timeval 结构体的值,这也要求我们如果想复用这个变量,必须给 timeval 变量重新设置值。
上面的例子中在调用 select 函数一次之后,变量 tv 的值也被修改了。具体修改成多少,得看系统的表现。当然这种特性却不是跨平台的,在 Linux 系统中是这样的,而在其他操作系统上却不一定是这样(Windows 上就不会修改这个结构体的值),这点在 Linux man 手册 select 函数的说明中说的很清楚。
-
select 函数的 timeval 结构体的 tv_sec 和 tv_usec 如果两个值设置为 0,即检测事件总时间设置为 0,其行为是 select 会检测一下相关集合中的 fd,如果没有需要的事件,则立即返回。
Linux select 函数的缺点也是显而易见的:
- 每次调用 select 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,同时每次调用 select 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大;
- 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下;
- select 函数在每次调用之前都要对传入参数进行重新设定,这样做也比较麻烦。
在 Linux 平台上,select 函数的实现是利用 poll 函数的,有兴趣的读者可以查找一下相关的资料来阅读一下。关于 poll 函数的使用,接下来我们会介绍。
二、poll函数用法
poll 函数用于检测一组文件描述符(File Descriptor, fd)上的可读可写和出错事件,其函数签名如下:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数解释:
fds:指向一个结构体数组的首个元素的指针,每个数组元素都是一个 struct pollfd 结构,用于指定检测某个给定的 fd 的条件;
nfds:参数 fds 结构体数组的长度,nfds_t 本质上是 unsigned long int,其定义如下:
typedef unsigned long int nfds_t;
timeout:表示 poll 函数的超时时间,单位为毫秒。
struct pollfd 结构体定义如下:
struct pollfd {
int fd; /* 待检测事件的 fd */
short events; /* 关心的事件组合 */
short revents; /* 检测后的得到的事件类型 */
};
struct pollfd的 events 字段是由开发者来设置,告诉内核我们关注什么事件,而 revents 字段是 poll 函数返回时内核设置的,用以说明该 fd 发生了什么事件。events 和 revents 一般有如下取值:
事件宏 | 事件描述 | 是否可以作为输入(events) | 是否可以作为输出(revents) |
---|---|---|---|
POLLIN | 数据可读(包括普通数据&优先数据) | 是 | 是 |
POLLOUT | 数据可写(普通数据&优先数据) | 是 | 是 |
POLLRDNORM | 等同于 POLLIN | 是 | 是 |
POLLRDBAND | 优先级带数据可读(一般用于 Linux 系统) | 是 | 是 |
POLLPRI | 高优先级数据可读,例如 TCP 带外数据 | 是 | 是 |
POLLWRNORM | 等同于 POLLOUT | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对端关闭,或者关闭了写操作,由 GNU 引入 | 是 | 是 |
POPPHUP | 挂起 | 否 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
poll 检测一组 fd 上的可读可写和出错事件的概念与前文介绍 select 的事件含义一样,这里就不再赘述。poll 与 select 相比具有如下优点:
- poll 不要求开发者计算最大文件描述符加 1 的大小;
- 相比于 select,poll 在处理大数目的文件描述符的时候速度更快;
- poll 没有最大连接数的限制,原因是它是基于链表来存储的;
- 在调用 poll 函数时,只需要对参数进行一次设置就好了。
/**
* 演示 poll 函数的用法,poll_server.cpp
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>
//无效fd标记
#define INVALID_FD -1
int main(int argc, char *argv[])
{
//创建一个侦听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == INVALID_FD)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//将侦听socket设置为非阻塞的
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
{
close(listenfd);
std::cout << "set listenfd to nonblock error." << std::endl;
return -1;
}
//复用地址和端口号
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *) &on, sizeof(on));
//初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *) &bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
//启动侦听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
std::vector<pollfd> fds;
pollfd listen_fd_info;
listen_fd_info.fd = listenfd;
listen_fd_info.events = POLLIN;
listen_fd_info.revents = 0;
fds.push_back(listen_fd_info);
//是否存在无效的fd标志
bool exist_invalid_fd;
int n;
while (true)
{
exist_invalid_fd = false;
n = poll(&fds[0], fds.size(), 1000);
if (n < 0)
{
//被信号中断
if (errno == EINTR)
continue;
//出错,退出
break;
}
else if (n == 0)
{
//超时,继续
continue;
}
for (size_t i = 0; i < fds.size(); ++i)
{
// 事件可读
if (fds[i].revents & POLLIN)
{
if (fds[i].fd == listenfd)
{
//侦听socket,接受新连接
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//接受客户端连接, 并加入到fds集合中
int clientfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clientaddrlen);
if (clientfd != -1)
{
//将客户端socket设置为非阻塞的
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set clientfd to nonblock error." << std::endl;
}
else
{
struct pollfd client_fd_info;
client_fd_info.fd = clientfd;
client_fd_info.events = POLLIN;
client_fd_info.revents = 0;
fds.push_back(client_fd_info);
std::cout << "new client accepted, clientfd: " << clientfd << std::endl;
}
}
}
else
{
//普通clientfd,收取数据
char buf[64] = {0};
int m = recv(fds[i].fd, buf, 64, 0);
if (m <= 0)
{
if (errno != EINTR && errno != EWOULDBLOCK)
{
//出错或对端关闭了连接,关闭对应的clientfd,并设置无效标志位
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++iter)
{
if (iter->fd == fds[i].fd)
{
std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl;
close(fds[i].fd);
iter->fd = INVALID_FD;
exist_invalid_fd = true;
break;
}
}
}
}
else
{
std::cout << "recv from client: " << buf << ", clientfd: " << fds[i].fd << std::endl;
}
}
} else if (fds[i].revents & POLLERR)
{
//TODO: 暂且不处理
}
}// end outer-for-loop
if (exist_invalid_fd)
{
//统一清理无效的fd
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end();)
{
if (iter->fd == INVALID_FD)
iter = fds.erase(iter);
else
++iter;
}
}
}// end while-loop
//关闭所有socket
for (std::vector<pollfd>::iterator iter = fds.begin(); iter != fds.end(); ++iter)
close(iter->fd);
return 0;
}
通过上面的示例代码,我们也能看出 poll 函数存在的一些缺点:
- 在调用 poll 函数时,不管有没有有意义,大量的 fd 的数组被整体在用户态和内核地址空间之间复制;
- 与 select 函数一样,poll 函数返回后,需要遍历 fd 集合来获取就绪的 fd,这样会使性能下降;
- 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
三、epoll模型
综合 select 和 poll 的一些优缺点,Linux 从内核 2.6 版本开始引入了更高效的 epoll 模型,本节我们来详细介绍 epoll 模型。
要想使用 epoll 模型,必须先需要创建一个 epollfd,这需要使用 epoll_create 函数去创建:
#include <sys/epoll.h>
int epoll_create(int size);
参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。
有了 epollfd 之后,我们需要将我们需要检测事件的其他 fd 绑定到这个 epollfd 上,或者修改一个已经绑定上去的 fd 的事件类型,或者在不需要时将 fd 从 epollfd 上解绑,这都可以使用 epoll_ctl 函数:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数 epfd 即上文提到的 epollfd;
参数 op,操作类型,取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分别表示向 epollfd 上添加、修改和移除一个其他 fd,当取值是 EPOLL_CTL_DEL,第四个参数 event 忽略不计,可以设置为 NULL;
参数 fd,即需要被操作的 fd;
参数 event,这是一个 epoll_event 结构体的地址,epoll_event 结构体定义如下:
struct epoll_event { uint32_t events; /* 需要检测的 fd 事件,取值与 poll 函数一样 */ epoll_data_t data; /* 用户自定义数据 */ };
函数返回值:epoll_ctl 调用成功返回 0,调用失败返回 -1,我们可以通过 errno 错误码获取具体的错误原因。
epoll_event 结构体的 data 字段的类型是 epoll_data_t,我们可以利用这个字段设置一个自己的自定义数据,它本质上是一个 Union 对象,在 64 位操作系统中其大小是 8 字节,其定义如下:
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
创建了 epollfd,设置好某个 fd 上需要检测事件并将该 fd 绑定到 epollfd 上去后,我们就可以调用 epoll_wait 检测事件了,epoll_wait 函数签名如下:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数的形式和 poll 函数很类似,参数 events 是一个 epoll_event 结构数组的首地址,这是一个输出参数,函数调用成功后,events 中存放的是与就绪事件相关 epoll_event 结构体数组;参数 maxevents 是数组元素的个数;timeout 是超时时间,单位是毫秒,如果设置为 0,epoll_wait 会立即返回。
当 epoll_wait 调用成功会返回有事件的 fd 数目;如果返回 0 表示超时;调用失败返回 -1。
epoll_wait 使用示例如下:
while (true)
{
epoll_event epoll_events[1024];
int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0)
{
//被信号中断
if (errno == EINTR)
continue;
//出错,退出
break;
}
else if (n == 0)
{
//超时,继续
continue;
}
for (size_t i = 0; i < n; ++i)
{
if (epoll_events[i].events & EPOLLIN)
{
// 处理可读事件
}
else if (epoll_events[i].events & EPOLLOUT)
{
// 处理可写事件
}
else if (epoll_events[i].events & EPOLLERR)
{
//处理出错事件
}
}
}
通过前面介绍 poll 与 epoll_wait 函数的介绍,我们可以发现:
epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);而 poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。
当然,这并不意味着,poll 函数的效率不如 epoll_wait,一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,epoll_wait 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。
四、LT模式和ET模式
与 poll 的事件宏相比,epoll 新增了一个事件宏 EPOLLET,这就是所谓的边缘触发模式(Edge Trigger,ET),而默认的模式我们称为水平触发模式(Level Trigger,LT)。这两种模式的区别在于:
- 对于水平触发模式,一个事件只要有,就会一直触发;
- 对于边缘触发模式,只有一个事件从无到有才会触发。
这两个词汇来自电学术语,你可以将 fd 上有数据认为是高电平,没有数据认为是低电平,将 fd 可写认为是高电平,fd 不可写认为是低电平。那么水平模式的触发条件是状态处于高电平,而边缘模式的触发条件是新来一次电信号将当前状态变为高电平,即:
以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 EPOLLIN 事件;而对于边缘模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 EPOLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 EPOLLOUT 事件。