AIO、BIO、NIO、IO多路复用

目录

AIO、BIO、NIO、IO多路复用

引言

相关概念;

BIO(Blocking IO)称之为同步阻塞IO

NIO(Non-Blocking IO) 同步非阻塞IO

IO多路复用

select实现的IO多路复用

poll实现的IO多路复用

epoll实现的IO多路复用

边缘触发和水平触发:

AIO( Asynchronous I/O)异步非阻塞IO

大总结


AIO、BIO、NIO、IO多路复用

引言

对于AIO、BIO、NIO主要用于网络IO即从网卡传输数据到用户进程。有以下四个主要步骤:

  1. 用户进程执行系统调用转入内核态
  2. 操作系统等待远处客户端发送数据(前提是客户端和服务器通过TCP三次握手成功),客户端发送数据后,操作系统通过从网卡设备获取数据,并把数据从Socket协议栈拷贝到内核缓冲区(需要等待Socket的数据到达)阻塞
  3. 把内核缓冲区的数据拷贝到用户缓冲区(真正拷贝过程)阻塞
  4. 用户进程获取到数据,继续执行

想了解BIO(同步阻塞IO),NIO(同步非阻塞IO),AIO(异步非阻塞IO)最好先知道同步与异步、阻塞与非阻塞的概念。(如果不太理解可以可以先跳过,结合后面的图片去理解,再回过头来看

  • 同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
  • 异步:异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
  • 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是当前函数没有返回而已。
  • 非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

小总结:同步异步IO、阻塞非阻塞IO概念很相似,但你可以这么区分:同步异步是IO两个阻塞流程中是否有一个阻塞了。阻塞和非阻塞:IO第二个步骤(用户进程等待Socket资源到达)这个步骤是否阻塞。

相关概念;

用户态:程序只能访问自己的内存空间和受限的系统资源,无法直接访问操作系统的核心功能和硬件设备。

内核态:指程序在执行时所处的较高特权级别。可以直接访问和操作操作系统的核心功能和硬件资源。

上下文切换:一次系统调用过程中的,会发生两次 CPU 上下文切换。保存用户态的寄存器(代码段,数据段,保存参数的寄存器)上下文切换的目的是保存线程的执行数据和状态,每次都要保存和恢复线程。

第一次 CPU 上下文切换是从用户态切换到内核态:CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。

第二次 CPU 上下文切换是从内核态切换到用户态: CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。

进入正题

有了这些概念现在让我们来了解他们三兄弟

BIO(Blocking IO)称之为同步阻塞IO

对于每个IO都需要一个单独的线程去处理,期间线程处于阻塞状态,如果有大量的IO请求,线程数比较多,会造成频繁的上下文切换,造成较大的开销。

NIO(Non-Blocking IO) 同步非阻塞IO

与BIO不同的是:在NIO模式下,当用户进程执行系统调用后,如果当前数据还没有准备好,则会立即返回,用户进程会一直轮询,判断数据是否准备好,当轮询到数据准备好了,会进入到阻塞状态,将数据从内核拷贝过来直到拷贝结束。这种方式就有点傻傻的一直占用CPU资源去轮询。从而引出了IO多路复用的技术(后续)。

相比BIO,NIO可以一个线程去处理多个客户端Socket请求,当轮询到请求到达时再去处理,但是轮询也不是什么好事,会一直空转占用CPU资源。

IO多路复用

(把它放在这因为我觉得他属于同步IO,会更好理解)

select实现的IO多路复用

select将已经连接的Socket放进一个文件描述符集合中,然后将集合拷贝到内核中,让内核遍历一遍集合,修改就绪Socket的标志位。内核再将集合拷贝回用户进程,用户进程再遍历一遍,得知哪些Socket就绪后再去对其进行read OR write。所以select实现的IO多路复用需要拷贝和遍历 2 次文件描述符集合,同时其实现的文件描述符集合在内核中的大小是有限制的,最大为1024。

poll实现的IO多路复用

poll实现的IO多路复用与select实现的有很多相似之处,可以理解为poll是对select的升级。

poll一样是把文件描述符数组拷贝到内核(用户进程进入阻塞),在内核中转换成链表,内核对其进行遍历,如果有Socket就绪,就把就绪的Socket放入Socket就绪列表。如果内核发现没有Socket就绪就继续阻塞,直到有Socket到达,唤醒用户进程,进行后续的read OR wirte。

小总结:可以看到select和poll非常相似,只是使用的文件描述符集合不同,poll使用链表就没有1024这个大小限制。二者都需要通过遍历来判断Socket是否到达。二者的效率会随着文件描述符的增多而降低。

epoll实现的IO多路复用

epoll可以理解为是IO多路复用的终极版本。他通过红黑树存储所有待检测的文件描述符,通过维护一个就绪链表保存所有就绪的文件描述符。解决了select、poll的问题。

epoll_create在内核建立红黑树,将所有Socket文件描述符放到红黑树上(一次拷贝),之后调用epoll_ctl函数向红黑树中add文件描述符,同时会在内核中注册一个该文件描述符的回调函数,当内核检测到该文件描述符就绪,就会调用该回调函数,将其放入就绪链表。当用户进程调用epoll_wait函数就会把就绪链表拷贝会用户进程(少量且都是就绪的)。

从上面的调用方式就可以看到epoll比select/poll的优越之处:

  • epoll同poll一样,也没有文件句柄的数量限制。
  • select/poll每次系统调用时,都要传递所有监控的socket给内核缓冲区,如果有数以万计的Socket文件句柄,意味着每次都要copy几十几百KB的内存到内核态,非常低效。epoll不需要每次都将Socket文件句柄从用户态拷贝到内核态,在执行epoll_create时已经在内核建立了epoll句柄,每次调用epoll_ctl只是在往内核的数据结构里加入新的socket句柄,所以不需要每次都重新复制一次。
  • select 和 poll 都是主动轮询,select在内核态轮询所有的fd_set来判断有没有就绪的文件句柄,poll 轮询链表判断有没有就绪的文件句柄,而epoll是被动触发方式。epoll_ctl执行add动作时除了将Socket文件句柄放到红黑树上之外,还向内核注册了该文件句柄的回调函数,当Socket就绪时,则调用该回调函数将文件句柄放到就绪链表,epoll_wait只监控就绪链表就可以判断有没有事件发生了。

边缘触发和水平触发:

官方版本:

边缘触发:使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;

水平触发:使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

我的理解:水平触发:你女朋友叫你带瓶酱油,在你带回来之前会不停跟你絮叨,直到你带回来为止。

边缘触发:你的女朋友只会告诉你一遍,她就会默认为你会把她带回来。(只会触发一次)

相比之下,边缘触发最大程度上减少epoll_wait调用的次数,因此效率会比水平触发高(因为该函数的调用也涉及到上下文切换)。但是边缘触发必须跟非阻塞IO搭配,避免因为阻塞的read OR wirte 把其他文件描述符的读写任务饿死。

注:select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

补充:epoll实现的多路复用中,调用了epoll_creat进入内核后,用户进程没有阻塞,因为还需要调用epoll_ctl、epoll_wait函数来处理新来的和就绪的Socket,这块与select和poll不同,但当执行read和write是三兄弟都会进入阻塞状态,可以理解epoll为同步非阻塞,select和poll为同步阻塞。

AIO( Asynchronous I/O)异步非阻塞IO

异步IO的用户进程不会进入阻塞状态,用户进程会把Socket文件描述符丢给内核,并注册回调函数,当Socket资源到达会直接调用read OR wirte进行拷贝,完成后通知用户进程。整个过程中用户进程都处于非阻塞状态。

大总结

1、IO可以分为两个主要阶段:

  • 第一阶段,判断有没有事件发生(或判断数据有没有准备好,或判断Socket是否就绪)
  • 第二阶段,在数据准备好以后,执行真正的读(或写)操作,将数据从内核空间拷贝到用户空间。
  • 通过第一个阶段是否阻塞判断是阻塞IO还是非阻塞IO,通过两个阶段是否有阻塞判断是同步IO还是异步IO。

2、BIO在Socket多的情况下,线程数就会很多,因为BIO一个任务分配一个线程,大量线程处于阻塞,且会导致频繁的上下文切换,导致较大的性能开销。

3、NIO在系统调用后会一直轮询Socket资源是否到达,所以NIO可以做到一个线程处理多个SOcket请求,但一直占用CPU资源,当Socket连接数量多时也会导致频繁的上下文切换(重复从用户进程到内核去判断数据有没有准备好)。所以引出了IO多路复用的技术。

4、IO多路复用:单个线程能监听多个客户端Socket请求,当请求资源到达时内核通过回调函数告知用户线程去处理。

  • select、poll通过拷贝整个文件描述符集合到内核,内核去判断文件描述符是否就绪,再拷贝回用户进程遍历有去进行read OR write,select在内核中的集合被限制为1024的大小,而poll在内核中转换为链表就没有了1024的限制。单二者都需要多次拷贝和遍历文件描述符集合,效率比较低。
  • epoll使用了红黑树和事件驱动机制,通过epoll_creat创建红黑树,通过epoll_ctl添加后续新的Socket并在内核中注册回调函数,回调函数会将就绪Socket加入到就绪链表,当用户线程调用epoll_wait会返回就绪链表。epoll红黑树数据结构遍历上时间复杂度为o(lgn)优于select、poll的o(n),同时减少大集合的拷贝次数,新来的只需要通过ctl添加到红黑树,wait函数返回的也是就绪的Socket。所以epoll时select和poll的 老大哥。
  • epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

5、AIO就是用户进程将文件描述符交给内核后就等着内核告诉自己拷贝完了。自己去干别的事。

小声哔哔:第一次写文章,有很多不足,期待大家的建议和批评。

【参考文档】

  1. https://www.cnblogs.com/Mr-shen/p/12832501.html
  2. https://www.zhihu.com/question/20122137
  3. https://www.cnblogs.com/pluto-yang/p/12546942.html
  4. https://www.cnblogs.com/sunsky303/p/8962628.html
  5. https://blog.csdn.net/qq_22121229/article/details/103101191
  6. https://www.cnblogs.com/pugang/p/12823108.html
  7. https://blog.csdn.net/qq_33330687/article/details/81558198
  8. https://www.cnblogs.com/ljbkyBlog/p/10190576.html
  9. https://zhuanlan.zhihu.com/p/121651179
  10. https://zhuanlan.zhihu.com/p/260450151
  11. https://zhuanlan.zhihu.com/p/272891398
  12. https://zhuanlan.zhihu.com/p/159357381
  13. 详解磁盘IO、网络IO、零拷贝IO、BIO、NIO、AIO、IO多路复用(select、poll、epoll)_io分为网络io-CSDN博客
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值