阿里云 外贸网站,宜兴网站优化,建筑设计方案汇报ppt,广告投放收费标准1. 五种IO模型1.1 阻塞IO阻塞 IO#xff1a;这是最常见的 I/O 模型。在内核将数据准备好之前#xff0c;系统调用会一直等待。所有的套接字#xff0c;默认 都是阻塞方式。特点#xff1a;- 实现简单#xff0c;易于理解- 效率较低#xff0c;在 I/O 操作期间进程资源被闲…1. 五种IO模型1.1 阻塞IO阻塞 IO这是最常见的 I/O 模型。在内核将数据准备好之前系统调用会一直等待。所有的套接字默认 都是阻塞方式。特点 - 实现简单易于理解 - 效率较低在 I/O 操作期间进程资源被闲置 - 例如Socket 的 recv ()、read () 等函数默认都是阻塞的1.2 非阻塞IO非阻塞 IO如果内核还未将数据准备好系统调用不会阻塞而是立即返回结果返回EWOULDBLOCK 错误码。非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符这个过程称为轮询。这对 CPU 来说是较大的浪费一般只有特定场景下才使用。特点 - 不会阻塞进程可在等待 I/O 时执行其他任务 - 需要轮询检查会消耗 CPU 资源 - 适用于操作频繁且处理时间短的场景1.3 信号驱动IO信号驱动 IO应用程序通过系统调用注册一个信号处理函数当 I/O 操作准备就绪时内核会发送信号 SIGIO 通知应用程序进行实际的 I/O 操作。特点 - 无需轮询减少 CPU 消耗应用程序 - 在收到信号前可以正常执行其他任务 - 信号处理机制可能比较复杂1.4 多路转接IOIO 多路转接虽然从流程图上看起来和阻塞 IO 类似。实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。特点 - 可以同时处理多个 I/O 操作 - 常用的实现有 select、poll、epollLinux和 kqueueBSD - 适用于需要处理大量并发连接的场景如服务器1.5 异步IO异步 IO应用程序发起 I/O 操作后立即返回继续执行其他任务。由内核在数据拷贝完成时通知应用程序信号驱动是告诉应用程序何时可以开始拷贝数据。特点- 完全异步应用程序几乎不需要等待 I/O 操作 - 效率最高但实现复杂度也最高 - 与非阻塞 IO 的区别异步 IO 是操作完成后通知而非阻塞 IO 是操作准备好后通知
任何 IO 过程中都包含两个步骤。第一是等待第二是拷贝。而且在实际的应用场景中等待消耗的时间往往都远远高于拷贝的时间。让 IO 更高效最核心的办法就是让等待的时间尽量少。2. 高级 IO 重要概念 2.1 同步通信 vs 异步通信同步和异步关注的是消息通信机制。同步通信- 调用发出后必须等待结果返回才能继续执行- 例如打电话问快递员是否已送达必须等对方回应才能挂电话- 对应 IO 模型阻塞 IO、非阻塞 IO、多路转接 IO、信号驱动 IO异步通信- 调用发出后立即返回无需等待结果- 结果通过通知、回调等方式后续处理- 例如发微信问快递员是否已送达发送后可以做其他事等对方回复即可- 对应 IO 模型异步 IO注意这里的 同步 与多线程中的 同步 完全不同。线程同步是指多个线程间的执行顺序协调如用锁保证临界资源访问顺序而通信同步是指消息传递的方式。看到 同步 这个词一定要先搞清楚大背景是什么是同步通信异步通信的同步还是同步与互斥的同步。2.2 阻塞 vs 非阻塞 阻塞和非阻塞关注的是程序在等待调用结果消息返回值时的状态。 - 阻塞调用是指调用结果返回之前当前线程会被挂起。被调用线程只有在得到结果之后才会返回。 - 非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程线程可以继续执行其他任务。2.3 四者组合关系可以形成四种组合同步阻塞最常见的传统 IO 方式调用后一直等待如阻塞 recv ()同步非阻塞调用后不断轮询检查结果如非阻塞 IO 的轮询异步阻塞实际中很少见可理解为发出异步请求后阻塞等待通知异步非阻塞效率最高的方式发出请求后完全不阻塞通过回调处理结果。妖怪蒸唐僧形象类比同步阻塞妖怪生火后一直守在锅边啥也不干直到唐僧蒸熟同步非阻塞妖怪生火后去打游戏但每隔几分钟就去看看唐僧熟了没异步非阻塞妖怪设置好蒸锅的定时提醒然后去打游戏时间到了提醒器会响再回来处理。非阻塞 IO纪录锁系统 V 流机制I/O 多路转接也叫 I/O 多路复用readv 和writev 函数以及存储映射 IOmmap这些统称为高级 IO。3. 非阻塞IO3.1 ftcnl函数原型和基本用法fcntlfile control是 Unix/Linux 系统中一个非常重要的系统调用用于对已打开的文件描述符进行各种控制操作是实现高级 IO 模型如非阻塞 IO的核心工具。
#include unistd.h
#include fcntl.hint fcntl(int fd, int cmd, ... /* arg */ );参数说明
fd需要操作的文件描述符如通过 open、socket 获得的描述符。
cmd操作命令决定要执行的具体功能。
可变参数 ...根据 cmd 的不同可能需要传入额外参数通常是 int 或 struct flock* 类型。
返回值
成功根据 cmd 不同返回不同值如文件状态标记、0 等。
失败返回 -1并设置 errno 表示错误原因。3.2 fcntl 函数 5 种功能1. 复制文件描述符F_DUPFD、F_DUPFD_CLOEXEC
用于复制一个已有的文件描述符类似 dup 和 dup2 函数。F_DUPFD复制 fd返回一个大于等于第三个参数arg的新描述符新描述符与原描述符指向同一文件且共享文件状态。
int new_fd fcntl(old_fd, F_DUPFD, 0); // 复制old_fd新描述符从0开始找可用值F_DUPFD_CLOEXEC功能与 F_DUPFD 类似但新描述符会设置 FD_CLOEXEC 标志进程执行 exec 时自动关闭该描述符避免资源泄漏。2. 获取 / 设置文件描述符标记F_GETFD、F_SETFD
文件描述符标记是描述符本身的属性而非文件的属性最常用的是 FD_CLOEXEC 标志。F_GETFD获取当前文件描述符的标记返回一个 int 值。
F_SETFD设置文件描述符的标记第三个参数为新标记值通常是 FD_CLOEXEC 或 0。
// 设置FD_CLOEXEC标志exec时自动关闭
int flags fcntl(fd, F_GETFD);
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);3. 获取 / 设置文件状态标记F_GETFL、F_SETFL
文件状态标记是文件的属性决定了 IO 操作的行为如阻塞 / 非阻塞、读写模式等是 fcntl 最常用的功能。O_RDONLY/O_WRONLY/O_RDWR读写模式打开文件时设置fcntl 不能修改。
O_NONBLOCK非阻塞模式核心标志设置后 IO 操作不阻塞。
O_APPEND追加模式写入时数据自动加到文件末尾。
O_ASYNC异步 IO 模式数据就绪时触发信号
// 1. 获取当前状态标记
int flags fcntl(fd, F_GETFL, 0);
if (flags -1) {perror(fcntl F_GETFL failed);
}// 2. 修改标记如添加非阻塞和追加模式
flags | O_NONBLOCK | O_APPEND;// 3. 设置新标记
if (fcntl(fd, F_SETFL, flags) -1) {perror(fcntl F_SETFL failed);
}4. 异步 IO 所有权控制F_GETOWN、F_SETOWN
用于设置异步 IO 的接收进程 / 线程当文件描述符可读写时内核会向指定进程发送 SIGIO 信号。F_GETOWN获取当前接收异步信号的进程 ID 或线程 ID。
F_SETOWN设置接收异步信号的进程 ID正数或线程 ID负数。
// 设置O_ASYNC标志启用异步通知
int flags fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);// 设置当前进程接收SIGIO信号
fcntl(fd, F_SETOWN, getpid());5. 文件记录锁F_GETLK、F_SETLK、F_SETLKW
用于对文件的部分内容记录加锁实现多进程间的同步。
struct flock {short l_type; // 锁类型F_RDLCK读锁、F_WRLCK写锁、F_UNLCK解锁short l_whence; // 偏移基准SEEK_SET、SEEK_CUR、SEEK_ENDoff_t l_start; // 锁的起始偏移off_t l_len; // 锁的长度0表示到文件末尾pid_t l_pid; // 持有冲突锁的进程IDF_GETLK时使用
};struct flock lock;
lock.l_type F_WRLCK; // 写锁
lock.l_whence SEEK_SET; // 从文件开头开始
lock.l_start 0; // 起始位置0
lock.l_len 100; // 锁定前100字节// 加锁若冲突则阻塞等待
if (fcntl(fd, F_SETLKW, lock) -1) {perror(fcntl lock failed);
}下面使用第三种功能获取/设置文件状态标记就可以将一个文件描述符设置为非阻塞。
void SetNoBlock(int fd) {int fl fcntl(fd, F_GETFL);if (fl 0) {perror(fcntl);return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)。然后再使用 F_SETFL 将文件描述符设置回去。设置回去的同时, 加上一个 O_NONBLOCK 参数。轮询方式读取标准输入#include stdio.h
#include unistd.h
#include fcntl.h
void SetNoBlock(int fd) {int fl fcntl(fd, F_GETFL);if (fl 0) {perror(fcntl);return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {SetNoBlock(0);while (1) {char buf[1024] {0};ssize_t read_size read(0, buf, sizeof(buf) - 1);if (read_size 0) {perror(read);sleep(1);continue;}printf(input:%s\n, buf);}return 0;
}4. 多路转接selectselect 是一种 I/O 多路复用机制其核心作用是允许一个进程同时监控多个文件描述符如套接字、普通文件、管道等并在其中一个或多个文件描述符处于就绪状态即可读、可写或发生异常时通知进程进行处理。select 的设计解决了传统阻塞 I/O 模型的局限性在单进程 / 单线程中若同时处理多个 I/O 操作如同时与多个客户端通信的服务器传统方式需要阻塞等待某一个 I/O 完成导致其他 I/O 无法及时响应。select 通过以下方式实现多路复用 - 进程将需要监控的文件描述符集合可读、可写、异常三类传递给 select- select 阻塞等待直到集合中至少有一个文件描述符进入就绪状态或超时返回- 进程通过检查文件描述符集合确定哪些 I/O 可以操作从而高效处理多个任务。4.1 函数原型与参数解析1函数原型
#include sys/select.h
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 2参数解析1. 参数 nfds 是需要监视的最大的文件描述符的值1。fd_set 本质是位图内核通过该值确定需要遍历的位范围从 0 到 nfds-1。例如若监控的 fd 为 3、5则 nfds 需设为 651。2. rdsetwrsetexset 分别对应于需要检测的可读文件描述符的集合可写文件描述符的集合及异常文件描述符的集合。若不需要监控某类事件可设为 NULL如只监控读事件时writefds 和 exceptfds 设为 NULL。这些集合是 “输入输出型参数”调用时传入待监控的 fd 集合返回时内核会修改它们仅保留触发事件的 fd。3. 参数 timeout 为结构 timeval用于设置 select 的等待超时时间类型为 struct timeval
struct timeval {long tv_sec; // 秒long tv_usec; // 微秒1秒1000000微秒
};取值 NULLselect 会一直阻塞直到至少一个 fd 触发事件。
取值 (0, 0)立即返回仅检查当前 fd 状态非阻塞轮询。
特定时间若超时前无事件返回 0否则返回触发事件的 fd 数量。3函数返回值 - 执行成功则返回文件描述词状态已改变的个数 - 如果返回 0 代表在描述词状态改变前已超过 timeout 时间没有返回 - 当有错误发生时则返回-1错误原因存于 errno此时参数 readfdswritefdsexceptfds 和 timeout 的值变成不可预测。
4错误值- EBADF 文件描述词为无效的或该文件已关闭- EINTR 此调用被信号所中断- EINVAL 参数 n 为负值。- ENOMEM 核心内存不足4.2 fd_set 操作与原理
fd_set 是一个位图结构本质是整数数组每一位对应一个文件描述符用于标记该 fd 是否被监控。系统提供了 4 个宏操作该结构
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组 set 中相关fd 的位int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组 set 中相关fd 的位是否为真void FD_SET(int fd, fd_set *set); // 用来设置描述词组 set 中相关fd 的位void FD_ZERO(fd_set *set); // 用来清除描述词组 set 的全部位fd_set 结构体是输入输出型参数其含义在调用 select 前后有所不同输入前fd_set 是 “监控清单”告诉内核要关注哪些 fd 的哪些事件。
输入后fd_set 是 “就绪清单”内核返回哪些 fd 实际发生了事件。示例fd_set 状态变化取 fd_set 长度为 1 字节fd_set 中的每一 bit 可以对应一个文件描述符 fd。则 1 字节长的 fd_set 最大可以对应 8 个 fd。1执行 fd_set setFD_ZERO(set)则 set 用位表示是 0000,0000。 2若 fd5执行 FD_SET(fd,set)后 set 变为 0001,0000(第 5 位置为 1) 3若再加入 fd2fd1则 set 变为 0001,0011 4执行 select(6,set,0,0,0)阻塞等待 5若 fd1fd2 上都发生可读事件则 select 返回此时 set 变为0000,0011。注意没有事件发生的 fd5 被清空。4.3 select就绪条件读就绪 - socket 内核中接收缓冲区中的字节数低水位标记SO_RCVLOWAT此时可以无阻塞的读该文件描述符并且返回值大于 0 - socket TCP 通信中对端关闭连接此时对该 socket 读则返回 0 - 监听的 socket 上有新的连接请求 - socket 上有未处理的错误读操作返回 -1 并设置 errno写就绪 - socket 内核中发送缓冲区中的可用字节数发送缓冲区的空闲位置大小低水位标记 SO_SNDLOWAT此时可以无阻塞的写并且返回值大于 0 - socket 的写操作被关闭(close 或者 shutdown)。对一个写操作被关闭的 socket 进行写操作会触发 SIGPIPE 信号 - socket 使用非阻塞 connect 连接成功或失败之后 - socket 发生错误写操作返回 -1 并设置 errno
异常就绪exceptfds 触发 - socket 收到 TCP 带外数据紧急数据通过 MSG_OOB 标志发送4.4 select的特点和缺点select 的特点 - 可监控的文件描述符个数取决于 sizeof(fd_set)的值。如果服务器上 sizeof(fd_set)512每 bit 表示一个文件描述符则该服务器上支持的最大文件描述符是 512*84096。 - 将 fd 加入 select 监控集的同时还要再使用一个数据结构 array 保存放到 select 监控集中的 fd - 一是用于再 select 返回后array 作为源数据和 fd_set 进行 FD_ISSET 判断。 - 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先)扫描 array 的同时取得 fd 最大值 maxfd用于 select 的第一个参数。 select 缺点 - 每次调用 select都需要手动设置 fd 集合从接口使用角度来说也非常不便。 - 每次调用 select都需要把 fd 集合从用户态拷贝到内核态这个开销在 fd 很 多时会很大 - 同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很 多时也很大 - select 支持的文件描述符数量太小。5. 多路转接poll和epoll5.1 poll1函数原型
#include poll.h
int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {int fd; /* 要监控的文件描述符 */short events; /* 注册的事件输入要监控哪些事件 */short revents; /* 返回的事件输出实际发生了哪些事件 */
};2参数说明 - fds 是一个 poll 函数监听的结构列表。每一个元素中包含了三部分内容文件描述符监听的事件集合返回的事件集合。若 fd 为 -1则 events 字段被忽略 revents始终为 0。 - nfds 表示 fds 数组的长度 - timeout 表示超时时间单位为毫秒ms取值规则 - -1永久阻塞直到有事件发生才返回。 - 0非阻塞模式立即返回当前状态无论是否有事件。 - 正数最多等待指定毫秒数超时后返回 0。events 和 revents 的取值3返回结果返回值小于 0表示出错返回值等于 0表示 poll 函数等待超时返回值大于 0表示 poll 由于监听的文件描述符就绪而返回。poll 就绪条件 同 select4poll 的优点 - 不同于 select 使用三个位图来表示三个 fdset 的方式poll 使用一个 pollfd 指针实现。 - pollfd 结构包含了要监视的 event 和发生的 event不再使用 select“参数-值”传递的方式。接口使用比 select 更方便。 - poll 并没有最大数量限制 (但是数量过大后性能也是会下降)。5poll 的缺点 poll 中监听的文件描述符数目增多时 - 和 select 函数一样poll 返回后需要轮询 pollfd 来获取就绪的描述符. - 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中. - 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
示例
int main()
{struct pollfd poll_fd;poll_fd.fd 0;poll_fd.events POLLIN;for (;;){int ret poll(poll_fd, 1, 1000);if (ret 0){perror(poll);continue;}if (ret 0){printf(poll timeout\n);continue;}if (poll_fd.revents POLLIN){char buf[1024] {0};read(0, buf, sizeof(buf) - 1);printf(stdin:%s, buf);}}
}5.2 epollepoll 是 Linux 内核为处理大批量文件描述符File DescriptorFD而设计的多路 I/O 就绪通知机制于 Linux 2.5.44 内核中引入被公认为 Linux 2.6 及以上版本中性能最优的多路 I/O 模型。它针对传统的 select/poll 机制的缺陷进行了根本性改进尤其适合高并发场景如网络服务器。5.2.1 核心系统调用epoll 的使用依赖三个核心系统调用分别负责创建句柄、注册事件和等待事件就绪。1epoll_create创建 epoll 句柄
int epoll_create(int size);作用创建一个 epoll 实例句柄用于管理后续注册的事件。
参数size 在 Linux 2.6.8 及以上版本中被忽略早期用于提示内核预分配资源。
返回值成功返回 epoll 句柄非负整数失败返回 -1 并设置 errno。注意使用完毕后必须通过 close() 关闭句柄释放内核资源。
2 epoll_ctl事件注册 / 修改 / 删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);作用向 epoll 句柄注册、修改或删除需要监控的文件描述符及其事件。返回值
成功时返回 0整数零表示事件注册、修改或删除操作已顺利完成。
失败时返回 -1同时会设置全局变量 errno 来指示具体的错误原因。参数
epfdepoll_create 返回的 epoll 句柄。
op操作类型可选值
EPOLL_CTL_ADD注册新的 FD 到 epoll 中。
EPOLL_CTL_MOD修改已注册 FD 的监控事件。
EPOLL_CTL_DEL从 epoll 中删除某个 FD此时 event 可为 NULL。fd需要监控的文件描述符如 socket、管道等。
event指向 struct epoll_event 结构体的指针描述需要监控的事件类型。
struct epoll_event 结构
struct epoll_event {uint32_t events; // 监控的事件类型宏的集合epoll_data_t data; // 用户数据可存储 FD 或自定义指针
};其中 events 支持的事件类型
EPOLLINFD 可读包括对端正常关闭。
EPOLLOUTFD 可写。
EPOLLPRIFD 有紧急数据可读如带外数据。
EPOLLERRFD 发生错误无需主动注册内核会自动通知。
EPOLLHUPFD 被挂断如对端关闭无需主动注册。
EPOLLET边缘触发Edge TriggeredET模式默认是水平触发。
EPOLLONESHOT仅监控一次事件触发后需重新注册才能再次监控。3epoll_wait等待事件就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);作用等待 epoll 句柄中监控的 FD 发生就绪事件并收集这些事件。返回值
成功返回就绪事件的数量0 表示超时。
失败返回 -1 并设置 errno如被信号中断。参数
epfdepoll 句柄。
events用户预先分配的 struct epoll_event 数组用于存储就绪事件。
maxeventsevents 数组的最大长度不能超过 epoll_create 时的 size尽管 size 已被忽略但需保证数组足够大。
timeout超时时间毫秒
-1永久阻塞直到有事件就绪。
0立即返回无论是否有事件。
正数等待指定毫秒数后返回。5.2.2 两个关键数据结构和回调机制epoll 的高效性源于其内核设计核心依赖两个关键数据结构和回调机制1内核核心结构 eventpoll
当调用 epoll_create 时内核会创建一个 eventpoll 结构体用于管理监控的事件
struct eventpoll {struct rb_root rbr; // 红黑树根节点存储所有注册的事件epitemstruct list_head rdlist; // 双链表存储就绪的事件待返回给用户// 其他成员如等待队列、锁等
};2事件节点 epitem
每个注册到 epoll 的 FD 对应一个 epitem 结构体用于关联事件信息
struct epitem {struct rb_node rbn; // 红黑树节点挂到 rbr 上struct list_head rdllink; // 双链表节点事件就绪时挂到 rdlist 上struct epoll_filefd ffd; // 关联的 FD 信息struct eventpoll *ep; // 指向所属的 eventpollstruct epoll_event event; // 监控的事件类型用户注册
};3工作流程
1. 注册事件通过 epoll_ctl 注册 FD 时内核会创建 epitem 结构体将其插入 eventpoll 的红黑树rbr中红黑树保证高效的插入 / 删除 / 查找时间复杂度 O (log n)。2. 事件触发回调注册的 FD 会与设备驱动如网卡绑定回调函数 ep_poll_callback。当 FD 就绪如可读 / 可写时驱动会调用该回调将对应的 epitem 加入 eventpoll 的就绪双链表rdlist。3. 等待与返回调用 epoll_wait 时内核只需检查 rdlist 是否为空若不为空将 rdlist 中的事件复制到用户态的 events 数组返回就绪事件数量时间复杂度 O (1)。若为空进程进入阻塞或超时后返回 0。
4总结 - 每一个 epoll 对象都有一个独立的 eventpoll 结构体用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。 - 这些事件都会挂载在红黑树中如此重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 logn其中 n 为树的高度)。 - 而所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系也就是说当响应的事件发生时会调用这个回调方法。 - 这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到 rdlist 双链表中。 - 在 epoll 中对于每一个事件都会建立一个 epitem 结构体 - 当调用 epoll_wait 检查是否有事件发生时只需要检查 eventpoll 对象中的rdlist 双链表中是否有 epitem 元素即可
epoll 的优点(和 select 的缺点对应) - 接口使用方便虽然拆分成了三个函数但是反而使用起来更方便高效不需要每次循环都设置关注的文件描述符也做到了输入输出参数分离。 - 数据拷贝轻量只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)。- 事件回调机制避免使用遍历而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度 O(1)即使文件描述符数目很多, 效率也不会受到影响。 - 没有数量限制: 文件描述符数目无上限5.2.3 epoll与poll、select的对比维度selectpollepollfd 数量限制有默认 1024由内核编译参数决定无仅受系统最大文件描述符限制无仅受系统最大文件描述符限制事件存储结构fd_set固定大小的位集合struct pollfd 数组动态大小红黑树管理注册的 fd 双向链表就绪事件事件注册方式每次调用需重新设置关注的事件输入输出参数混合每次调用需传入整个 pollfd 数组输入输出参数混合提前通过 epoll_ctl 注册 / 修改 / 删除输入输出参数分离数据拷贝成本每次调用需将 fd_set 从用户态拷贝到内核态每次调用需将 pollfd 数组从用户态拷贝到内核态仅在 epoll_ctl 注册时拷贝一次拷贝后续复用就绪事件获取遍历所有监控的 fdO (n)遍历所有监控的 fdO (n)直接获取就绪链表O (1)触发模式仅水平触发LT仅水平触发LT支持水平触发LT和边缘触发ET适用场景连接数少且固定的场景如简单的客户端连接数中等的场景比 select 灵活但效率有限高并发、连接数多的场景如服务器
6. epoll工作方式
epoll 有 2 种工作方式---水平触发LT和边缘触发ET。
6.1 水平触发LT默认模式epoll 默认工作在 LT 模式无需额外设置标志。核心特点当文件描述符如 TCP socket上的事件就绪如可读时epoll_wait 会反复通知该事件直到缓冲区中的数据被完全处理。场景示例
若 socket 接收缓冲区有 2KB 数据第一次调用 epoll_wait 会返回 “可读”若只读取了 1KB剩余 1KB第二次调用 epoll_wait 仍会立刻返回 “可读”直到 2KB 数据全部读完epoll_wait 才不会再因该事件触发。支持的 I/O 方式既支持阻塞读写也支持非阻塞读写。6.2 ET边缘触发
启用方式将 socket 添加到 epoll 描述符时需指定 EPOLLET 标志。核心特点当文件描述符上的事件就绪时epoll_wait 只会通知一次无论数据是否被完全处理。若未及时处理所有数据剩余数据不会再次触发事件直到下次有新数据到来。场景示例
同样是 socket 接收 2KB 数据第一次 epoll_wait 返回 “可读”若只读取 1KB剩余 1KB第二次调用 epoll_wait 不会返回因事件仅通知一次剩余 1KB 需等待客户端发送新数据时才会被再次触发。优势与适用场景
性能更高epoll_wait 返回次数少Nginx 等高性能服务器默认使用 ET 模式。限制仅支持非阻塞读写详细见6.3。使用 ET 能够减少 epoll 触发的次数。但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完。相当于一个文件描述符就绪之后不会反复被提示就绪看起来就比 LT 更高效一些。但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理不让这个就绪被重复提示的话其实性能也是一样的。另一方面ET 的代码复杂程度更高了。
6.3 理解 ET 模式和非阻塞文件描述符 使用 ET 模式的 epoll需要将文件描述设置为非阻塞。这个不是接口上的要求而是 工程实践 上的要求。假设这样的场景服务器接收到一个 10k 的请求会向客户端返回一个应答数据。如果客户端收不到应答不会发送第二个 10k 请求。
如果服务端写的代码是阻塞式的 read并且一次只 read 1k 数据的话read 不能保证一次就把所有的数据都读出来参考 man 手册的说明可能被信号打断剩下的 9k 数据就会待在缓冲区中此时由于 epoll 是 ET 模式并不会认为文件描述符读就绪epoll_wait 就不会再次返回剩下的 9k 数据会一直在缓冲区中。直到下一次客户端再给服务器写数据epoll_wait 才能返回。但是问题来了服务器只读到 1k 个数据要 10k 读完才会给客户端返回响应数据。而客户端要读到服务器的响应才会发送下一个请求客户端发送了下一个请求epoll_wait 才会返回服务器才能去读缓冲区中剩余的数据。
最终导致形成死锁服务器等剩余数据客户端等响应。所以为了解决上述问题阻塞 read 不一定能一下把完整的请求读完于是就可以使用 非阻塞轮询的方式来读缓冲区保证一定能把完整的请求都读出来。
如果是 LT 则没这个问题只要缓冲区中的数据没读完就能够让 epoll_wait 返回文件描述符读就绪。6.4 epoll 的使用场景 epoll 的高性能是有一定的特定场景的。如果场景选择的不适宜epoll 的性能可能适得其反。对于多连接且多连接中只有一部分连接比较活跃时比较适合使用 epoll。例如典型的一个需要处理上万个客户端的服务器,例如各种互联网 APP 的入口服务器这样的服务器就很适合 epoll。如果只是系统内部服务器和服务器之间进行通信只有少数的几个连接这种情况下用 epoll 就并不合适具体要根据需求和场景特点来决定使用哪种 IO 模型。