五种IO模型:
阻塞IO
在内核准备好数据之前,系统调用会一直等待,所有套接字,默认都是阻塞方式. 其为最常见的IO模型
非阻塞IO
如果内核还未将数据报准备好,系统调用会直接返回,同时返回WOULDBLOCK错误码.
在使用过程中需要注意 : 一般需要循环读写文件描述符( 轮询) ,对CPU来说浪费较大.需要考虑使用环境
阻塞与非阻塞的区别: 发起调用后是否会立即返回
信号驱动IO
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
异步IO
由内核在完成数据拷贝时,通知应用程序,与信号驱动不同的是信号驱动是告诉应用程序何时开始拷贝程序
同步:为了完成功能发起调用, 若当前不具备完成条件,则自己等待完成功能后返回
异步: 为了完成功能发起调用, 但是功能由别人完成
同步操作通常都是阻塞操作
异步:
异步阻塞操作 : 等待别人完成操作
异步非阻塞操作 : 操作由系统完成,但是不等待立即返回,操作由系统完成后通过信号通知不等待别人完成操作
同步在于则塞完成任务,效率较低,但是控制流程较为简单
多路转接IO
多路转接IO的核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
其可以理解为一种IO时间的监控: 同时对大量的描述符进行监控,监控文件描述符是否具备IO条件
以前博客所写TCP服务端程序为例:
同一时间只能与一个客户端通讯一次: 原因在于服务端不知道客户端的数据什么时候到来,只能写一个死循环,但是这个死流程就会导致程序阻塞.但是对文件描述符监控,就可以知道哪个描述符的数据什么时候到来,只针对那些就绪的描述符进行处理,程序就不会在阻塞.
多路转接IO模型:
实现对大量描述符进行时间看监控的操作
就绪
对于可读事件, 缓冲区有数据就是读就绪
对于可写事件,缓冲区有空闲空间就是写就绪
select模型
select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数nfds是需要监视的最大的文件描述符值+1
rdset, wrset, exset分别对应与需要检测的可读文件描述符的集合, 可写文件描述符的集合及异常文件描述符的集合
参数timeout为结构timeval,用来设置select()的等待时间
timeout取值:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
函数返回值:
执行成功则返回文件描述词状态已改变的个数
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的
值变成不可预测。
错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
常见代码片段;
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}
通过对几个时间集合中的描述符进行各自的事件监控, 当对应集合中有描述符事件就绪则返回,(返回之前将集合中没有局徐的描述符全部移除)
- 定义描述符集合fd_set
- 将集合拷贝到内核进行监控, 监控的原理是对所有描述符进行轮询遍历状态
- 当有描述符就绪时, 在调用返回之前,将集合中没有就绪的描述符剔除出去
- 用户操作: 对所有的描述符进行遍历,看哪一个还在集合中,则这个描述符已经就绪
fd_set接口:
void FD_CLR(int fd, fd_set *set); //将指定的描述符从集合中移除
int FD_ISSET(int fd, fd_set *set);//判断指定的描述符是否在集合中
void FD_SET(int fd, fd_set *set);//将指定的描述符添加到监控集合中
void FD_ZERO(fd_set *set);// 清空描述符集合
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
-
socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0; -
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
-
socket使用非阻塞connect连接成功或失败之后;
-
socket上有未读取的错误;
select优点:
可以快平台–移植性强
select监控的超时时间更加精确
select缺点:
- select所能监控的描述符是有上限的-默认1024,取决于_FD_SETSIZE
- select实现监控原理是在内核中进行轮询遍历状态, 因此性能会随着描述符增多而下降
- select每次监控都会修改监控集合,需要用户每次监控时重新添加描述符到集合中
- select要监控的集合中的描述符数据,需要每次都重新向内核中拷贝。
- select也不会直接告诉哪一个描述符事件就绪,只是告诉了用户有就绪事件,需要用户遍历查找
Maxfds: 监控的描述符中最大的那个描述符+ 1;
epoll模型
epoll有3个相关的系统调用
epoll_create
int epoll_create(int size);
创建一个epoll的句柄
linux 2.6.8之后,size参数是被忽略的
必须调用close()关闭
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数
1.不同于select()是在监听事件事告诉内核监听什么类型的事件,而是先注册要监听的事件类型
2.epfd是epoll_create()的返回值
EPOLL_CTL_ADD: 注册新的fd到epfd中
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
3.op 是表示动作,用三个宏来表示
4.第三个参数是需要监听的fd
5.第四个参数是告诉内核要监听什么事
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
1.参数events是分配好的epoll_event结构体数组.
2.epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个
events数组中,不会去帮助我们在用户态中分配内存).
3.maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
4.参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
5.如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函
数失败.
epoll的工作原理
- 当进程调用epoll_create时,内核会创建一个eventpoll结构体, 其中有两个成员与epoll使用方式密切相关
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
- 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法的epoll对象中添加进来的事件
- 添加进来的事件都会挂载到红黑树种, 重复添加事件就可以通过红黑树识别
- 所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,即响应的事件发生时会调用这个调用方法(ep_poll_callback)
- ep_poll_callback,会将发生的事件添加到rdlist双链表中
- 在epoll中,对于每一个事件都会建立一个epitem结构体
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的优点相对于select - 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪.这个操作时间复杂度O(1). 即使文件描述 符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限
epoll的工作方式
两种工作方式
水平触发(Level Triggered)
epoll的默认状态 LT 工作模式
- 当epoll检测到socket事件就绪后可以不立即处理,或者只处理一部分
- 在第二次调用epoll_wait时,epoll_wait仍会返回并立即通知socket毒事件就绪
- 直到缓冲区上所有的数据都被处理完,epoll_wait才不会立即返回
- 支持阻塞读写和非阻塞读写
边缘触发(Edge Triggered)
步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
- 当epoll检测到socket上的事件就绪时,必须立即处理
- 在第二次调用epoll_wait不会返回
- ET模式下.文件描述符上的事件就绪后,只有一次处理机会
- ET模式比LT性能更高(wait返回次数少)
- 只支持非阻塞的读写
LT和ET的对比
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价是一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高.
ET模式,非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞.如果服务端写的代码是阻塞式的read, 并且一次只 read 部分数据的话(read不能保证一次就把所有的数据都读出来,可能被信号打断), 剩下的数据就会待在缓冲区中.
此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回,这就形成了一个矛盾,
服务器只读到部分数据, 要读完全部才会给客户端返回响应数据.
客户端要读到服务器的响应, 才会发送下一个请求
客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.
所以需要视同非阻塞轮询的方式来读缓冲区,保证一次把完整的请求读出来.