五种IO模型(阻塞,非阻塞,信号驱动[select, poll, epoll],多路复用,异步IO)
- 本章节代码:
- 一,五种IO模型
- 阻塞IO
- 非阻塞IO
- 多路复用(也叫多路转接)
- 信号驱动
- 异步IO
- 例子加深影响
- 二,多路复用之select
- select 的作用
- select 接口认识
- select 的执行过程
- select 的特点
- select 的优缺点
- 三,多路复用之poll
- poll 的作用
- poll 的接口认识
- poll 的执行过程
- poll的优缺点
- 四,多路复用之epoll
- epoll相关系统调用接口认识
- epoll_create——创建epoll模型
- epoll_ctl——事件注册
- epoll_wait——处理事件
- epoll的工作原理
- epoll的echo server
- epoll的工作模式(Reactor 反应堆模式) LT/ET
- 理解LT/ET模式
- 理解 ET 模式和非阻塞文件描述符
- LT与ET的性能比较
- 五,读写事件就绪
- 六,select,poll,epoll对比
本章节代码:
本章节代码gitee仓库
一,五种IO模型
阻塞IO
- 阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式,再比如我们我们C语言使用的scanf获取字符同样也阻塞IO,只有当你输入字符的时候才行。
非阻塞IO
- 非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用.
- 参数设置为非阻塞状态
在讲文件系统调用时讲open的参数时有个flags我们是没有具体提到的。
这个选项是可以设置为非阻塞选项的
同理recv系统调用也是有相关的设置
系统调用send也是可以
也就是说操作系统天然支持打开文件时设置为非阻塞状态,同时发送数据时也是支持非阻塞状态的
- fcntl系统调用设置为非阻塞状态
该系统调用的作用是:指定文件描述符的状态
C
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
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).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
std::cout << "fcntl error" << std::endl;
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
char buffer[1024];
SetNonBlock(0);
while (true)
{
ssize_t s = ::read(0, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "Echo# " << buffer << std::endl;
}
else
{
//这里要注意的是底层IO条件就绪读取错误采用的是同样的返回值操作。
//但是如果读取错误错误码会被设置,数据灭有就绪同样也会被设置,所以可以用错误码来区分
if (errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "底层数据没有就绪" << std::endl;
sleep(1);
continue;
}
//EINIT信号中断了
else if (errno == EINTR)
{
continue;
}
else
{
std::cout << "读取错误" << std::endl;
break;
}
}
}
return 0;
}
在底层:
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define EAGAIN 11 /* Try again */
#define EINTR 4 /* Interrupted system call */
多路复用(也叫多路转接)
- IO 多路转接: 虽然从流程图上看起来和阻塞 IO 类似. 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态.
信号驱动
- 信号驱动 IO: 内核将数据准备好的时候, 使用 SIGIO 信号通知应用程序进行 IO操作.
异步IO
- 异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
例子加深影响
- 这里我么举一个钓鱼的例子:一群钓鱼佬去钓鱼,张三的钓鱼方式是一直盯着浮漂钓鱼,只要浮漂动了立马钓起,鱼漂没动就一直盯着看。李四的钓鱼方式是一边看手机一边钓鱼,只有鱼漂动了才会关系钓鱼,鱼漂没动就一直玩手机。王五的钓鱼方式就是给鱼竿装上给警报器,一旦鱼漂动了就会发出警报。赵六的钓鱼方式就是一次性放多个鱼竿,赵六就一直在这些鱼竿中来回观察,一旦其中有一个鱼漂动了就钓鱼。田七的钓鱼方式就是雇佣一个钓鱼佬帮他钓鱼,他就在岸边做自己的事情。
- 所以上面描述中,张三就是阻塞IO,李四就是非阻塞IO,王五就是信号驱动,赵六就是多路转接,田七就是异步IO。
二,多路复用之select
select 的作用
- select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select 接口认识
- 函数原型
C
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 参数解析
- 参数 nfds 是需要监视的最大的文件描述符值+1;
- readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;
- 参数 timeout 为结构 timeval,用来设置 select()的等待时间
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
- 参数 timeout 取值:
- NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件,阻塞状态
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,非阻塞状态
- 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。
- 关于 fd_set 结构
- fd_set:表示的是文件描述符集的概念。
- 其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符. 我们举个例子,假设fd_set的长度是1个字节,也就是8个比特位(0000 0000),假设添加了1号描述符和2号描述符,那么fe_set的值就会变成(0000 0011)这样就可以标识那些描述符被标记了。
- 提供了一组操作 fd_set 的接口, 来比较方便的操作位图.
C
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 的全部位
- 返回值
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds, exceptfds 和 timeout 的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值。
- ENOMEM 核心内存不足
select 的执行过程
- 首先select的三个参数readfds,writefds,exceptfds都是输入输出型参数,而上面我们也讲过了fd_set其实就是位图,这里我们只拿readfds来举例子,其他两个是一样的道理。也就是说当位图中的bit位被设置为1并当作参数传递个select其实就是用户告诉内核,让内核关心一下fd_set集合中的fd文件描述符的读事件。而select在工作的时候就会监听事件,一旦监听到事件就绪就会返回监听到的个数,并当作输出型参数输出,也就是内核告诉用户你要我关心的多个fd中有哪些文件描述符的读事件已经就绪了,而关键就是在输出这里。假设我们的读事件是(0000 01101)但是这里如果select只是监听到了3和4的文件描述符的事件就绪了,那么输出就只会输出(0000 01100)它会把没有监听到的事件清空,只返回监听到的事件。所以基于这个原因,在实际写select的业务代码的时候,通常要定义一个用户级内存来暂时保存原有的fd_set集,在就绪文件处理完毕后在恢复原来的fd_set集。
select 的特点
-
可监控的文件描述符个数取决于 sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=128,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符是 128*8=1024.
-
将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select监控集中的 fd,
- 一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。
- 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始select 前都要重新从 array 取得 fd 逐一加(FD_ZERO 最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数。
-
备注:
fd_set 的大小可以调整,可能涉及到重新编译内核. 感兴趣的同学可以自己去收集相关资料.
select 的优缺点
优点
- 可以同时关注多个文件描述符
缺点
- select 支持的文件描述符数量太小.
- select输入输出参数是混合的,每次调用时都需要进行重新定义。
- 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 时,OS底层会在内部遍历检测所有的fd那些就绪了,这个如果fd过多时内核开销会很大
三,多路复用之poll
poll 的作用
- 在讲select的时候我们提到了select的缺点,其中有两个缺点尤为明显:第一等待的fd是有上限的,第二输入输出参数是混合的,这就会导致调用select之前和之后都会使用大量的循环遍历。而poll的作用其实和select是一样的,而poll的出现出现是优化了select的以上两个缺点。
poll 的接口认识
- 函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数解析
- 参数1:struct pollfd *fds,pollfd数组首元素地址,pollfd是操作系统给我们提供的结构体,主要成员如下
fd:文件描述符
events:用户告诉内核,让内核关系fd的events事件
revents:poll返回,内核告诉用户,关心的fd的events已经就绪了
同时也是利用了个结构体来解决输入输出参数混合的问题。- 参数2:nfds_t nfds,数组元素个数。
- 参数3:int timeout,毫秒级的等待时间
timeout > 0 等待timeout毫秒或者有fd就绪再返回。
timeout = = 0 非阻塞轮询。
timeout = = -1 阻塞等待,直到有fd就绪。
- 常见的事件
返回值
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1。
poll 的执行过程
-
其实poll的执行过程和select是非常相似的,只不过poll并不是将输入输出参数给混合了,相反poll是将事件放到了一个结构体中,即有用户到内核的事件添加,也有内核到用户的事件通知,这样也就优化了select输入输出参数混合的问题。同样这样也可以一定程度上从用户态到内核的拷贝。并且这样的设计理论上是可以无上限的关注fd。
-
用户将想要监听的socket文件绑定struct pollfd对象,并注册监听事件至struct pollfd对象events成员,监听多个socket文件使用struct pollfd数组。用户通过struct pollfd数组注册poll事件至poll_list链表,poll_list链表单个元素可以存储固定数量的struct pollfd对象。poll系统调用采用轮询方式获取socket事件信息,一次poll调用需完成整个poll_list链表轮询工作,轮询socket的过程中会创建socket等待队列项,并加入socket等待队列(用于socket唤醒进程)。如果检测到socket处于就绪状态,将socket事件保存在struct pollfd对象的revents成员。poll系统调用完成一次轮询后,如果检测到有socket处于就绪状态,则将poll_list链表所有的struct pollfd通过copy_to_user拷贝至用户struct pollfd数组。如果未检测到有socket处于就绪状态,根据超时时间确定是否返回或者阻塞进程。socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询poll_list链表。
poll的优缺点
优点:
- poll没有最大文件描述符限制。
- poll监视事件(events)和返回事件(revents)分离,简化编程。
缺点:
- 采用轮询方式获取就绪文件描述符,效率低,和select一样。
- 每次调用poll都需要把所有文件描述符从内核空间复制到用户空间。
- 虽然poll没有1024最大文件描述符限制,但是注册的文件描述符越多,poll效率越低。
四,多路复用之epoll
epoll相关系统调用接口认识
epoll_create——创建epoll模型
- 函数原型
#include <sys/epoll.h>
int epoll_create(int size);
-
返回值
如果成功返回一个非负的文件描述符,否则-1被返回并且错误码被设置 -
作用
创建一个 epoll 的句柄.
- 注意:
- 自从 linux2.6.8 之后,size 参数是被忽略的.
- 用完之后, 必须调用 close()关闭.
epoll_ctl——事件注册
- 函数原型
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数介绍
- 第一个参数是 epoll_create()的返回值(epoll 的句柄).
- 第二个参数表示动作,增加,删除,修改,用三个宏来表示.
- 第三个参数是需要监听的 fd.
- 第四个参数是告诉内核需要监听什么事.
- op参数介绍
操作类型 | 描述 |
---|---|
EPOLL_CTL_ADD | 注册新的 fd 到 epfd 中; |
EPOLL_CTL_MOD | 修改已经注册的 fd 的监听事件; |
EPOLL_CTL_DEL | 从 epfd 中删除一个 fd; |
- struct epoll_event 结构介绍
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events 可以是以下几个宏的集合:
事件类型 | 描述 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭); |
EPOLLOUT | 表示对应的文件描述符可以写; |
EPOLLRDHUP | 对端关闭连接(被动),或者套接字处于半关闭状态(主动),这个事件会被触发。当使用边缘触发模式时,很方便写代码测试连接的对端是否关闭了连接 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来); |
EPOLLERR | 表示对应的文件描述符发生错误; |
EPOLLHUP | 表示对应的文件描述符被挂断; |
EPOLLET | 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的. |
EPOLLONESHOT | 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里. |
错误码 | 理解 |
---|---|
EINVAL | 参数无效。可能是传递给epoll_create,epoll_ctl或epoll_wait的某个参数无效。 |
ENOMEM | 没有足够的内存来完成操作。这可能是因为系统内存不足,或者是epoll文件描述符的数量超过了限制。 |
EBADF | 一个或多个提供给epoll函数的文件描述符不是有效的,或者不支持epoll。 |
EFAULT | 指向用户空间的指针指向无效的内存区域。 |
EPERM | 对于epoll_ctl操作,尝试对一个无法受到影响的事件类型执行操作。 |
ENOSPC | 系统限制:系统级别的epoll文件描述符的数量已经达到上限。 |
- 返回值
成功返回0,否则返回-1错误码被设置
epoll_wait——处理事件
- 函数原型
include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
-
作用
收集在 epoll 监控的事件中已经发送的事件. -
参数介绍
- 第一个参数是 epoll_create()的返回值(epoll 的句柄).
- 参数 events 是分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存).
- maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建epoll_create()时的 size.
- 参数 timeout 是超时时间 (毫秒,0 会立即返回,-1 是永久阻塞).
- 返回值
如果函数调用成功,返回对应 I/O 上已准备好的文件描述符数目,如返回 0 表示已超时, 返回小于 0 表示函数失败
epoll的工作原理
-
当调用epoll_creat的时候,操作系统会在底层创建一颗红黑树,和一个双链表的就绪队列。这个红黑树的每个节点是一个叫做eventpoll的结构体。每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。对于调用epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)函数其实就是说:用户告诉内核,叫内核帮我注意一下那些fd上的那些event事情。对于参数op来讲,从数据结构的角度来讲就是对红黑树的增加节点,删除节点,修改节点这三个操作。但是从系统角度来讲,epoll_ctl的作用就是根据用户过来的fd文件描述符和要关系的event事件,在内核当中构建一个eventpoll红黑树节点进行op操作,这就做到了让操作系统关系那些fd文件描述符中的那些event事件了。
-
一旦有数据接收,一定是操作系统先知道的,操作系统就会自动完成将fd和就绪event事件插入到就绪队列中。
-
这个时候epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout)就会以各种策略来定期检查就绪队列,并且它只关心就绪队列,一旦就绪队列中没有事件要么超时要么阻塞,如果有对应的事件就要求用户提供一个epoll_event 的一个缓冲区,缓冲区的大小是maxevents,也就是说你可以定义一个maxevents大小类型是struct epoll_event的数据,该数组会把就就绪队列中的就绪事件严格的按照下标顺序依次放入所提供的数组里。
-
所以现在检测有没有事件就绪的事件复杂度是O(1),也就是只需要检测就绪队列是否为空。
-
那么问题来了,操作系统是如何知道事件就绪了呢?在我们之前也讲过了,数据在网络传输到主机中,根据计算机体系结构一点是电脑的网卡先收到数据,然后触发了硬件中断,这个时候操作系统就一定可以将数据拿到,也就是根据协议栈将数据依次解包最终让用户拿到数据。但是当数据被操作系统拿到并且事件发生时,操作系统除了创建一个红黑树就跟我们上面说过的一样创建一个eventpoll结构体插入红黑树中,这个时候操作系统允许驱动建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法,这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到就绪队列中。
-
所以总结来讲epoll中有三种东西,第一种是红黑树,第二种是就绪队列,第三种是底层回调,这三种结合统称为epoll模型。
-
而到现在我们都还没有对epfd做解释,我们只是知道epfd是epoll_create的返回值,这个返回值是一个文件描述符。那么操作系统中可不可以有多个epoll模型呢?如果有多个的话谁去调用呢?当然是可以有多个epoll模型,而管理多个epoll模型也是六字真言"先描述,再组织",所以维护多个epoll模型本质其实就是维护一个特定的数据结构,而最终都是由进程去调用的,在linux中操作系统也是把epoll模型整合到了文件体系,所以每个epoll模型都是一个个的进程在调用,而每一个进程都有一个文件描述符表,而其中一个文件描述符指向一个文件对象,而文件对象中也会包含一个指针,这个指针将来就可以指向epoll模型,这样将来只需要返回这个文件描述符就可以对epoll模型进行操作了。这就是为什么epoll_create返回的是一个文件描述符,而epoll_ctl和epoll_wait需要传递一个参数epfd的原因,因为只有它可以通过传递过来的文件描述符找到文件对象进而找到epoll模型,最后进行epoll操作。
eventtop对象:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到 epoll 中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过 epoll_wait 返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
红黑树节点对象是:
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的 eventpoll 对象
struct epoll_event event; //期待发生的事件类型
}
epoll的echo server
epoll的工作模式(Reactor 反应堆模式) LT/ET
理解LT/ET模式
- 水平触发 Level Triggered 工作模式
- 边缘触发 Edge Triggered 工作模式
我们举个例子:
- 有一天张三和李四两个快递员分别怕送快递,有一天小王买了五个快递,这天张三给小王派送了他买的5个快递,张三到了小王楼下后就开始给他打电话叫他来取快递,小王知道后会回复马上下来,可是张三等了一会还没有等到小王,于是又给小王打电话,小王还是回复马上下来,过来一会小王下来的,但是只拿走了一个快递,过来一会张三看还有4个快递没有拿走就又给小王大点话叫小王下来拿快递,就这样一直打电话,知道快递被取完。第二天李四给小王派送小王买的3个快递,李四到了小王楼下后就直接打电环,说小王有你的3个快递,如果你不下来取走我就走了,于是小王立马下来了,但是还是只是取走了一个快递,这个时候李四就说,如果你只拿走一个快递我就走了,你要拿就全部拿走,小王听见后立马把全部快递取走。
- 上面的例子中张三就是LT模式,只要有数据还在就一直通知你来拿数据。而李四就是ET模式,只有数据从无到有或者从少到多的时候才会通知,并且必须要求对方一次性把数据全部拿走。
- epoll默认采用的LT模式。
理解 ET 模式和非阻塞文件描述符
- 首先我们明白的是,这是一个单进程的多路转接,对于ET模型来讲,他要求的是一次性把数据全部拿走。那么要怎么怎么样才能把数据全部拿走呢?所以如果一次不能把数据全部拿走的话,就多来几次,也就是循环读取直到把数据全部拿走。但是这又会有一系列的问题出现,那就是如果有5000kb数据,一次可以拿1000kb数据,我们可以保证循环5次后把数据全部拿走,但是计算机知不知道循环5次后可以把数据全部拿走呢?所以计算会在第6次的时候判定数据全部被拿完了,但是此时没数据了,就会进入阻塞状态,但是我们要记住啊这是一个单进程,循环中阻塞可向后果,所以再ET模式下必须设置为非阻塞状态。
LT与ET的性能比较
- 从我们一开始举的送快递的例子我们的第一反应肯定是ET模式更高效的,这是从通知角度这样理解的,LT模式是只要有数据就一直通知,ET模式是只有当数据一开始来了,或者数据开始变多的时候才会通知一次。
- 但是从上面的理解来看我们不能单单从通知角度来理解,上面说过了ET模式下必须要求一次性把数据全部拿走,也就倒逼着程序员写一个循环读取将数据全部一次性拿走,这也就说明了ET模式下一定是要求读取数据的速度效率要快,也就是尽快将缓冲区的数据一次性打包带走,这样也就间接的可以将缓冲区的大小快速的腾出空间,并再ACK确认应答中将更大的滑动窗口大小告诉对方,这样就可以提高效率了。
- 但是也不是LT的性能一定会比ET的性能低,LT之所以说名义上比ET模式性能低,其本质其实就是一次性读取数据的大小是不固定的,并且可能会进行通知多次。那要是解决了这个问题不是也可以和ET模式新能把手腕了吗?所以LT模式其实可以在特定的优化有也可以做到提高性能。
五,读写事件就绪
- 从我们之前的学习当中,我们知道了所有的输入和输出都是从输入输出缓冲区中写和读的。
- 那么如果是读事件的话我们关心的是输出缓冲区中有没有数据。
- 如果是写事件的话,我们关心的是输出缓冲区的是否有足够的空间可以进行写入。
- 所以我们就一个新创建的一个fd来讲,这个文件描述符管理的输入输出缓冲区中的特点是,没有数据但是有足够的空间,那么结论就是对于一个新创建fd来讲默认是读事件是不满足的,因为输入缓冲区中没有数据,但是写事件默认是满足的,因为有足够的空间去写入。
- 但是在多路复用中处理一个fd,首先读事件是一定要进行关心的,因为它缓冲区中有没有数据有关。但是对于写事件则需要按照需求设定。因为多路复用其实本质上是一个等待数据就绪的操作,但是对于一个新创建的fd或者说也不是一个新的fd来讲大部分时间可能都是处于一个有足够空间的情况,也就可直接进行写操作并不需要做等待空间就绪操作。所以在多路复用模型下对于写事件不直接交给多路转接,而只是直接写入,只有当空间被写满了之后再交给多路转接。
- 这里要注意的是如果再epoll下打开了写事件,那么EPOLLOUT就会自动被触发一次。
六,select,poll,epoll对比
- 首先是select和poll的对比,我们可以发现select和poll在编写测试代码的时候是有很多的相似之处的,我们可以理解位poll其实是强化版本的select模型。poll强化的方法就是通过一个传递结构体数组,并指定数组的大小,并且是采用链表的方式存储事件对象的,这样做即解决了文件描述符上限的问题,同时将输入输出参数进行了分离,一定程度上减少了不必要的轮询,提高了效率,但是这样的效率在文件描述符过多的时候也是微乎其微的。但是poll依然没有解决效率影响最大的的部分,也就是内核和用户之间的数据拷贝问题,以及select,poll监视多个fd时OS底层采用的时轮询的方式遍历检测所有的fd,并且在用户层这两个多需要采用轮询的方式找到就绪的fd,这样的轮询在fd过多的时候效率时非常的不理想的。所以才会有epoll的出现,epoll基本上解决了上述的所有缺点,epoll采用底层数据结构红黑树结构体,并采用底层回调函数的方式解决了内核和用户数据拷贝的问题,采用回调添加到就绪队列中的方式替换轮询方式,大大提高了效率。
对比项 | select | poll | epoll |
---|---|---|---|
事件对象存储方式 | 位图 | 链表+数组 | 红黑树 |
底层实现 | 轮询方式,每次调用都需要进行内核和用户数据拷贝(内核->用户,用户->内核) | 轮询方式,每次调用都需要从内核空间拷贝所有事件到用户空间 | 采用回调方式,每次调用只需要将就绪队列中的数据从内核拷贝到到用户空间即可。 |
最大连接数 | sizeof(fd_set) * 8 | 理论上无限制(由系统资源就决定) | 理论上无限制(由系统资源就决定) |
是否适用于高并发场景 | 否,随着连接的数量增加,性能呈现线性下降 | 否,随着连接的数量增加,性能呈现线性下降 | 是否,随着连接的数量增加,性能无明显下降 |
变成难度 | 低 | 低 | 中 |