主页:醋溜马桶圈-CSDN博客
目录
[1.五种 IO 模型](#1.五种 IO 模型)
[1.1 阻塞 IO](#1.1 阻塞 IO)
[1.2 非阻塞 IO](#1.2 非阻塞 IO)
[1.3 信号驱动 IO](#1.3 信号驱动 IO)
[1.4 IO 多路转接](#1.4 IO 多路转接)
[1.5 异步 IO](#1.5 异步 IO)
[2.高级 IO 重要概念](#2.高级 IO 重要概念)
[2.1 同步通信 vs 异步通信(synchronous communication/ asynchronous communication)](#2.1 同步通信 vs 异步通信(synchronous communication/ asynchronous communication))
[2.2 阻塞 vs 非阻塞](#2.2 阻塞 vs 非阻塞)
[2.3 其他高级 IO](#2.3 其他高级 IO)
[2.3.1 非阻塞 IO](#2.3.1 非阻塞 IO)
[2.3.1.1 fcntl](#2.3.1.1 fcntl)
[2.3.1.2 实现函数 SetNoBlock](#2.3.1.2 实现函数 SetNoBlock)
[2.3.2 轮询方式读取标准输入](#2.3.2 轮询方式读取标准输入)
[3.1 初识 select](#3.1 初识 select)
[3.2 select 函数原型](#3.2 select 函数原型)
[3.2.1 参数解释](#3.2.1 参数解释)
[3.2.2 参数 timeout 取值](#3.2.2 参数 timeout 取值)
[3.2.3 fd_set 结构](#3.2.3 fd_set 结构)
[3.2.4 timeval 结构](#3.2.4 timeval 结构)
[3.2.5 函数返回值](#3.2.5 函数返回值)
[3.3 理解 select 执行过程](#3.3 理解 select 执行过程)
[3.4 socket 就绪条件](#3.4 socket 就绪条件)
[3.4.1 读就绪](#3.4.1 读就绪)
[3.4.2 写就绪](#3.4.2 写就绪)
[3.5 select 的特点](#3.5 select 的特点)
[3.6 select 缺点](#3.6 select 缺点)
[3.7 select 使用示例: 检测标准输入输出](#3.7 select 使用示例: 检测标准输入输出)
[4.多路转接 epoll](#4.多路转接 epoll)
[4.1 poll 函数接口](#4.1 poll 函数接口)
[4.1.1 参数说明](#4.1.1 参数说明)
[4.1.2 events 和 revents 的取值](#4.1.2 events 和 revents 的取值)
[4.1.3 返回结果](#4.1.3 返回结果)
[4.1.4 socket 就绪条件](#4.1.4 socket 就绪条件)
[4.2 poll 的优点](#4.2 poll 的优点)
[4.3 poll 的缺点](#4.3 poll 的缺点)
[4.4 poll 示例: 使用 poll 监控标准输入](#4.4 poll 示例: 使用 poll 监控标准输入)
[5.多路转接 epoll](#5.多路转接 epoll)
[5.1 epoll 初识](#5.1 epoll 初识)
[5.2 epoll 的相关系统调用](#5.2 epoll 的相关系统调用)
[5.2.1 epoll_create](#5.2.1 epoll_create)
[5.2.2 epoll_ctl](#5.2.2 epoll_ctl)
[5.2.2.1 int op 的取值](#5.2.2.1 int op 的取值)
[5.2.2.2 struct epoll_event 结构](#5.2.2.2 struct epoll_event 结构)
[5.2.3 epoll_wait](#5.2.3 epoll_wait)
[5.3 epoll 工作原理](#5.3 epoll 工作原理)
[5.4 epoll 的优点](#5.4 epoll 的优点)
[5.5 epoll 工作方式](#5.5 epoll 工作方式)
[5.5.1 水平触发 Level Triggered 工作模式](#5.5.1 水平触发 Level Triggered 工作模式)
[5.5.2 边缘触发 Edge Triggered 工作模式](#5.5.2 边缘触发 Edge Triggered 工作模式)
[5.5.3 对比 LT 和 ET](#5.5.3 对比 LT 和 ET)
[5.5.4 理解 ET 模式和非阻塞文件描述符](#5.5.4 理解 ET 模式和非阻塞文件描述符)
[5.6 epoll 的使用场景](#5.6 epoll 的使用场景)
[5.7 epoll 中的惊群问题](#5.7 epoll 中的惊群问题)
[6.Reactor 反应堆模式](#6.Reactor 反应堆模式)
1.五种IO模型
1.1 阻塞 IO
在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式
阻塞 IO 是最常见的 IO 模型
1.2 非阻塞 IO
如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码
非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为**轮询,**这对 CPU 来说是较大的浪费,一般只有特定场景下才使用
1.3 信号驱动 IO
内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作
1.4 IO 多路转接
虽然从流程图上看起来和阻塞 IO 类似,实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态
1.5 异步 IO
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
任何 IO 过程中,都包含两个步骤,第一是等待,第二是拷贝;而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间,让 IO 更高效,最核心的办法就是让等待的时间尽量少
2.高级 IO****重要概念
**2.1 同步通信vs 异步通信(synchronous communication/**asynchronous communication)
同步和异步关注的是消息通信机制
- 所谓同步,就是在发出一个调用 时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者 主动等待这个调用的结果
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
这里的同步通信和进程之间的同步是完全不相干的概念
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候
以后在看到 "同步" 这个词,一定要先搞清楚大背景是什么,这个同步,是同步通信异步通信 的同步,还是同步与互斥的同步
2.2 阻塞vs非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
2.3 其他高级 IO
非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和 writev 函数以及存储映射 IO(mmap),这些统称为高级 IO
2.3.1 非阻塞 IO
2.3.1.1 fcntl
一个文件描述符,默认都是阻塞 IO
函数原型如下
cpp
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的 cmd 的值不同,后面追加的参数也不相同
fcntl 函数有 5 种功能
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
- 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
- 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
2.3.1.2 实现函数****SetNoBlock
基于 fcntl,我们实现一个 SetNoBlock 函数,将文件描述符设置为非阻塞
cpp
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 参数
2.3.2 轮询方式读取标准输入
cpp
#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;
}
3.多路转接select
3.1 初识****select
系统提供 select 函数来实现多路复用输入/输出模型
- select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的
- 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
3.2 select函数原型
cpp
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3.2.1 参数解释
- 参数 nfds 是需要监视的最大的文件描述符值+1
- rdset,wrset,exset 分别对应于需要检测的可读 文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合
- 参数 timeout 为结构 timeval,用来设置 select()的等待时间
3.2.2 参数 timeout 取值
- NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
- 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回
3.2.3 fd_set 结构
其实这个结构就是一个整数数组,更严格的说,是一个 "位图",使用位图中对应的位来表示要监视的文件描述符
提供了一组操作 fd_set 的接口,来比较方便的操作位图
cpp
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 的全部位
3.2.4 timeval****结构
timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0
3.2.5 函数返回值
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds,exceptfds 和 timeout 的值变成不可预测
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值
- ENOMEM 核心内存不足
常见的程序片段如下:
cpp
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){......}
3.3 理解select执行过程
理解 select 模型的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd,则 1 字节长的 fd_set 最大可以对应 8 个 fd
- (1)执行 fd_set set; FD_ZERO(&set);则 set 用位表示是 0000,0000
- (2)若 fd=5,执行 FD_SET(fd,&set);后 set 变为 0001,0000(第 5 位置为 1)
- (3)若再加入 fd=2,fd=1,则 set 变为 0001,0011
- (4)执行 select(6,&set,0,0,0)阻塞等待
- (5)若 fd=1,fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011
注意:没有事件发生的 fd=5 被清空
3.4 socket 就绪条件
3.4.1 读就绪
- socket 内核中,接收缓冲区中的字节数,大于等于低水位标记
- SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于 0
- socket TCP 通信中,对端关闭连接,此时对该 socket 读,则返回 0
- 监听的 socket 上有新的连接请求
- socket 上有未处理的错误
3.4.2 写就绪
- socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于 0
- socket 的写操作被关闭(close 或者 shutdown),对一个写操作被关闭的 socket 进行写操作,会触发 SIGPIPE 信号
- socket 使用非阻塞 connect 连接成功或失败之后
- socket 上有未读取的错误
3.5 select 的特点
- 可监控的文件描述符个数取决于 sizeof(fd_set)的值,如果服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则服务器上支持的最大文件描述符是 512*8=4096
- 将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd
○ 一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断
○ 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数
备注:
fd_set 的大小可以调整,可能涉及到重新编译内核,感兴趣的小伙伴可以自己去收集相关资料
3.6 select****缺点
- 每次调用 select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 支持的文件描述符数量太小
3.7 select 使用示例: 检测标准输入输出
只检测标准输入:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
for (;;)
{
printf("> ");
fflush(stdout);
int ret = select(1, &read_fds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select");
continue;
}
if (FD_ISSET(0, &read_fds))
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("input: %s", buf);
}
else
{
printf("error! invaild fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
说明:
- 当只检测文件描述符 0(标准输入)时,因为输入条件只有在你有输入信息的时候,才成立,所以如果一直不输入,就会产生超时信息
4.多路转接 epoll
4.1 poll****函数接口
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
4.1.1 参数说明
- fds 是一个 poll 函数监听的结构列表,每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合
- nfds 表示 fds 数组的长度
- timeout 表示 poll 函数的超时时间,单位是毫秒(ms)
4.1.2 events 和 revents 的取值
4.1.3 返回结果
- 返回值小于 0,表示出错
- 返回值等于 0,表示 poll 函数等待超时
- 返回值大于 0,表示 poll 由于监听的文件描述符就绪而返回
4.1.4 socket 就绪条件
同 select
4.2 poll****的优点
不同于 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现.
- pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select"参数-值"传递的方式,接口使用比 select 更方便
- poll 并没有最大数量限制 (但是数量过大后性能也是会下降)
4.3 poll 的缺点
poll 中监听的文件描述符数目增多时
- 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符
- 每次调用 poll 都需要把大量的 pollfd 结构从用户态拷贝到内核中
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
4.4 poll 示例: 使用 poll 监控标准输入
cpp
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
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.多路转接****epoll
5.1 epoll****初识
按照 man 手册的说法: 是为处理大批量句柄而作了改进的****poll.
它是在 2.5.44 内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为 Linux2.6 下性能最好的多路 I/O 就绪通知方法
5.2 epoll****的相关系统调用
epoll 有 3 个相关的系统调用
5.2.1 epoll_create
cpp
int epoll_create(int size);
创建一个 epoll 的句柄
- 自从 linux2.6.8 之后,size 参数是被忽略的
- 用完之后, 必须调用 close()关闭
5.2.2 epoll_ctl
cpp
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll 的事件注册函数
- 它不同于 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
- 第一个参数是 epoll_create()的返回值(epoll 的句柄)
- 第二个参数表示动作,用三个宏来表示.
- 第三个参数是需要监听的 fd
- 第四个参数是告诉内核需要监听什么事
5.2.2.1 int op 的取值
- EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
- EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
- EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
5.2.2.2 struct epoll_event 结构
events 可以是以下几个宏的集合:
- EPOLLIN:表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭)
- EPOLLOUT:表示对应的文件描述符可以写
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误
- EPOLLHUP:表示对应的文件描述符被挂断
- EPOLLET:将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
5.2.3 epoll_wait
cpp
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll 监控的事件中已经发送的事件
- 参数 events 是分配好的 epoll_event 结构体数组
- epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)
- maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size
- 参数 timeout 是超时时间 (毫秒,0 会立即返回,-1 是永久阻塞)
- 如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时,返回小于 0 表示函数失败
5.3 epoll 工作原理
- 当某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关
cpp
struct eventpoll
{
....
/*红黑树的根节点,这颗树中存储着所有添加到 epoll 中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
- 每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件
- 这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是 lgn,其中 n 为树的高度)
- 而所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
- 这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到 rdlist 双链表中
- 在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体
cpp
struct epitem
{
struct rb_node rbn; // 红黑树节点
struct list_head rdllink; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向其所属的 eventpoll 对象
struct epoll_event event; // 期待发生的事件类型
}
- 当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可
- 如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的时间复杂度是 O(1)
总结一下,epoll 的使用过程就是三部曲:
- 调用 epoll_create 创建一个 epoll 句柄
- 调用 epoll_ctl,将要监控的文件描述符进行注册
- 调用 epoll_wait,等待文件描述符就绪
5.4 epoll 的优点
- **接口使用方便:**虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开
- **数据拷贝轻量:**只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而 select/poll 都是每次循环都要进行拷贝)
- **事件回调机制:**避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度 O(1),即使文件描述符数目很多,效率也不会受到影响
- **没有数量限制:**文件描述符数目无上限
网上有些资料说,epoll 中使用了内存映射机制
- 内存映射机制:内核直接将就绪队列通过 mmap 的方式映射到用户态,避免了拷贝内存这样的额外性能开销
这种说法是不准确的,我们定义的 struct epoll_event 是我们在用户空间中分配好的内存,势必还是需要将内核的数据拷贝到这个用户空间的内存中的
5.5 epoll****工作方式
- eg:你正在吃鸡, 眼看进入了决赛圈, 妈妈饭做好了, 喊你吃饭的时候有两种方式:
- 如果妈妈喊你一次, 你没动, 那么妈妈会继续喊你第二次, 第三次...(水平触发)
- 如果妈妈喊你一次, 你没动, 妈妈就不管你了(边缘触发)
epoll 有 2 种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子:
- 我们已经把一个 tcp socket 添加到 epoll 描述符
- 这个时候 socket 的另一端被写入了 2KB 的数据
- 调用 epoll_wait,并且它会返回. 说明它已经准备好读取操作
- 然后调用 read,只读取了 1KB 的数据
- 继续调用 epoll_wait......
5.5.1 水平触发Level Triggered工作模式
epoll 默认状态下就是 LT 工作模式
- 当 epoll 检测到 socket 上事件就绪的时候,可以不立刻进行处理,或者只处理一部分
- 如上面的例子, 由于只读了 1K 数据,缓冲区中还剩 1K 数据,在第二次调用 epoll_wait 时,epoll_wait 仍然会立刻返回并通知 socket 读事件就绪
- 直到缓冲区上所有的数据都被处理完,epoll_wait 才不会立刻返回
- 支持阻塞读写和非阻塞读写
5.5.2 边缘触发Edge Triggered工作模式
如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志,epoll 进入 ET 工作模式
- 当 epoll 检测到 socket 上事件就绪时,必须立刻处理
- 如上面的例子,虽然只读了 1K 的数据,缓冲区还剩 1K 的数据,在第二次调用 epoll_wait 的时候,epoll_wait 不会再返回了
- 也就是说,ET 模式下,文件描述符上的事件就绪后,只有一次处理机会
- ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多). Nginx 默认采用 ET 模式使用 epoll
- 只支持非阻塞的读写
select 和 poll 其实也是工作在 LT 模式下,epoll 既可以支持 LT,也可以支持 ET
5.5.3 对比 LT 和 ET
LT 是 epoll 的默认行为
使用 ET 能够减少 epoll 触发的次数,但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完
相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些,但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的
另一方面,ET 的代码复杂程度更高了
5.5.4 理解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 返回文件描述符读就绪
5.6 epoll****的使用场景
epoll 的高性能是有一定的特定场景的,如果场景选择的不适宜,epoll 的性能可能适得其反
- 对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用 epoll
例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网 APP 的入口服务器,这样的服务器就很适合 epoll
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用 epoll 就并不合适,具体要根据需求和场景特点来决定使用哪种 IO 模型
5.7 epoll****中的惊群问题
6.Reactor反应堆模式
将epoll和socket转化成"一切皆连接"来处理