一、IO模型有哪些?
1 阻塞IO
顾名思义,阻塞IO就是两个阶段都必须阻塞等待:
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
阻塞IO模型中,用户进程在两个阶段都是阻塞状态。
2 非阻塞IO
非阻塞IO的recvfrom 操作会立刻返回结果,而不是阻塞用户进程
阶段一:
- 用户进程调用revefrom尝试读取数据 (比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到error后,再次尝试读取
- 循环往复,直到数据就绪
阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
而在单线程情况下,只能依次处理IO事件,如果正在处理的IO事件恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有IO事件都必须等待,性能自然会很差
提高效率的方法有哪些?
- 增加线程
增加线程可以提高效率,但在IO操作很多的情况下,多线程依然处理不过来
- 不排队,哪个IO就绪就处理哪个
3. IO多路复用(事件通知机制)
IO多路复用是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源 。 FD是什么?
阶段一:
- 用户进程调用select,指定要监听的FD集合
- 内核监听FD对应的多个socket
- 任意一个或多个socket数据就绪则返回readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找到就绪的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
3.1 selelct模式
第一阶段
- 用户进程调用select,指定要监听的FD集合
- 内核监听FD对应的多个socket
- 任意一个或多个socket数据就绪则返回readable
- 此过程中用户进程阻塞
第二阶段
- 用户进程找到就绪的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
监听一次的流程:
select模式存在的问题:
- 需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间
- select无法得知具体是哪个fd就绪,需要遍历整个fd_set
- fd_set监听的fd数量不能超过1024
3.2 poll模式
poll模式原理基本与select 相似,只画出主要步骤逻辑
•select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
•监听FD越多,每次遍历消耗时间也越久,性能反而会下降
3.3 epoll模式
epoll模式是对select和poll的改进,它提供了三个函数
int epoll_create(int size); // 创建一个epoll实例,内部是event poll,返回对应的句柄epfd
// eventpoll 的结构体
struct eventpoll {
struct rb_root rbr // 一颗红黑树,记录要监听的FD
struct list_head rdlist // 链表,记录就绪的FD
}
/*
添加一个Fd到epoll的监听列表rbr中,并自动设置一个callback函数
callback函数会在FD就绪时被触发
该函数会自动将对应的FD添加到eventpoll 的 rdlist (就绪列表)
*/
int epoll_ctl (
int epfd // epoll 实例句柄
int op // 操作,包括:增、改、删
int fd // 需要监听的Fd
struct epoll_event *event // 监听事件的类型,读、写、异常
)
// 检查relist列表是否为空,不为空就返回就绪FD的数量
int epoll_wait (
int epfd //epoll实例句柄
struct epoll_event *events //空event数组,用于接收就绪的FD
int maxevents //events 数组的最大长度
int timeout //超时时间
)
/*
补充说明:
用户进程调用epoll_wait函数时,它会进入阻塞状态,
等待已经注册到epoll中的文件描述符上发生事件。
当有事件发生时,内核会将相应的事件添加到内核缓冲区中,
并唤醒等待epoll_wait返回的用户进程,
然后epoll_wait函数返回就绪的文件描述符及其对应的事件类型,
用户进程可以通过这些就绪的文件描述符进行操作
*/
epoll执行示例图:
epoll流程图:
epoll 解决的问题:
- 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
- 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
- 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
3.4 总结一下
select模式
优势:
- select模式是最早的IO多路复用机制,可以在几乎所有的操作系统上运行。
- select支持文件描述符数量的限制比较大,一般能够支持1024个文件描述符。
不足:
- select每次都需要将所有的文件描述符集合从用户态复制到内核态,所以随着文件描述符数量的增加,性能会逐渐下降。
- select的复杂度比较高,需要遍历整个文件描述符集合来判断每个文件描述符是否有事件发生,这样的时间复杂度是O(n)的。
poll模式
优势:
- poll相比select,对文件描述符集合的大小没有限制。
- poll在内核中维护了一个链表,不需要遍历整个文件描述符集合,所以性能比select好。
不足:
- 每次调用poll时,需要将整个文件描述符集合从用户态复制到内核态,所以随着文件描述符数量的增加,性能也会下降。
epoll模式
优势:
- epoll只需要在用户态和内核态之间传递事件列表,而不是文件描述符集合,所以性能比select和poll都要好。
- epoll支持边缘触发和水平触发两种模式,可以更好地适应不同的场景。
不足:
- epoll只能运行在Linux系统上,不支持其他操作系统。
- epoll在实现上比较复杂,需要进行内核修改,所以可移植性比较差。
总的来说,select模式最适合文件描述符数量比较少的情况,poll模式可以适应更多的场景,而epoll模式在高并发、高吞吐量的情况下性能最好。
4 信号驱动IO
信号驱动IO是与内核建立SIGIO得信号 关联并设置回调,当讷河有FD就绪时,就会发出SIGIO信号通知用户进程,期间用户进程可以执行其他业务,无需阻塞等待。
阶段一
- 用户进程调用sigaction,注册信号处理函数
- 内核返回成功,开始监听FD
- 用户进程不阻塞等待,可以执行其他业务
- 当内核数据就绪后,回调用户进行得SIGIO处理函数
阶段二
- 收到SIGIO回调信号
- 调用recvfrom
- 内核将数据拷贝到用户空间
- 用户进程处理数据
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也比较低
5 异步IO
异步IO的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其他事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程
阶段一:
用户进程调用aio_read,创建信号回调函数
内核等待数据就绪
用户进程无需阻塞,可以做任何事情
阶段二:
内核数据就绪
内核数据拷贝到用户缓冲区
拷贝完成,内核递交信号触发aio_read中的回调函数
用户进程处理数据
异步IO模型中,用户进行在两个阶段都是非阻塞的,但是使用异步IO需要注意的是限制数量,否则内核资源被占用过多。
二、五种IO模型对比
阻塞式I/O模型
适用场景:当应用程序需要等待数据到达或发送完成时,将被阻塞直到数据可用或已发送完成。
示例:在服务器上,当使用套接字进行读取或写入操作时,如果没有可用的数据或目标套接字忙于处理其他数据,则应用程序将会被阻塞。
非阻塞式I/O模型
适用场景:当应用程序需要在等待数据到达或发送完成时继续执行其他任务时,可以使用非阻塞式I/O模型。
示例:当应用程序在等待一个套接字的读取或写入完成时,可以使用非阻塞I/O来继续执行其他任务。
I/O复用模型
适用场景:当应用程序需要同时处理多个套接字的读写操作时,可以使用I/O复用模型。
示例:使用select()或epoll()系统调用来同时监听多个套接字,以等待数据到达或可写。
信号驱动式I/O模型
适用场景:当应用程序需要等待一个特定的事件发生时,可以使用信号驱动式I/O模型。
示例:使用sigaction()系统调用来等待一个特定的信号,例如SIGIO,以指示某个套接字已准备好读取或写入。
异步I/O模型
适用场景:当应用程序需要处理大量的I/O操作,但又不想使用多线程或多进程来处理这些操作时,可以使用异步I/O模型。
示例:在Windows系统中,使用异步I/O操作和回调函数来处理大量的文件I/O操作。在Linux系统中,使用aio_*()系统调用来实现异步I/O操作。
补充五种IO模式适合的应用场景
- 阻塞 I/O:在阻塞 I/O 模型中,当一个线程执行 I/O 操作时,该线程会被阻塞直到 I/O 完成。阻塞 I/O 最适合用于单线程、单连接、低并发量的情况,例如处理简单的客户端请求。
- 非阻塞 I/O:在非阻塞 I/O 模型中,当一个线程执行 I/O 操作时,该线程会立即返回,而不会阻塞等待 I/O 操作完成。非阻塞 I/O 适合用于多线程、单连接、高并发量的情况,例如处理大量客户端请求。
- I/O 多路复用:在 I/O 多路复用模型中,一个线程可以同时处理多个 I/O 事件,而不需要阻塞等待每个 I/O 操作的完成。I/O 多路复用适合用于单线程、多连接、高并发量的情况,例如 Web 服务器。
- 信号驱动 I/O:在信号驱动 I/O 模型中,当 I/O 操作完成时,内核会向应用程序发送信号,应用程序在接收到信号后再进行处理。信号驱动 I/O 适合用于单线程、多连接、高并发量的情况,例如实时数据采集系统。
- 异步 I/O:在异步 I/O 模型中,I/O 操作的完成不需要等待应用程序的响应,应用程序会在操作完成后得到通知。异步 I/O 适合用于多线程、多连接、高并发量的情况,例如大规模的高并发数据库系统。
五种IO互相结合使用
在实际的系统设计中,往往需要将多种 I/O 模型结合使用,以充分发挥各种模型的优势,提高系统的性能。以下是一些常见的 I/O 模型结合使用的案例:
- 使用阻塞 I/O 和多线程结合:可以使用阻塞 I/O 来处理 I/O 操作,同时使用多线程来处理 CPU 密集型任务。这种结合方式可以充分发挥多核 CPU 的处理能力,提高系统的处理能力。
- 使用非阻塞 I/O 和多线程结合:可以使用非阻塞 I/O 来处理 I/O 操作,同时使用多线程来处理 CPU 密集型任务。这种结合方式可以避免了阻塞 I/O 的性能问题,同时可以提高系统的并发性能。
- 使用异步 I/O 和线程池结合:可以使用异步 I/O 来处理 I/O 操作,同时使用线程池来处理 CPU 密集型任务。这种结合方式可以避免了线程创建和销毁的开销,同时可以提高系统的并发性能。
- 使用 I/O 多路复用和定时器结合:可以使用 I/O 多路复用来监听多个连接的事件,同时使用定时器来处理定时任务。这种结合方式可以避免了轮询事件的开销,同时可以提高系统的定时精度。
- 使用信号驱动 I/O 和定时器结合:可以使用信号驱动 I/O 来处理 I/O 事件的通知,同时使用定时器来处理定时任务。这种结合方式可以避免了轮询事件的开销,同时可以提高系统的响应性能和定时精度。
需要注意的是,在使用多种 I/O 模型结合时,需要考虑到系统的复杂性和安全性问题,避免出现死锁、竞态条件等问题。同时,需要根据实际情况选择不同的 I/O 模型来优化系统性能。
拓展知识:
I/O 多路复用 和信号驱动IO 适用场景一致,该如何选择?
虽然 I/O 多路复用和信号驱动 I/O 有一些相似的应用场景,但它们之间还是有一些区别的。在选择使用哪种 I/O 模型时,需要根据具体的应用场景和需求来决定。
I/O 多路复用通常适用于单线程、多连接、高并发量的情况,例如 Web 服务器。使用 I/O 多路复用,可以在一个线程中同时处理多个连接的 I/O 事件,避免了线程切换和创建过多的线程的开销。但是,I/O 多路复用对于 I/O 事件的处理有一定的开销,在处理较少的连接时可能不太适合。
信号驱动 I/O 适用于单线程、多连接、高并发量的情况,例如实时数据采集系统。使用信号驱动 I/O,可以避免了在轮询 I/O 事件的过程中浪费 CPU 资源。但是,信号驱动 I/O 需要使用异步信号处理机制,这可能会带来一些复杂性和安全性问题。
因此,选择使用哪种 I/O 模型需要根据具体的应用场景和需求来决定。如果需要处理大量的连接和高并发量的情况,可以考虑使用 I/O 多路复用;如果需要实时采集数据或避免 CPU 浪费,可以考虑使用信号驱动 I/O。同时,在实际应用中,可以根据实际情况选择不同的 I/O 模型来优化系统性能。
三、拓展阅读
五种网络IO模型详解_后端_Linux服务器开发_InfoQ写作社区https://juejin.cn/post/6882984260672847879https://zhuanlan.zhihu.com/p/115912936https://cloud.tencent.com/developer/article/1684951
非阻塞IO和IO多路复用有什么区别和联系?
非阻塞I/O和I/O多路复用都是用于提高I/O性能的技术,它们之间有些许区别和联系:
区别:
- 非阻塞I/O指在进行I/O操作时,如果没有数据可读或可写,系统不会阻塞当前进程或线程,而是直接返回一个错误码,表示当前I/O操作无法立即完成。这种I/O模型需要使用非阻塞的系统调用来实现。
- I/O多路复用则是一种I/O模型,它通过在一个线程中同时监听多个I/O事件,可以在任意一个I/O事件就绪时通知应用程序进行处理,避免了多个I/O操作阻塞的情况。这种I/O模型通常使用select、poll或epoll等系统调用来实现。
联系:
- 非阻塞I/O和I/O多路复用都可以用于处理并发的I/O请求,提高系统性能和响应时间。
- 非阻塞I/O通常与轮询或异步I/O配合使用,而I/O多路复用使用的是事件驱动的方式,可以更高效地处理大量I/O事件。
- 在某些情况下,非阻塞I/O和I/O多路复用可以结合使用,例如在处理网络通信时,可以使用I/O多路复用来监听多个套接字,而对于每个就绪的套接字,可以使用非阻塞I/O进行读写操作。
总之,非阻塞I/O和I/O多路复用都是I/O处理中常用的技术,它们可以提高系统性能和响应时间,但具体使用哪种技术,取决于应用场景和需求。