IO模型
五种IO
模型:
- 阻塞IO(
BIO
) - 非阻塞IO(
NIO
) - 信号驱动IO(
signal-driven I/O
) - 异步IO(
AIO
) - 多路转接(复用)IO(
I/O multiplexing
)
IO操作分了两个过程:等待 + 数据拷贝。
- 阻塞
IO
:发起IO
调用,若不具备IO
条件,则等待IO
条件具备。具备则数据拷贝完毕后返回。一直等待资源浪费。 - 非阻塞
IO
:发起IO
调用,若不具备条件则立即报错返回。通常是循环发起调用,若具备IO
条件,则拷贝数据完毕后返回。不够实时。 - 信号驱动
IO
:先定义IO
信号处理方式,若IO
条件具备,直接信号通知进程,发起调用,拷贝数据后返回。比较实时。但流程控制较难。也是一种异步,因为拷贝就是异步的。 - 异步
IO
:定义信号处理,发起异步IO
调用,自己直接返回,之后让别人等待条件具备(等待和数据拷贝都不用自己完成,进程或线程完成)“事情由别人干,干完通知我”。
IO
条件具备后,数据拷贝由别人完成,信号通知进程:IO
已经完成。可以对数据直接进行操作。(AIO
) - 多路转接
IO
:一种IO
事件监控。同时对大量的描述符进行事件(描述符的可读/可写/异常)默认阻塞监控,监控描述符是否具备IO
条件。
如果具备(就绪时)进行返回,对就绪的IO
进行操作。是高并发的处理模型。
多路转接模型:(select
、poll
、epoll
)都是实现对大量描述符进行事件监控的操作。
select
模型:
【通过对几个事件集合中的描述符进行各自的事件监控,当对应集合中有描述符事件就绪则返回,返回前集合中没有就绪的描述符则移除。否则超时返回】
int select( //阻塞函数
int maxfds ,
fd_set* readfds ,
fd_set* writefds ,
fd_set* exceptfds ,
struct timeval* timeout
);
maxfds
:监控的描述符中,最大的描述符 + 1fd_set
:描述符集合,本质是一个位图(bitmap
),位图大小取决于一个宏:__FD_SETSIZE
=1024
,即最多监控1024
个。readfds
:监控读事件集合。writefds
:监控写事件集合。exceptdfs
:监控异常事件集合。struct timeval
:分为秒与微秒。timeout
:是select
等待超时时间。
步骤:
- 定义描述符集合
fd_set
。 - 将集合拷贝到内核进行监控,监控的原理是对所有描述符进行轮询遍历状态(这个监控是阻塞的)
- 当有描述符就绪时,在调用返回之前,将集合中没有就绪的描述符剔除出去。
- 用户操作:对所有的描述符进行遍历,看集合还剩余什么描述符,则这些描述符已经就绪。
接口:
void FD_CLR(int fd,fd_set *set); //将指定的描述符从集合中移除
void FD_ISSET(int fd,fd_set *set); //判断制定的描述符是否在集合中
void FD_SET(int fd,fd_set *set); //将指定的描述符添加到监控集合中
void FD_ZERO(fd_set *set); //清空描述符集合
select
优缺点分析:
- 优点:
- 遵循
POSIX
标准,可以跨平台。(移植性强) select
监控的超时等待时间更加精细。(微秒级别)
- 缺点:
select
所能监控的描述符是有上限的,Linux
下默认1024,取决于__FD_SETSIZE
。select
实现监控原理是在内核中进行轮询遍历状态,因此性能会跟着描述符增多而下降。select
监控每次返回时都会修改监控集合,需要用户每次监控前重新添加描述符到集合中。select
要监控的集合中的描述符数据,需要每次都重新向内核中拷贝。select
不会直接告诉用户哪一个描述符事件就绪,只是告诉用户有就绪事件,需要用户遍历查找。(会增加代码复杂度)
poll
模型:
【poll
采用了一个描述符事件结构的方式对描述符所关心的事件进行监控,相对于select
不再需要创建多个事件集合的遍历方式了】
int poll(
struct pollfd* fds ,
nfds_t nfds ,
int timeout
);
监控实现原理:
- 用户定义事件数组,对描述符可以添加用户关心的事件到结构体数组中,进行监控。
(POLLIN
读事件、POLLOUT
写事件)
struct pollfd{
int fd; //用户关心监控的 文件描述符
short events; //保存用户 关心 的事件
short revents; //保存当前 就绪 的事件
};
- 内核:
-
poll
实现监控的原理也是将数据拷贝到内核,然后进行轮询遍历监控,若有描述符就绪,则返回。其性能随着描述符的增多而下降。
-
- 若有描述符就绪,则修改这个相应描述符事件结构中的实际就绪事件。
poll
也不会直接告诉用户哪一个描述符事件就绪,只是告诉用户有就绪事件,需要用户遍历查找。根据返回的revents
判断哪一个事件就绪,查看实际发生的事件是否是自己关心的事件,来查找就绪,进而进行操作。
- 优点:
- 采用事件结构的方式对描述符进行监控,简化了多个事件集合的监控方式。
- 描述符的具体监控无上限。
- 缺点:
- 不能跨平台。所以
poll
已经逐渐在历史舞台上淡出。 - poll采用轮询遍历判断就绪,性能随着描述符增多而性能下降。
poll
也不会告诉用户具体就绪的描述符,需要用户进行轮询判断。性能也会随着描述符增多而下降,代码复杂度较高。
epoll
模型:
Linux
下性能最高的IO
多路转接模型,也是采用事件结构的形式对描述符进行监控。
头文件<sys/epoll.h>
- 就绪:
对于可读事件来说,有数据到来就是读就绪
。接收缓冲区中的数据大小大于低水位标记(1Bit
),就会触发可读事件。可读事件就绪。
对于可写事件来说,缓冲区有空闲空间就是写就绪
。发送缓冲区中的空闲空间大小大于低水位标记(1Bit
),就会触发可写事件。可写事件就绪
创建epoll
接口:
int epoll_create(int size); //创建epoll
size
:自从Linux 2.6.8
之后,size
参数就被忽略了,但必须大于0
。以后的版本以一种动态增长的方式进行变化。- 返回值:成功返回非负的文件描述符,当然文件描述符也是一个非负整数,他就是
epoll
的操作句柄。
功能:在内核中创建一个eventpoll
结构体,其中包括{
struct eventpoll{
rbr //(红黑树)
rdlist //(双向链表)
};
接口:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//通过这个接口告诉操作系统:在哪一个epoll中,添加/移除/修改,哪一个描述符,所对应的监控事件
//通过参数解释:向内核中 epfd 对应的epoll结构,进行添加删除修改,一个 fd 描述符,对应关心的 event 事件
epfd
:epollcreate()
接口返回的epoll
操作句柄。op
:用户要进行的操作。
EPOLL_CTL_ADD 向内核的eventpoll中添加要监控的事件结构
EPOLL_CTL_DEL 从内核的eventpoll中移除要监控的事件结构
EPOLL_CTL_MOD 修改内核中监控的事件结构
fd
:用户所要监控的描述符event
:描述符对应所监控的事件- 返回值:成功返回
0
,失败返回-1
。
这里的event
参数就是下面的epoll_event
结构体:
typedef union epoll_data{
void *ptr; //给定一个指针就可以包含任意大小与类型的数据了
int fd; //事件结构中携带的这个描述符数据,当事件在内核中就绪时,OS会返回给用户事件结体
//用户可以获得这个数据,若这个数据就是关心的描述符,就可以直接操作了
//不再需要 遍历 了
//但也有可能不是用户关心的描述符。
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event{
uint32_t events; // Epoll events 用户对描述符关心的事件 其中包括:
//可读 EPOLLIN
//可写 EPOLLOUT
//边缘触发 EPOLLET
//水平触发 EPOLLLT
epoll_data_t data; // User data variable 事件对应的数据
};
epoll
在内核中保存的事件结构不需要用户每次重新拷贝,只拷贝一次即可。
功能:向内核的eventpoll
结构中添加 / 移除 / 修改所监控的事件结构。
接口:
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
epfd
:epoll
操作句柄event
:事件结构体数组,用于保存就绪的描述符对应事件。maxevent
:告诉内核数组这个数组的大小,最多一次性获取就绪事件个数,防止event
数组溢出。timeout
:超时时间,精度为毫秒。- 返回值:
<0
:出错
== 0
:超时等待
>0
:就绪的事件个数
流程:
- 接口告诉内核要开始对描述符进行监控了。(异步)
-
- 操作系统:对描述符进行监控,采用的是事件触发方式进行监控,为每一个要监控的描述符都定义了一个事件,并且对这个事件定义一个事件回调函数。
ep_poll_callback();
- 操作系统:对描述符进行监控,采用的是事件触发方式进行监控,为每一个要监控的描述符都定义了一个事件,并且对这个事件定义一个事件回调函数。
-
- 这个事件回调函数做的事情为:将就绪的这个描述符所对应的
epoll_event
事件结构添加到双向链表rdlist
中。
- 这个事件回调函数做的事情为:将就绪的这个描述符所对应的
epoll_wait
并没有立即返回,而是每隔一段时间检查一次,内核中的eventpoll
中双向链表是否为空。进而判断是否有描述符就绪。- 若链表不为空,表示有描述符就绪,则
epoll_wait
即将返回,返回之前将就绪的描述符对应事件结构向用户态结构体数组拷贝一份(这个结构体数组就是epoll_wait
的第二个参数)
(链表中保存的都是就绪的描述符对应的事件结构) - 将就绪的描述符对应事件拷贝一份到用户态,直接告诉用户有哪些描述符就绪,用户可以直接操作就绪的描述符。
struct eventpoll{
rbr; //红黑树:保存用户添加的事件结构结点。
rdlist; //双向链表
};
使用红黑树是因为它的特性:
红黑树去重快
,可以添加重复结点快速检测出。
使用C++封装Epoll类,实现简单的调用接口
【 https://blog.csdn.net/qq_42351880/article/details/92820142 】
- 阻塞:为了完成功能发起调用,若当前不具备完成条件,则一直等待直到完成后调用返回。
- 非阻塞:为了完成功能发起调用,若当前不具备完成条件,直接报错返回,通常需要循环处理。
阻塞与非阻塞区别:发起调用后是否立即返回。
- 同步:为了完成功能发起调用,若当前不具备完成条件,则自己等待完成功能后返回。同步通常都是阻-塞的。
- 异步:为了完成功能发起调用,但功能由别人完成。包含异步阻塞 / 异步非阻塞。
- 异步阻塞操作:操作由操作系统完成,但是自己等待别人(OS)完成条件。
- 异步非阻塞操作:操作由操作系统完成,但是不等待别人完成操作,立即返回。操作系统完成操作后通过信号通知进程。
同步与异步区别:功能是否由自己完成。
同步与异步的优缺点对比:
同步流程控制简单,但效率较低。
异步流程控制较难,消耗资源少、效率较高。
Linux下的异步IO:
aio
(用于大量文件读写)