高级IO
IO
用户层read&&write:本质就是把用户层数据写到OS,也就是拷贝
IO的本质:等待+拷贝
五种IO模型
1、阻塞式IO
2、非阻塞式IO
3、信号驱动IO
4、多路复用(多路转接),后面详细介绍
5、异步IO
同步通信和异步通信
同步:当发出一个调用时,如果没有结果,那就一直等,知道有结果再返回,这种就是调用者主动等待这个调用结果
异步:发出调用后,调用者不会等待结果,而是被调用者通过状态来通知调用者,或者通过回调函数来处理这个调用
注意:
这里的同步和线程之间的同步不是一个概念
进程/线程的同步是进程/线程直接制约关系,线程之间对共享资源访问时的保护
阻塞和非阻塞
阻塞调用:在得到返回结果之前,线程会被挂起,只有得到返回结果之后才返回
非阻塞调用:在得到返回结果之前,线程不会被阻塞
非阻塞IO往往需要循环的方式反复尝试读写文件描述符,也就是轮询
多路转接
select函数
fd_set
![](https://i-blog.csdnimg.cn/direct/ad660f9f40654451997d84a2f7902a3f.png)
当输入时 ,用户告诉内核,关注输入的一个或多个fd上的读事件(写事件或者异常事件),如果事件就绪了,内核要返回给用户
当输出时,内核告诉用户,fd上关注的事件(读、写或异常)已经就绪了
select缺点
1、等待的文件描述符有上限
2、输入和输出型参数参数多,导致数据拷贝的频率很高
3、每次都需要对关心的fd事件进行重置
4、用户层管理关心的fd,每次都需要遍历,内核检查fd事件是否就绪也需要遍历
poll
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
这里events表示要关心的事件(用户填写),revents表示已经发生的事(内核填写)
poll优点
与select相比,解决了select关心文件描述符上限和重置问题
因为每次select都需要重置事件,但是poll的结构体解决了这个问题,只需要在结构体中添加关心事件
poll缺点
因为poll也是需要遍历fd,当文件描述符多的时候效率依旧不高
epoll
epoll_create
1、作用:创建一个epoll句柄,返回的文件描述符来调用epoll其他接口函数
2、参数可以省略
3、当不使用之后,需要使用close来关闭
epoll_ctl
epoll事件注册函数
结构体epoll_data中不能存储一个有效的fd之后,有存储一个有效的ptr
epoll_wait
返回已经就绪的fd和事件
epoll原理
当epoll_create之后, 内核会创建一个结构体eventpoll,这个结构体中有两个成员非常关键
第一个是红黑树,注册的fd和事件就存放在里面
第二个是一个双链表,epoll_wait返回就绪的事件存放在其中
每个添加到epoll的事件都有与设备(网卡)驱动的回调函数,当相应事件发生时,就会触发这个回调函数,函数会把就绪事件挂到双链表中
所以判断是否有事件就绪,就只需要判断双链表是否为空
epoll的优点
1、检查就绪O(1),获取就绪O(N)
2、fd和event没有上限
3、返回值n,就是就绪的事件个数
水平触发和边缘触发
LT(Level Triggered) 水平触发
epoll默认下就是LT,当有事件就绪时,可以不处理或者只处理一部分
比如当读了一部分数据,但是还有一部分在缓冲区,下次调用epoll_wait时,也会立刻通知事件就绪,直到缓冲区数据被读完,epoll_wait才不会立即返回事件就绪
支持阻塞读写和非阻塞读写
ET(Edge Triggered)边缘触发
事件就绪时,必须立即处理
与上述例子相同,如果缓冲区数据没有读完,下次epoll_wait就不会再返回了,事件就绪之后,只有一次处理机会
只支持非阻塞读写
select和poll在工作模式下是LT,epoll两种方法都支持
这里为什么ET不支持阻塞读写?
如下图,如果没有读完数据,那么就需要再把数据读出来,才会对客户端进行应答,但是这里的问题是ET模式下认为资源没有就绪,所以Server端就会等Client再发消息,但是Client一直在等Server端的应答
LT和ET对比
1、LT是默认行为,ET减少epoll触发次数,但是每次就绪就必须把数据处理完
2、ET的逻辑更为复杂
惊群效应
在多线程下epoll可能惊群效应,惊群效应产生原因?导致什么结果?
在多线程或者多进程条件下,为了提高程序稳定性,会让多个线程或者多个进程同时在epoll_wait监听socket的描述符。当接收到一个新链接请求时,操作系统不知道把这个新链接交给那个线程,所以就会把所有线程唤醒。
但是只有一个线程会来处理这个链接,那么其他的线程都会失败,并且把errno设置为EAGAIN,这种现象就是惊群效应
这种导致资源额外开销和性能的损失
解决办法
1、唤醒部分子进程,仍然只有一个子进程处理链接,其他失败捕获EAGAIN错误,并无视
2、保证永远只有一个子进程在监听socket上的epoll_wait,主要思路就是申请一个全局锁
但是当链接数占链接总数大部分之后,就不再加锁,每个子进程专注处理已经连接好的事件请求