• 五种常见的IO模型


    目录

    一. IO的概述

    1.1 什么是IO

    1.2 IO的效率问题

    1.3 同步IO和异步IO的概念

    二. 阻塞式IO

    三. 非阻塞式IO

    四. 信号驱动式IO

    五. IO多路复用

    六. 异步IO

    七. 总结


    一. IO的概述

    1.1 什么是IO

    IO,表示输入输出,即:InPut / OutPut,访问外部设备读取数据或者向外部设备写数据。常见的文件读写操作、网络通信操作,其实都是IO的过程。、

    在Linux操作系统下,我们认为一切皆是文件,只有操作系统有权利对文件进行读和写的操作,用户如果要读写文件,本质上都是使用操作系统的文件读写接口来实现的。

    这里以阻塞式读取为例,分析IO操作所进行的工作:

    • 当read/recv时,如果缓冲区中没有数据,那就阻塞式等待数据就绪 -- 等待。
    • 当read/recv时,如果缓冲区中有数据,那么就将数据从缓冲区拷贝到应用层 -- 拷贝。

    因此,我们可以认为,IO = 等待 + 拷贝。只要执行流(进程/线程)参与了等待和拷贝其中之一,那么就认为这个执行流进行了IO操作。

    图1.1展示了在进行文件写文件操作(input)时系统所进行的工作,可分为三步:

    1. 将文件的内容和属性信息加载到内存中去。
    2. CPU在内存中对文件内容进行修改。
    3. 将内存中被修改后的文件内容写回到内存中。
    图1.1 修改磁盘文件内容的步骤

    1.2 IO的效率问题

    由于IO要从外设读取数据或者向外设写数据,而在计算机体系中,硬件的效率由高到低的排序是:CPU寄存器 > 高速缓存 > 内存 > 磁盘 > 网卡,对于任意的IO操作,其实大部分时间都是在等待外设数据就绪,因此效率十分低下

    结论:由于IO操作消耗大量时间在等待数据就绪,因此IO的效率很低。

    在互联网行业中,提高IO效率是备受关注的,结合上面的分析,高效IO和低效IO的特点为:

    • 低效IO:单位时间内,等待时间长,拷贝数据时间短。
    • 高效IO:单位时间内,拷贝数据时间长,等待数据时间短。

    因此,设法降低等待时间的占比,是提高IO效率的关键所在

    1.3 同步IO和异步IO的概念

    在了解同步IO和异步IO之前,首先了解同步和异步的概念:

    • 同步:就是指一个调用发出后不直接返回,直到调用完成拿到结果才返回。即:发起调用的执行流(进程/线程)自身来执行调用,在调用完成时由其自身拿到结果。
    • 异步:就是指一个调用发出后马上返回,暂时先不获取调用结果。即:调用者发起调用后,其自身不执行调用的方法,而是继续执行自己本身的方法,由被调用者执行调用的方法,当被调用者执行结束后,将结果反馈给调用者。

    推而广之,我们这样这样理解同步IO和异步IO:

    • 同步IO:发起IO的执行流,其本身会参与到IO的工作,即拷贝数据或等待数据就绪,并且由这个执行流本身获取到IO的结果,这种IO操作称之为同步IO。
    • 异步IO:发起IO的执行流本身不参与IO工作,而是有另外一个执行流来进行IO操作,发起IO的线程只需要等待进行IO的执行流反馈回结果即可。

    二. 阻塞式IO

    阻塞式IO,就是当进行读数据或写数据时,如果外设条件没有就绪,就阻塞式的等待条件就绪。如:在read/recv时,如果缓冲区中没有数据,该进程/线程就自动被添加到特定的等待队列中去等待缓冲区中被写入数据,在等待的过程中,进程/线程 不执行其它的工作。

    所有的IO操作,默认都是阻塞式的,非阻塞IO需要人为进行设置

    图2.1 阻塞式IO的模型图

    三. 非阻塞式IO

    如果一个进程/线程在进行IO操作时,数据尚未就绪,那么读取接口不会阻塞,先返回执行其他的工作,数据就绪才进行读取。

    非阻塞式IO,一般采用轮询检查的方法进行IO操作,即:通过循环,不断检查IO资源是否已经就绪,就绪就读取,不就绪就执行其他的工作。

    图3.1 非阻塞式IO的模型图

    IO操作默认是阻塞式的,有两种方法可以实现非阻塞式IO:

    • 在使用open打开文件/调用socket接口获得fd时,传入O_NONBLOCK/SOCK_NONBLOCK作为参数,实现非阻塞式IO。
    • 通过调用fcntl函数,对指定的fd设置非阻塞式IO。

    这里重点介绍如图通过调用fcntl函数来设置非阻塞式的IO。

    fcntl函数 -- 对特定fd设置非阻塞IO

    函数原型:int fcntl(int fd, int cmd, .../*args*/)

    函数参数:

    • fd:进行操作的文件描述符。
    • cmd:F_GETFL -- 获取文件状态、F_SETFL -- 设置文件状态
    • 缺省参数:当cmd为F_SETFL时,传入O_NONBLOCK可以设置非阻塞。

    返回值:当cmd为F_SETFL时,返回当前的文件状态,当cmd为F_SETFL时,应当返回0,如果函数调用失败,则返回-1。

    代码3.1通过提供SetNonBlock接口,对标准输入(fd = 0)进行非阻塞式读取,在SetNonBlock内部调用fcntl函数将指定的fd设置为非阻塞。但在使用read进行非阻塞读取时,有以下几点问题需要注意:

    • 如果调用read时底层资源尚未就绪而产生非阻塞,那么read会以读取出错的形式返回。
    • read如果出错,不仅会体现在返回值上,也会设置全局错误码errno。
    • 当因为数据未就绪而非阻塞时,错误码errno被设置为EWOULDBLOCK || EAGAIN,其余为真正的读取失败。
    • 在非阻塞式read读取时,还应当判断错误码是否为EINTR,表示是否因为进程收到信号而造成read终止,如果是,应当尝试再次读取。

    其中 EWOULDBLOCK 和 EAGAIN 对应的错误码都是11,描述信息为:Resource temporarily unavailable,就是资源尚未就绪的意思。

    代码3.1:非阻塞式IO

    1. #include <iostream>
    2. #include <cerrno>
    3. #include <cstring>
    4. #include <unistd.h>
    5. #include <fcntl.h>
    6. // 设置非阻塞IO函数
    7. bool SetNonBlockIO(int fd)
    8. {
    9. // 获取文件状态
    10. int fd_status = fcntl(fd, F_GETFL);
    11. if(fd_status < 0)
    12. {
    13. // 如果失败返回false,表示设置非阻塞失败
    14. return false;
    15. }
    16. // 在原有的文件状态下添加非阻塞状态
    17. fcntl(fd, F_SETFL, fd_status | O_NONBLOCK);
    18. return true;
    19. }
    20. int main()
    21. {
    22. // 对标准输入设置非阻塞
    23. bool ret = SetNonBlockIO(0);
    24. if(!ret)
    25. {
    26. exit(1);
    27. }
    28. // 开始进行非阻塞IO操作
    29. char buffer[1024] = { 0 };
    30. while(1)
    31. {
    32. ssize_t sz = read(0, buffer, 1024);
    33. // 如果正常读取
    34. if(sz > 0)
    35. {
    36. buffer[sz - 1] = '\0'; // 设置字符串结束标识
    37. std::cout << "echo# " << buffer << ", [errno:" << errno << ", errorMsg:" << strerror(errno) << "]" << std::endl;
    38. }
    39. else // 读取失败
    40. {
    41. // 如果是因为资源没有就绪而读取失败
    42. if(errno == EWOULDBLOCK || errno == EAGAIN)
    43. {
    44. std::cout << "errno:" << errno << ", errorMsg:" << strerror(errno) << std::endl;
    45. }
    46. else if(errno == EINTR) // 因为进程收到信号而终止read
    47. {
    48. std::cout << "Interrupt because of signal, errno:" << errno << ", errorMsg:" << strerror(errno) << std::endl;
    49. }
    50. sleep(1);
    51. }
    52. }
    53. return 0;
    54. }

    四. 信号驱动式IO

    通过自定义对29号SIGIO信号的处理函数来实现信号驱动式IO,当进程收到SIGIO信号的时候,就调用对应的处理函数来进行IO操作,这样保证在调用IO接口的时候数据一定是就绪的,在没有收到信号时不影响进程进行其他的工作,信号驱动式IO避免了阻塞等待资源就绪,提高了IO效率。

    图4.1 信号驱动式IO的模型图

    五. IO多路复用

    一个进程能够同时等待多个文件描述符的资源是否就绪,只要其中一个文件描述符的资源就绪变为可读,那么进程就会去读取这个文件描述符对应的数据。由于可以同时等待多个文件描述符,只要其中一个资源就绪,就可以进行读取,这样就大大提高了IO的效率。

    图5.1 IO多路复用的模型图

    六. 异步IO

    异步IO,就是发起IO的执行流本身不参与IO工作,而是有另外一个执行流来进行IO操作,发起IO的线程只需要等待进行IO的执行流反馈回结果。这样发起IO的执行流就不需要等到,可以处理其他的工作。

    图6.1 异步IO的模型图

    七. 总结

    • IO就是对外部设备进行输入输出操作的简称,IO的效率是十分低下的。
    • IO操作在单位时间内等待资源就绪的时长越短,IO效率就越高。
    • 共有5种常见的IO模型:阻塞式IO、非阻塞式IO、信号驱动IO、IO多路复用和异步IO,其中前四种属于同步IO。
    • 同步IO和异步IO的区别在于,是否由发起IO的执行流实际执行IO操作。
  • 相关阅读:
    【Dolphinscheduler3.1.1】二次开发本地启动项目(前端+后端)
    stm32mp157系统移植 | 移植ST官方5.10内核到小熊派开发板
    PHP 经纬度坐标相关计算方法
    【Vue】组件通信与方法暴露实践
    提升媒体文字质量:常见错误及改进措施解析
    经典算法之直接插入排序(DirectInsert Sort)
    基于taro开发微信小程序
    viewerjs -v 11 动态获取图片(ajax),以及重复初始化问题。
    在生产环境中部署Elasticsearch:最佳实践和故障排除技巧——安装篇(一)
    element-plus 使el-dropdown只显示当前选择节点
  • 原文地址:https://blog.csdn.net/weixin_43908419/article/details/134336677