几种典型的IO模型
- 读写文件 read/write/fread/fwrite
- 网络接收与发送 send/recv/sendto/recvfrom
- 上述两种场景都有一个共同点,就是最终都会和操作系统打交道
- 等待数据
- 拷贝数据到用户空间
阻塞IO
1.当程序员在代码当中调用一个IO接口,如果内核还没有将数据准备好,IO接口就会阻塞等待,把这种IO的过程称之为阻塞IO
2. IO调用的返回,预示着一定拿到了想要的数据
![](https://img-blog.csdnimg.cn/20210422230128589.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
非阻塞IO
当程序员在代码当中发起一个非阻塞IO调用,本质上就是判断IO数据是否准备完毕
- 准备完毕:直接拷贝,IO调用返回
- 没准备好:直接返回(因为实际想要拷贝数据的操作,并没有完成)搭配循环使用(采用轮询的方式),直到数据拷贝完毕
![](https://img-blog.csdnimg.cn/20210422230148491.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
阻塞vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
信号驱动IO
信号驱动IO就是在内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
![](https://img-blog.csdnimg.cn/20210422230252709.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
异步IO
异步IO是指数据的等待和数据的拷贝都是由内核来完成的,程序员在异步调用成功之后,可以直接操作拷贝好的数据。
![](https://img-blog.csdnimg.cn/20210422230318922.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
aio_read 函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间。让IO更高效, 最核心的办法就是让等待的时间尽量少。
同步通信 vs 异步通信
同步和异步关注的是消息通信机制。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了,换句话说,就是由调用者主动等待这个调用的结果。
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果,换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
另外,在表明多进程多线程的时候,也提到同步和互斥,这里的同步通信和进程之间的同步是完全不相干的概念。
进程/线程同步也是进程/线程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候。
多路转接IO模型(多路复用)
多路转接的作用——可以监控多个文件描述符,当文件描述符当中有事件(读,写,异常)产生的时候,则通知调用者。
多路转接IO模型之slelect
/* According to POSIX.1-2001 */
#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:
struct timeval{
long tv_sec;/*seconds秒*/
long tv_usec;/*microseconds微秒*/
- 阻塞监控:传递的参数位 NULL
- 非阻塞监控:传递的参数为 0
eg: tv_sec!= 0 && ty_usec!= 0
eg: tv_sec!= 0 || ty_usec!= 0
- 在超时时间范围内,是阻塞监控,如果监控的文件描述符对应的事件发生,则返回;超过超时时间还没有文件描述符对应的事件产生,则select也会返回
返回值:
- 大于0:监控成功了,返回的数字为有事件产生的文件描述符的个数也就是就绪的文件描述符个数
- 等于0:监控超时
- 小于0:监控出错
关于fd_set
1. d_set是一个结构体,这个结构体内部是一个数组,而数组的元素类型为long类型
2. fd_set在使用的时候并不是按照数组的方式来进行使用的,而是按照位图的方式来进行使用的
3. select接口中的readfds、writefds、exceptfds参数都是fd_set类型的,在Linux源码中fd_set表示如下:
![](https://img-blog.csdnimg.cn/20210422230701202.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
fd_set事件集合位图的使用
fd_set事件集合分为读时间集合、写事件集合和异常事件集合,不同类型的集合代表着对该集合中文件描述符相应事件的关心,例如读事件集合,会关心相应的文件描述符的读事件。
1. select 最大监控的文件描述符的数量为1024个,对应监控的文件描述符数值的范围为:[0,1023]
2. 如果关心某个文件描述符的某个事件,则将文件描述符”添加”到对应的事件集合当中。添加的含义:将文件描述符对应的比特位置为1,则表示关心该文件描述符发生什么事件。
操作fd_set位图的接口
void FD_CLR(int fd, fd_set *set);
功能:将fd文件描述符,从事件结合set当中删除掉
本质:将fd文件描述符对应的比特位置为0
int FD_ISSET(int fd, fd_set *set);
功能:判断fd是否存在在事件集合set当中
本质:判断fd文件描述符对应的比特位是否为1
返回值:
如果为0,则不在
如果为1,则存在
void FD_SET(int fd, fd_set *set);
功能:将文件描述符fd,设置到set事件集合当中
本质:将fd文件描述符对应的比特位置为1
void FD_ZERO(fd_set *set);
功能:将事件结合set清空
本质:将所有的比特位置为0
总结
当我们在调用上述接口操作事件集合的时候,本质上是在执行自己写的代码,说明当前执行流在用户空间,当我们调用select监控事件集合的时候,需要将事件集合拷贝到内核,让内核进行监控。
select监控成功之后,事件集合的文件描述符的变化。
![](https://img-blog.csdnimg.cn/20210422230934862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
select_tcp程序&select的优缺点:
- 优点
- 可以跨平台,因为遵守的是Posix标准
- 超时的监控时间可以精确到微妙
2. 缺点
- 监控的文件描述符的数量最大是1024,取决于内核当中的__FD_SETSIZE宏
- select采用轮询遍历事件集合的方式,所以,随着监控文件描述符的增加,性能会下降
- select的事件集合,在监控的时候,需要从用户空间拷贝到内核空间
- select在监控成功之后,会将未就绪的文件描述符对应的比特位置为0,不方便下次监控
多路转接IO模型之poll
poll和select相比,没有了select跨平台的优势,除此之外,poll和后面要提到的epoll相比,没有epoll性能高。
相关接口
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
参数:
- fds:待要监控的事件结构数组
struct pollfd{
int fd;/*file descriptor*/待要监控的文件描述符
short events;/*requested events*/关心文件描述符的什么事件
指定为POLLIN表示可读事件
指定位POLLOUT表示可写事件
如果关注的是可读并且可写事件,则
需将POLLIN和POLLOUT使用按位或的方式连接起来
eg:POLLIN | POLLOUT
short revents;/*returned events*/
保存poll监控该文件描述符成功之后,该文件描述符产生的事件poll函数
会对revents进行初始化,在传递参数的时候,程序员可以不用关心
};
2. nfds:数组有效元素的个数,本质上也是告诉poll函数,监控的范围
3. timeout:
- <0:阻塞监控
- ==0:非阻塞监控
- >0:带有超时时间的监控方式,单位是毫秒
返回值:
- >0:表示就绪的文件描述符的个数
- ==0:超时
- <0:表示监控出错
poll的优缺点:
- 优点
- 采用了事件结构的方式,不用针对文件描述符的每一种事件进行分类监控。
- 要监控多少文件描述符,就定义多大的事件结构数组,数组的每一个元素的类型都是一个事件结构,可以对应一个文件描述符,并且将文件描述符关心的事件也填充进去了
2. 缺点:
- 不能跨平台
- 同样采用轮询遍历的方式,随着文件描述符的增多,轮询效率就会下降(监控效率也就会下降)
- 同样也是需要将事件结构数组从用户空间拷贝到内核空间,监控成功之后还需要从内核空间拷贝到用户空间
多路转接IO模型之epoll
epoll是当今公认的在linux操作系统下,性能最高的多路转接IO模型。
相关接口
1. 创建epoll句柄
#include <sys/epoll.h>
int epoll_create(int size);
作用:
创建epoll的操作句柄,在内核当中会创建一个eventpol结构体。
参数:
size:目前的内核版本当中这个size是毫无意义的,size定义了epoll维护的结构体的大小,epoll现在采用扩容的方式。(传参的时候只要不传递负数就行)
返回值:
返回了epol的操作句柄。
2. 添加/删除/修改事件结构
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
作用:
向内核维护的红黑树当中添加或删除或修改事件结构。
参数:
- epfd:也就是epoll_create的返回值,epoll的操作句柄
- op(options):告知epoll_cl函数做什么操作
- 添加:EPOLL_CTL_ADD
- 删除:EPOLL_CTL_DEL
- 修改:EPOLL_CTL_MOD
3. fd:待操作的文件描述符
4. event:事件结构
![](https://img-blog.csdnimg.cn/20210422231537248.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
联合体epoll_data中的fd——保存的监控的文件描述符,是为了监控成功之后,让我们知道从双向链表当中获取的事件结构是属于哪一个文件描述符的,从而可以针对文件描述符已经产生的事件进行处理。
使用epoll_data_t的方式有两种:
a. 使用fd,但是不使用ptr。如果使用了fd,还使用ptr,ptr当中的值会覆盖fd的值,导致后续找不到该事件结构对应哪一个文件描述符。
b. 使用ptr,但是不使用fd注意:ptr的类型为void*,接收一个地址在传递给ptr的类型当中一定要包含一个字段为文件描述符,换言之,就是要传递一个结构体。
3. 监控
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout;
参数:
- epfd:epoll的操作句柄
- events:事件结构数组
作用:从双向链表当中拷贝的事件结构放到events当中
例子:如果就绪了30个文件描述符,引申含义就是双向链表当中有30个事件结构
第一种:准备的事件结构数组大小大于就绪的文件描述符的个数
eg: struct epoll_event arr[50]
第二种:准备的事件结构数组大小等于就绪的文件描述符的个数
第三种:准备的事件结构数组大小小于就绪的文件描述符的个数
eg:struct epoll event arr[10];
3. maxevents:告知epoll,当前最多能拷贝多少个就绪的事件结构到用户准备的事件结构数组当中,防止越界
4. timeout:
<0:阻塞监控
==0:非阻塞监控
>0:带有超时时间的监控方式,单位是毫秒
返回值:
>0:表示就绪的文件描述符的个数
==0:超时
<0:表示监控出错
epoll工作原理
![](https://img-blog.csdnimg.cn/20210422231943979.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzczNzI1OQ==,size_16,color_FFFFFF,t_70)
- epoll_create创建epol操作句柄,相当于在内核当中创建了一个struct eventpoll...}的结构体。
- epoll操作句柄就是用户用来操作eventpoll结构体的“钥匙”。
- 添加/删除/修改事件结构都是针对于这个红黑树而言的,意味着红黑树的每个节点都是一个事件结构,也就是epitem结构体。
- epoll监控是在遍历红黑树,红黑树的遍历效率是O(lgn),n为红黑树的高度。
socket就绪条件
读就绪
1. socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
2. socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
3. 监听的socket上有新的连接请求;
4. socket上有未处理的错误.
写就绪
1. socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
2. socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
3. socket使用非阻塞connect连接成功或失败之后;
4. socket上有未读取的错误.
LT:水平触发
如果文件描述符有事件产生,epoll就会一直进行通知,直到程序员将该文件描述符所对应的事件处理掉
就好比妈妈在弟弟打游戏时催他吃饭,如果他没有立即放下手机去吃饭,妈妈会时不时地过来催一下
ET:边缘触发
如果文件描述符有事件产生,只会通知一次。直到新的数据或者新连接到来才会再次触发
就好比爸爸在弟弟打游戏时催他吃饭没如果他没有立即放下手机去吃饭,如果他没有立即放下手机去吃饭,爸爸会再下一顿饭做好的时候再过来催他
epoll的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适, 具体要根据需求和场景特点来决定使用哪种IO模型
epoll的优点
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回,直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响
- 没有数量限制: 文件描述符数目无上限