文件系统(IO-进程-线程)
目录
IO
同步/异步/阻塞/非阻塞/BIO/NIO/AIO
阻塞IO模型
非阻塞IO模型
多路复用IO模型
异步IO模型
IO模型总结
零拷贝
传统的文件传输有多糟糕?
使用零拷贝技术的项目
进程
进程的控制结构
什么是线程?
线程与进程的比较
IO模型
Java IO
BIO、NIO、AIO有什么区别
适用场景分析
zero 拷贝
文件描述符(File descriptor)
同步IO VS异步IO
同步阻塞IO/BlockingIO
同步非阻塞IO/ non BlockingIO
多路复用IO模型
IO多路复用之select
IO多路复用之epoll
信号驱动模型
异步IO(AIO)
inode 来存放文件的这些元信息
Linux 网络协议栈是根据 TCP/IP 模型来实现的,TCP/IP 模型由应用层、传输层、网络层和网络接口层,共四层组成,每一层都有各自的职责。
应用程序要发送数据包时,通常是通过 socket 接口,于是就会发生系统调用,把应用层的数据拷贝到内核里的 socket 层,接着由网络协议栈从上到下逐层处理后,最后才会送到网卡发送出去。
而对于接收网络包时,同样也要经过网络协议逐层处理,不过处理的方向与发送数据时是相反的,也就是从下到上的逐层处理,最后才送到应用程序。
通常是以 4 个指标来衡量网络的性能,分别是带宽、延时、吞吐率、PPS(Packet Per Second)
要想知道网络的配置和状态,我们可以使用 ifconfig 或者 ip 命令来查看。
这两个命令功能都差不多,不过它们属于不同的软件包,ifconfig 属于 net-tools 软件包。
socket 信息如何查看?
我们可以使用 netstat 或者 ss,这两个命令查看 socket、网络协议栈、网口以及路由表的信息。
虽然 netstat 与 ss 命令查看的信息都差不多,但是如果在生产环境中要查看这类信息的时候,尽量不要使用 netstat 命令,因为它的性能不好,在系统比较繁忙的情况下,如果频繁使用 netstat 命令则会对性能的开销雪上加霜,所以更推荐你使用性能更好的 ss 命令。
IO
两个名词:
- 用户空间
- 内核空间。
windows文件句柄 /Linux 文件描述符
我们普通程序的代码是跑在用户空间上的,而操作系统的代码跑在内核空间上,用户空间无法直接访问内核空间的。当一个进程运行在用户空间时就处于用户态,运行在内核空间时就处于内核态。
磁盘 I/O 指的是硬盘和内存之间的输入输出。
读取本地文件的时候,要将磁盘的数据拷贝到内存中,修改本地文件的时候,需要把修改后的数据拷贝到磁盘中。
网络 I/O 指的是网卡与内存之间的输入输出。
当网络上的数据到来时,网卡需要将数据拷贝到内存中。当要发送数据给网络上的其他人时,需要将数据从内存拷贝到网卡里。
那为什么都要跟内存交互呢?
我们的指令最终是由 CPU 执行的,究其原因是 CPU 与内存交互的速度远高于 CPU 和这些外部设备直接交互的速度。
因此都是和内存交互,当然假设没有内存,让 CPU 直接和外部设备交互,那也算 I/O。
总结下:I/O 就是指内存与外部设备之间的交互(数据拷贝)。
当一个网络IO发生(假设是read)时,它会涉及两个系统对象,一个是调用这个IO的进程,另一个是系统内核。
当一个read操作发生时,它会经历两个阶段:
①等待数据准备;
②将数据从内核拷贝到进程中。
同步/异步/阻塞/非阻塞/BIO/NIO/AIO
同步和异步
所谓同步,指的是协同步调。既然叫协同,所以至少要有2个以上的事物存在。
协同的结果就是:
多个事物不能同时进行,必须一个一个的来,上一个事物结束后,下一个事物才开始。
阻塞和非阻塞
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:
阻塞时指IO操作需要彻底完成后才返回到用户空间;
而非阻塞是指IO操作被调用后立即返回给用户一个状态值,不需要等到IO操作彻底完成。
【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)
阻塞IO模型
在Linux中,默认情况下所有socket都是阻塞的,一个典型的读操作如下图所示:
当应用进程调用了recvfrom这个系统调用后,系统内核就开始了IO的第一个阶段:
准备数据。
对于网络IO来说,很多时候数据在一开始还没到达时(比如还没有收到一个完整的TCP包),系统内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。
当系统内核一直等到数据准备好了,它就会将数据从系统内核中拷贝到用户内存中,然后系统内核返回结果,用户进程才解除阻塞的状态,重新运行起来。所以,阻塞IO模型的特点就是在IO执行的两个阶段(等待数据和拷贝数据)都被阻塞了。
非阻塞IO模型
在Linux中,可以通过设置socket使IO变为非阻塞状态。当对一个非阻塞的socket执行read操作时,读操作流程如下图所示:
从图中可以看出,当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那么它不会阻塞用户进程,而是立刻返回一个错误。
从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。当用户进程判断结果是一个错误时,它就知道数据还没有准备好,于是它可以再次发送read操作。
一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据复制到了用户内存中,然后返回正确的返回值。
所以,在非阻塞式IO中,用户进程其实需要不断地主动询问kernel数据是否准备好。非阻塞的接口相比阻塞型接口的显著差异在于被调用之后立即返回。
多路复用IO模型
多路IO复用,有时也称为事件驱动IO(Reactor设计模式)。它的基本原理就是有个函数会不断地轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,多路IO复用模型的流程如图所示:
当用户进程调用了select,那么整个进程会被阻塞,而同时,内核会"监视"所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。
这个模型和阻塞IO的模型其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而阻塞IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个连接。所以,如果系统的连接数不是很高的话,使用select/epoll的web server不一定比使用多线程的阻塞IO的web server性能更好,可能延迟还更大;select/epoll的优势并不是对单个连接能处理得更快,而是在于能处理更多的连接。
如果select()发现某句柄捕捉到了"可读事件",服务器程序应及时做recv()操作,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入writefds,准备下一次的"可写事件"的select()检测。同样,如果select()发现某句柄捕捉到"可写事件",则程序应及时做send()操作,并准备好下一次的"可读事件"检测准备。
如下图展示了基于事件驱动的工作模型,当不同的事件产生时handler将感应到并执行相应的事件,像一个多路开关似的。
IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。
异步IO模型
“真正”的异步IO需要操作系统更强的支持。如下展示了异步 IO 模型的运行流程(Proactor设计模式):
用户进程发起read操作之后,立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个异步的read请求操作之后,首先会立刻返回,所以不会对用户进程产生任何阻塞。
然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中,当这一切都完成之后,内核会给用户进程发送一个信号,返回read操作已完成的信息。
IO模型总结
调用阻塞IO会一直阻塞住对应的进程直到操作完成,
而非阻塞IO在内核还在准备数据的情况下会立刻返回。
两者的区别就在于同步IO进行IO操作时会阻塞进程。按照这个定义,之前所述的阻塞IO、非阻塞IO及多路IO复用都属于同步IO。实际上,真实的IO操作,就是例子中的recvfrom这个系统调用。
非阻塞IO在执行recvfrom这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中数据准备好时,recvfrom会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞。
而异步IO则不一样,当进程发起IO操作之后,就直接返回,直到内核发送一个信号,告诉进程IO已完成,则在这整个过程中,进程完全没有被阻塞。
各个IO模型的比较如下图所示:
解释下什么叫半连接
我们都知道 TCP 建立连接需要三次握手,当接收方收到请求方的建连请求后会返回 ack,此时这个连接在接收方就处于半连接状态,当接收方再收到请求方的 ack 时,这个连接就处于已完成状态:
零拷贝
DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。
什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
传统的文件传输有多糟糕?
如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
- 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
- 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
- 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
- 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。
这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。
使用零拷贝技术的项目
事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。
如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo方法:
@Overridepublic
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。
Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率,是否开启零拷贝技术的配置如下:
http {
...
sendfile on
...
}
sendfile 配置的具体意思:
设置为 on 表示,使用零拷贝技术来传输文件:sendfile ,这样只需要 2 次上下文切换,和 2 次数据拷贝。
设置为 off 表示,使用传统的文件传输技术:read + write,这时就需要 4 次上下文切换,和 4 次数据拷贝。
当然,要使用 sendfile,Linux 内核版本必须要 2.1 以上的版本。
PageCache【磁盘高速缓存】 的优点主要是两个:
- 缓存最近被访问的数据;
- 预读功能;
这两个做法,将大大提高读写磁盘的性能。
傻瓜三歪让我教他「零拷贝」
当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式。
进程
虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。
进程的状态变迁:
- NULL -> 创建状态:一个新进程被创建时的第一个状态;
- 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
- 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
- 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
- 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
- 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件
- 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
另外,还有一个状态叫挂起状态,它表示进程没有占有内存空间。这跟阻塞状态是不一样,阻塞状态是等待某个事件的返回。
进程的控制结构
在操作系统中,是用进程控制块(process control block,PCB)数据结构来描述进程的。
PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。
PCB 具体包含什么信息呢?
进程描述信息:
-
- 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
进程控制和管理信息:
-
- 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
- 进程优先级:进程抢占 CPU 时的优先级;
资源分配清单:
有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。
CPU 相关信息:
CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。
可见,PCB 包含信息还是比较多的。
每个 PCB 是如何组织的呢?
通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:
- 将所有处于就绪状态的进程链在一起,称为就绪队列;
- 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
- 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。
发生进程上下文切换有哪些场景?
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
什么是线程?
线程是进程当中的一条执行流程。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程都有独立一套的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程与进程的比较
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
对于,线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
所以,线程比进程不管是时间效率,还是空间效率都要高。
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分。
内核空间与用户空间的区别:
进程在用户态时,只能访问用户空间内存;
只有进入内核态后,才可以访问内核空间的内存;
名词
新一代基于微服务架构的esb企业级服务总线平台,集API网关,API监控,API门户于一体,提供强大的API编排与统一服务调度能力,全Web可视化拖,拉,拽实现服务的编排与聚合.
IO模型
网络通讯,一台计算机给另一台计算机传输数据,中间过程就叫做通信,
也就是通过IO接口输入输出到另一台计算机,这个就叫做网络IO.
Java IO
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个)
:InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生出来的.
字符流和字节流是根据处理数据的不同来区分的。字节流按照8位传输,字节流是最基本的,
所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符
而是先把字符编码成字节,再储存这些字节到磁盘。
BIO、NIO、AIO有什么区别
操作IO的模式也有很多种,
BIO【同步阻塞IO blocking IO】、
NIO【同步非阻塞IO Non BlockingIO】、
AIO [异步Asynchronous IO] AsynchronousServerSocketChannel
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO, ServerSocket
它的特点是模式简单使用方便,并发处理能力低。
NIO:Non-blockingIO 同步非阻塞 IO,是传统 IO 的升级, ServerSocketChannel
客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,
异步 IO 的操作基于事件和回调机制。
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
java.io包基于流模型实现,提供File抽象、输入输出流等IO的功能。
交互方式是同步、阻塞的方式,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞。
java.io包的好处是代码比较简单、直观,缺点则是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。
java.net包下提供的部分网络API,比如Socket、ServerSocket、HttpURLConnection
也可以被归类到同步阻塞IO类库,因为网络通信同样是IO行为
java 1.4中引入了NIO框架(java.nio 包),提供了Channel、Selector、Buffer等新的抽象,
可以构建多路复用IO程序,同时提供更接近操作系统底层的高性能数据操作方式.
在Java7中,NIO有了进一步的改进,也就是NIO2,引入了异步非阻塞IO方式,
也被称为AIO(Asynchronous IO),异步IO操作基于事件和回调机制。
原文链接:Java 中 IO 流分为几种?BIO,NIO,AIO 有什么区别?_bio—乐知是什么-CSDN博客
原文链接:Java中IO流分为几种?BIO,NIO,AIO 有什么区别?_java 中 io 流分为几种?bio,nio,aio 有什么区别?-CSDN博客
适用场景分析
BIO方式适用于连接数目比较小且固定的架构,
这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解;
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,
比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持;
AIO方式使用于连接数目多且连接比较长(重操作)的架构,
比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持;
原文链接:Java中IO流分为几种?BIO,NIO,AIO 有什么区别?_java 中 io 流分为几种?bio,nio,aio 有什么区别?-CSDN博客
zero 拷贝
在传统的数据 IO 模式中,读取一个磁盘文件,并发送到远程端的服务,
就共有四次用户空间与内核空间的上下文切换,四次数据复制,
包括两次 CPU 数据复制,两次 DMA 数据复制。
解放CPU,这也就是零拷贝Zero-Copy技术。数据应该可以直接从内核缓冲区直接送入Socket缓冲区。
解决思路:零拷贝技术的几个实现手段包括:mmap+write、sendfile、sendfile+DMA收集、splice等。
在Java NIO包中提供了零拷贝机制对应的API
(1)mmap + write 的零拷贝方式:
FileChannel 的 map() 方法产生的 MappedByteBuffer:FileChannel 提供了 map() 方法
(2)sendfile 的零拷贝方式:
FileChannel 的 transferTo、transferFrom 如果操作系统底层支持的话,
transferTo、transferFrom也会使用 sendfile 零拷贝技术来实现数据的传输。
FileChannel的实现类并不在JDK本身,而位于sun.nio.ch.FileChannelImpl类中,
零拷贝的具体实现自然也都是native方法,看源码。
零拷贝机制的应用
零拷贝在很多框架中得到了广泛应用,一般都以Netty为例来分析。但作为大数据工程师,
Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式
DMA(Direct Memory Access,直接内存访问):DMA 本质上是一块主板上独立的芯片,
允许外设设备直接与内存存储器进行数据传输,并且不需要CPU参与的技术
文件描述符(File descriptor)
是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上它是一个索引值,
指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统而windows为句柄的概念。
原文链接:网络通信和IO(1):网络通信与IO基本概念/什么是IO,什么是网络通信/文件IO和网络IO的区别/什么是文件描述符/什么是阻塞IO(BIO)/什么是非阻塞IO(NIO)/JAVA中的IO_io通信概念-CSDN博客
同步IO VS异步IO
经典应用阻塞socket/BIO
如果内核数组一直没准备好,那用户进程就将一直阻塞,浪费性能,可以使用非阻塞IO优化。
同步非阻塞IO/ non BlockingIO
如果内核数据还么有准备好,可以先返回错误信息给用户进程,让它需要等待(通过轮询方式再请求)
流程:
- 应用进程向操作系统内核,发起recvfrom读取数据。
- 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
- 应用程序轮询调用,继续向操作系统内核发起recvfrom读取数据。
- 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
- 完成调用,返回成功提示。
它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。
可以考虑IO复用模型,去解决这个问题。
多路复用IO模型
复习下,什么是文件描述符fd(File Descriptor),它是计算机科学中的一个术语,形式上是一个非负整数。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),
它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。
IO多路复用之select
应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,
只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,
这时应用进程再发起recvfrom请求去读取数据。
非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,
然而借助select的IO多路复用模型,只需要发起一次系统调用就够了,大大优化了性能。
但是呢,select有几个缺点:
监听的IO最大连接数有限,在Linux系统上一般为1024。
select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(
仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流)
因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。
但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。
如果同时连接的大量客户端在一时刻可能只有极少处于就绪状态,
伴随着监视的描述符数量的增长,效率也会线性下降。
因此经典的多路复用模型epoll诞生。
IO多路复用之epoll
为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件驱动来实现,流程图如下:
epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,
迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,
而是采用监听事件回调的的机制。这就是epoll的亮点。
我们一起来总结一下select、poll、epoll的区别
epoll明显优化了IO的执行效率,但在进程调用epoll_wait()时,仍然可能被阻塞的。
能不能酱紫:不用我老是去问你数据是否准备就绪,等我发出请求后,
你数据准备好了通知我就行了,这就诞生了信号驱动IO模型。
信号驱动模型
信号驱动IO不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号
(调用sigaction的时候建立一个SIGIO的信号),然后应用用户进程可以去做别的事不用阻塞。
当内核数据准备好后,再通过SIGIO信号通知应用进程,数据准备好后的可读状态。
应用用户进程收到信号之后,立即调用recvfrom,去读取数据。
信号驱动IO模型,在应用进程发出信号后,是立即返回的,不会阻塞进程。它已经有异步操作的感觉了。
但是你细看上面的流程图,发现数据复制到应用缓冲的时候,应用进程还是阻塞的。回过头来看下,
不管是BIO,还是NIO,还是信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的。
还有没有优化方案呢?AIO(真正的异步IO)!
异步IO(AIO)
前面讲的BIO,NIO和信号驱动,在数据从内核复制到应用缓冲的时候,都是阻塞的,
因此都不是真正的异步。AIO实现了IO全流程的非阻塞,就是应用进程发出系统调用后,
是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。
等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程IO操作执行完毕。
流程如下:异步IO的优化思路很简单,只需要向内核发送一次请求,
就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。
日常开发中,有类似的业务场景:
比如发起一笔批量转账,但是转账处理比较耗时,这时候后端可以先告知前端转账提交成功,
等到结果处理完,再通知前端结果即可。
参考链接:
https://juejin.cn/post/7036518015462015006