1。引言同步和异步I/O,阻塞和非阻塞I/O是程序员的共同话题,也是我一直以来都比较陌生的话题。例如:什么是同步和异步?什么是阻塞和非阻塞?两者有什么区别?阻塞在哪里?为什么要用多种IO模型来解决问题?框架中常用什么样的I/O?模特儿?各种IO模型各有什么优缺点,适用于哪些应用场景?总之,对I/O的认知不能只从字面理解,只有了解其内在玄机才能深刻理解I/OO,才能看清I/O相关问题的本质。2.I/O的定义I/O的全称是Input/Output。I/O虽然经常被提及,但你肯定一时半会儿无法给出一个完整的定义。我搜索了谷歌,发现了一些冗长的讨论。要理清I/O的概念,我们需要从不同的角度来理解它。2.1.计算机视角冯·诺依曼计算机的基本思想中提到,计算机硬件应该由控制器、运算器、存储器、输入输出五部分组成。其中,输入是指将数据输入计算机的设备,如键盘、鼠标等;输出是指从计算机获取数据的设备,如显示器;以及输入输出设备、硬盘、网卡等。用户可以通过操作系统完成对计算机的操作。计算机启动时,最先启动的程序是操作系统的内核,它将负责计算机的资源管理和进程的调度。换句话说:操作系统负责从输入设备读取数据,并将数据写入输出设备。因此,I/O对于计算机来说有两个含义:I/O设备读取数据和向I/O设备写入数据。对于一个I/O操作,必须有两个参与者参与,一个输入终端和一个输出终端。根据参与方的设备类型,我们可以将其分为磁盘I/O、网络I/O(网络、网卡的请求响应)等。2.2.程序视角应用程序作为文件保存在磁盘上,只有加载到内存中成为进程后才能运行。在计算机内存中运行的应用程序不可避免地会涉及到数据交换,比如读写磁盘文件、访问数据库、调用远程API等等。但是我们编写的程序不能像操作系统内核那样直接进行I/O操作。因为为了保证操作系统的安全稳定运行,在操作系统启动后,会开启保护模式:内存分为内核空间(内核对应进程所在的内存空间)和内存隔离的用户空间。我们构建的程序会运行在用户空间,用户空间不能操作内核空间,也就是说用户空间的程序不能直接访问内核管理的I/O,比如:硬盘,网卡,ETC。但是,操作系统对外提供API,由各种类型的系统调用(SystemCalls)组成,提供安全的访问控制。因此,如果应用程序想要访问内核管理的I/O,就必须通过调用内核提供的系统调用(systemcall)来进行间接访问。因此,对于应用程序而言,I/O强调通过向内核发起系统调用来间接访问I/O。也就是说,应用程序发起的一次IO操作实际上包括两个阶段:IO调用阶段:应用进程向内核发起系统调用IO执行阶段:内核执行IO操作并返回2.1。数据准备阶段:内核等待I/O设备准备数据2.2.复制数据阶段:将数据从内核缓冲区复制到用户空间缓冲区如何理解数据准备阶段?对于写请求:等待系统调用完整的请求数据写入内核缓冲区;forreadrequests:等待系统调用的完整请求数据;(如果内核缓冲区中不存在请求数据),将外围设备的数据读入内核缓冲区。应用进程发起IO调用之前到内核执行IO返回之前应用进程/线程的状态是我们下面要讨论的第二个话题,阻塞IO和非阻塞IO。3.IO模型的阻塞I/O(BIO)如果应用程序中的进程发起IO调用,在内核执行IO操作并返回结果之前,如果发起系统调用的线程一直处于等待状态状态下,IO操作是阻塞IO。阻塞IO简称为BIO,BlockingIO。处理流程如下图所示:从上图我们可以看出,当用户进程发起IO系统调用时,用户调用线程在内核的两个阶段选择阻塞等待数据返回从准备数据到将数据复制到用户空间。因此,BIO带来了一个问题:如果内核数据准备时间过长,那么用户进程就会被阻塞,浪费性能。为了提高应用性能,虽然可以采用多线程来提高性能,但是线程的创建仍然需要系统调用,而多线程会导致频繁的线程上下文切换,也会影响性能。所以我们要想解决BIO带来的问题,就得看问题的本质,就是blocking这个词。4、IO模型的非阻塞I/O(NIO)方案自然很容易想到,变阻塞为非阻塞,即用户进程在发起系统调用时指定非阻塞,之后内核收到请求,会立即返回,然后用户进程通过轮询的方式拉取处理结果。即如下图所示:应用程序中进程发起IO调用后,内核执行IO操作返回结果前,如果发起系统调用的线程不等待而是立即返回,则IO操作是非阻塞IO模型。非阻塞IO简称NIO,Non-BlockingIO。然而,虽然非阻塞IO相对于阻塞IO在性能上有了很大的提升,但仍然不是一个完美的解决方案。它仍然存在性能问题,即频繁的轮询导致频繁的系统调用,消耗大量的CPU资源。比如并发量大的时候,假设有1000个并发,单位时间周期内会有1000次系统调用轮询执行结果,但实际上可能只有2次请求结果被执行,也就是998次无效的系统调用,造成严重的性能浪费。如果有问题,就必须解决。NIO问题的本质是频繁轮询导致的无效系统调用。5、IO模型中的IO多路复用解决NIO的思路是降级无效的系统调用。如何降解它们?下面我们就来看看IO多路复用的解决方案。5.1.IO多路复用的select/pollSelect是内核提供的系统调用。支持一次查询多个系统调用的可用状态。当有任何结果状态可用时,它会返回,用户进程会发起另一个系统调用来读取数据。也就是说,是NIO中的N次系统调用。使用Select,只需要发起一次系统调用。它的IO过程如下:但是select有一个限制,就是有连接数限制。为此,提出民意调查。与select相比,主要是解决了连接限制。select/epoll虽然解决了NIO重复无效系统调用的问题,但同时也引入了新的问题。问题是:在用户空间和内核空间之间,复制了大量的数据。内核循环遍历IO状态,浪费CPU时间。也就是说,虽然select/poll减少了用户进程发起的系统调用次数,但是内核的工作量只会增加。减少。在高并发的情况下,内核的性能问题依然存在。所以select/poll问题的实质是:内核中存在无效的循环遍历。5.2.epollforIOmultiplexing针对select/pool引入的问题,我们将解决问题的思路转回内核。如何减少内核重复无效的循环遍历?变主动为被动,基于事件驱动实现。流程图如下:与select/poll相比,epoll多了两个系统调用,其中epoll_create与内核建立连接,epoll_ctl注册事件,epoll_wait阻塞用户进程等待IO事件。epoll对IO的执行效率进行了极大的优化,但是在IO执行的第一阶段:数据准备阶段仍然是阻塞的。所以这是一个可以继续优化的点。6.IO模型的信号驱动IO(SIGIO)信号驱动IO与BIO、NIO最大的区别在于用户进程在IO执行的数据准备阶段不会被阻塞。如下图所示:当用户进程需要等待数据时,它会向内核发送一个信号,告诉内核我要什么数据,然后用户进程会继续做其他事情,当数据在kernelisready时,kernel立即向用户进程发送一个信号,说“数据准备好了,快来看看”。用户进程收到信号后,立即调用recvfrom查看数据。乍一看,信号驱动的I/O模型有一种异步操作的感觉,但是在IO执行的第二阶段,即从内核空间向用户空间拷贝数据的阶段,用户进程仍然处于阻塞状态。综上所述,你会发现无论是BIO、NIO还是SIGIO,它们最终都会在IO执行的第二阶段被阻塞。如果IO执行的第二阶段能做到非阻塞就完美了。7、IO模型的异步IO(AIO)异步IO真正实现了整个IO过程的非阻塞。用户进程发送系统调用后立即返回。内核等待数据准备完成,然后将数据复制到用户进程缓冲区,然后发送信号告诉用户进程IO操作完成(相对于SIGIO,一种是发送信号告诉用户进程表示数据准备完成,一种是IO执行完成)。过程如下:所以,之所以叫异步IO,要看第二阶段IO执行是否阻塞。所以上面说的BIO、NIO、SIGIO都是同步IO。8.总结梳理了这几个IO模型,之前一直处于懵懂状态的阻塞、非阻塞、同步、异步IO终于有了一个概念。同时,我也一直在纠正自己的错误,所以一路走来,越来越觉得返朴归真很重要。只有这样,我们才能在飞速的技术演进中以不变应万变。本文根据多方资料撰写而成,难免有错误,但只有写下来才能改正。因此,还请各位评委赐教。
