掘金 后端 ( ) • 2024-04-25 15:45

UINX环境

  • 常见的操作系统比如Linux 和 MacOS都是基于UNIX的。它们都继承了UNIX的许多特性和设计理念。因此,它们的高级I/O技术确实是建立在UNIX基础之上的。Linux和macOS都提供了丰富的高级I/O功能和API,利用了UNIX系统调用和原则。这些功能包括非阻塞I/O、多路复用I/O(如Linux上的select()poll()epoll(),以及macOS上的kqueue)、异步I/O(aio_*函数),以及其他更高级的I/O技术。这篇文章主要介绍在Linux环境下得高级IO。

这篇文章主要介绍五种IO模型,分别是:

  • 阻塞式IO
  • 非阻塞式IO
  • 信号驱动式IO
  • 多路转接IO
  • 异步IO

IO

什么是IO?

IO,就是Input(输入),Output(输出)的简称。 最简单的使用C语言调用printf函数时,就是向显示器输出数据。 调用scanf函数时,就是从标准输入键盘读入数据。这就是最基本的IO,一般称之为标准IO。还有一些文件IO。比如readwirte。这些函数,从文件中输入或者向文件中输出时,这也是IO操作,一般称之为文件IO。在比如网络中socket套接字中的recvsend这些函数,从socket套接字中输入或者输出时,都是IO操作,一般称之为网络IO。

而应用层在调用printfwirtesend这些函数时,本质是把数据从用户层拷贝到操作系统内核层中的缓冲区中。在数据被写入到内核缓冲区后,操作系统会负责将数据从内核缓冲区传输到实际的输出设备或目标,比如显示器、文件或网络。这个过程可以是立即发生,也可以是延迟到适当的时机再进行。

image.png

同理,调用scnafreadrecv这些函数时,本质是从操作系统内核缓冲区中将数据拷贝到用户层中。这些函数本质也就是拷贝函数。

而在IO时,大部分的时间是在等待,只有有数据的时候,才会进行拷贝。举个简单的例子。比如调用scanf时,不从键盘输入时,就不会读取数据。进程就会一直阻塞等待,只有当用户从键盘中输入完数据按下回车时,才会读取。

根据上面的认识,可以建立一个共识,那就是IO = 等待 + 拷贝。而要进行拷贝,必须先要判断条件是否成立(即内核缓冲区是否有空间 或者有数据)。

在进程等待的这段时间内,如果进程一直判断内核的接收缓冲区是否有数据,有数据就进行读取。一直进行判断,肯定会带来CPU资源的消耗。大佬们就会想出解决办法,让进程不要一直进行判断,根据特定的规则,来判断内核缓冲区的状态。将其称之为高效IO。

所谓的高效IO,就是在单位时间内,拷贝的数据量越大,进行判断的次数越少。根据这个解决思路,有了各种IO模型。 就是上面提到的五种IO模型,他们并不都是高效的,只有部分是高效的,最值得学习的是IO多路转接和非阻塞IO。这篇文章也会重点介绍这两种IO模型。

先简单得认识一下这五种模型,在详细介绍其中高效得。

阻塞式IO

阻塞式IO(Blocking IO)是一种常见的IO模型,其特点是当应用程序执行IO操作时,如果数据没有准备好或无法立即进行IO操作,应用程序会进入阻塞状态,直到数据准备就绪或IO操作完成为止。所有的套接字默认都是阻塞方式

阻塞式IO的工作流程通常如下:

  1. 应用程序发起IO操作(如读取文件、接收网络数据等)。
  2. 如果数据已经准备好,或者可以立即进行IO操作,IO操作会立即完成,应用程序继续执行后续代码。
  3. 如果数据尚未准备好,或者无法立即进行IO操作(如网络数据尚未到达、文件尚未准备好等),应用程序会进入阻塞状态,暂时挂起,直到数据准备就绪或IO操作完成。
  4. 一旦数据准备就绪或IO操作完成,应用程序会解除阻塞状态,继续执行后续代码。

阻塞式IO的优点是实现简单,易于理解和使用。但它也存在一些缺点,主要包括:

  • 资源浪费:在数据未准备就绪时,应用程序会一直等待,占用CPU资源。这可能导致系统资源的浪费。
  • 性能不佳:在大量IO操作时,阻塞式IO可能会导致程序的性能下降,特别是在需要等待大量IO操作完成时。
  • 不适合高并发环境:在高并发环境下,阻塞式IO可能会导致系统资源耗尽,无法满足大量并发IO请求的需求。

阻塞式IO存在一些缺点,但在某些场景下仍然是一种合适的IO模型,特别是在简单的应用中或者IO负载较轻的情况下。

非阻塞IO

非阻塞IO(Non-blocking IO)是一种IO模型,其特点是在应用程序执行IO操作时,如果数据尚未准备好或无法立即进行IO操作,应用程序不会进入阻塞状态,而是立即返回一个错误码或指示数据未就绪的状态,让应用程序可以继续执行其他任务。

非阻塞式IO的工作流程通常如下:

  1. 应用程序发起非阻塞IO操作(如读取文件、接收网络数据等)。
  2. 如果数据已经准备好,或者可以立即进行IO操作,IO操作会立即完成,应用程序继续执行后续代码。
  3. 如果数据尚未准备好,或者无法立即进行IO操作(如网络数据尚未到达、文件尚未准备好等),IO操作会立即返回一个错误码或指示数据未就绪的状态,应用程序可以继续执行其他任务,而不是等待数据准备就绪。
  4. 应用程序可以周期性地轮询IO操作的状态,直到数据准备就绪或IO操作完成。

非阻塞IO的优点包括:

  • 资源利用率高:应用程序不会一直等待数据准备就绪,而是可以继续执行其他任务,提高了系统的资源利用率。
  • 适合高并发环境:在高并发环境下,非阻塞IO可以有效地处理大量IO请求,而不会导致系统资源的耗尽。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用。

信号驱动式IO

信号驱动IO(Signal-driven IO)是一种IO模型,其基本思想是应用程序在发起IO操作后,不需要等待数据准备就绪或IO操作完成,而是继续执行其他任务。当数据准备就绪或IO操作完成时,操作系统会发送一个信号给应用程序,通知其进行IO操作。

信号驱动IO的工作流程通常如下:

  1. 应用程序发起信号驱动IO操作(如读取文件、接收网络数据等)。
  2. 应用程序继续执行其他任务,不会被阻塞。
  3. 当数据准备就绪或IO操作完成时,操作系统会向应用程序发送一个信号(如SIGIO),通知其进行IO操作。
  4. 应用程序在接收到信号后,处理IO操作的结果,读取数据或者进行其他后续处理。

信号驱动IO的优点包括:

  • 非阻塞:应用程序不会因为IO操作而被阻塞,可以继续执行其他任务,提高了系统的并发能力和响应速度。
  • 异步:应用程序在发起IO操作后,不需要等待IO操作完成,而是在IO操作完成后收到信号再进行处理,实现了异步IO。
  • 适用于高并发环境:在高并发环境下,信号驱动IO可以有效地处理大量IO请求,而不会导致系统资源的耗尽。
  • 简化编程:相对于非阻塞IO,信号驱动IO模型能够降低编程复杂度,因为应用程序不需要周期性地轮询IO操作的状态,而是在IO操作完成后收到信号再进行处理。

然而,信号驱动IO也存在一些缺点,如:

  • 信号处理的复杂性:应用程序需要编写信号处理函数来处理IO操作完成的信号,这可能会增加代码的复杂性和维护成本。
  • 可移植性:不同操作系统对信号驱动IO的支持程度不同,可能存在一定的可移植性问题。

信号驱动IO在一些情况下可以是高效的,特别是在需要处理大量并发IO请求时。然而,它也有一些局限性,需要根据具体的应用场景和需求来选择是否使用。

多路转接IO

IO多路复用(IO Multiplexing)是一种高效的IO模型,它允许应用程序同时监视多个IO事件,当其中任何一个IO事件就绪时,应用程序可以进行相应的处理。这样可以减少了IO操作的轮询次数,提高了系统的效率和性能。

常见的IO多路复用技术包括select、poll、epoll等。

  • select:select是最早的IO多路复用技术之一,在UNIX系统中广泛使用。它通过select函数来监视多个文件描述符的IO状态,并在其中任何一个IO事件就绪时返回。但select有一些限制,例如监视的文件描述符数量有限,时间复杂度较高等。
  • poll:poll是select的改进版,也是一种IO多路复用技术。它通过poll函数来监视多个文件描述符的IO状态,并在其中任何一个IO事件就绪时返回。相比于select,poll没有文件描述符数量的限制,但效率仍然不高。
  • epoll:epoll是Linux系统下的IO多路复用技术,是select和poll的替代品,也是目前性能最好的IO多路复用技术之一。它通过epoll_ctl和epoll_wait两个函数来监视文件描述符的IO状态,并在其中任何一个IO事件就绪时返回。epoll具有高效、可扩展等特点,适用于高并发的IO场景。

IO多路复用的优点包括:

  1. 高效:IO多路复用可以减少IO操作的轮询次数,提高系统的效率和性能。
  2. 可扩展:IO多路复用技术通常支持大量的文件描述符,能够满足高并发的IO请求。
  3. 简化编程:相比于其他IO模型,IO多路复用可以降低编程复杂度,提高代码的可读性和可维护性。

总之,IO多路复用是一种高效的IO模型,适用于需要处理大量并发IO请求的场景,如网络服务器等。在选择IO模型时,可以根据具体的应用需求和性能要求来选择合适的IO多路复用技术。

异步IO

异步IO是一种IO模型,其核心思想是应用程序在发起IO操作后不需要等待IO操作完成,而是可以继续执行其他任务。当IO操作完成后,操作系统会通知应用程序,并将IO操作的结果传递给应用程序进行处理。

异步IO的工作流程通常如下:

  1. 应用程序发起异步IO操作(如读取文件、接收网络数据等)。
  2. 应用程序继续执行其他任务,不会被阻塞。
  3. 当IO操作完成后,操作系统会向应用程序发送通知,告知IO操作的结果。
  4. 应用程序在接收到通知后,处理IO操作的结果,读取数据或者进行其他后续处理。

异步IO通常需要操作系统提供支持,以便操作系统能够在IO操作完成后通知应用程序。在不同的操作系统中,异步IO的实现方式可能会有所不同。

异步IO的优点包括:

  • 非阻塞:应用程序不需要等待IO操作完成,而是可以继续执行其他任务,提高了系统的并发能力和响应速度。
  • 高效:异步IO可以在IO操作完成后立即进行下一步处理,而不需要额外的等待时间,因此可以提高系统的效率和性能。
  • 适用于高并发环境:在高并发环境下,异步IO可以有效地处理大量IO请求,而不会导致系统资源的耗尽。
  • 简化编程:相比于其他IO模型,异步IO可以降低编程复杂度,因为应用程序不需要周期性地轮询IO操作的状态,而是在IO操作完成后收到通知再进行处理。

然而,异步IO也存在一些局限性,如:

  • 实现复杂性:异步IO的实现可能较为复杂,需要操作系统提供支持,并且需要编写异步IO的处理逻辑,这可能会增加代码的复杂性和维护成本。
  • 可移植性:不同操作系统对异步IO的支持程度不同,可能存在一定的可移植性问题。

综上所述,异步IO在一些情况下可以是高效的,特别是在需要处理大量并发IO请求时。然而,它也有一些局限性,需要根据具体的应用场景和需求来选择是否使用。

专业术语太多 看不懂? 没关系,外卖总点过吧。

阻塞IO: 想象一下你在等待外卖送到家。你打电话给外卖店订购食物,然后你必须一直在家等待,直到外卖送到。在这个过程中,你不能做其他事情,而是一直在等待外卖到达。

非阻塞IO: 现在你改变了策略,不再一直等待外卖到达,而是每隔一段时间就去看一次外面是否有外卖员。如果外卖员还没到,你就继续等待,否则就接收外卖。这样,你可以在等待外卖的同时做其他事情,不必一直停留在家中。

多路复用IO: 这次你和朋友一起等待外卖,你们决定轮流去看外面是否有外卖员。每个人都负责一段时间内的外卖观察,如果有外卖员到达,就通知其他人,然后大家一起去接收外卖。这样,大家可以共同等待外卖,而不必每个人都去看。

信号驱动IO: 现在你和朋友们一起等待外卖,但是你们并不一直在关注外面是否有外卖员,而是等外卖员到达后,外卖员会按门铃或敲门,通知你们外卖已经送到。这时,你们就会立即接收外卖。这样,你们可以在等待外卖的过程中做其他事情,不必一直注意外面的情况。

异步IO: 最后,你和朋友们订购了外卖,但是你们并不需要自己去等待外卖的到达。相反,你们委托了一个专门的外卖服务,他们会负责外卖的送达,并在外卖送到后通知你们。这样,你们可以完全不用关注外卖的送达过程,而是在等待的同时做其他的事情,直到外卖送到。

高级IO的几个重要概念

同步 VS 异步

  • 同步IO

    • 同步IO是指程序在执行IO操作时会阻塞(或等待),直到IO操作完成为止。
    • 在同步IO中,程序会按照顺序执行,每个IO操作都必须等待上一个IO操作完成后才能继续执行。
    • 例如,当程序调用读取文件的操作时,在读取完成之前,程序会一直等待,直到文件中的数据读取完毕后才能继续执行后续代码。
  • 异步IO

    • 异步IO是指程序在执行IO操作时不会阻塞,而是可以同时执行其他任务。
    • 在异步IO中,程序不必等待IO操作完成,而是可以继续执行其他任务,当IO操作完成时,会通过回调函数或其他方式来通知程序。
    • 例如,当程序调用异步读取文件的操作时,程序可以继续执行后续代码,不必等待文件读取完成,而是在文件读取完成后通过回调函数来处理读取的数据。

再简单的理解就是同步与异步的本质区别就是有没有参与IO的过程。异步不参与IO的过程,只是发起IO。同步则需要参与IO的过程。结合外卖的例子很好理解。五种IO模型中,除了异步IO,其余都是同步的。

注意: 这里的同步要与多线程中的线程同步进行区分,他们二者毫无关系。 线程同步是指在多线程编程中,通过一些机制来确保多个线程之间的协调和合作,以达到正确、可靠地共享资源和避免竞争条件的目的。

阻塞VS非阻塞

  • 阻塞IO(Blocking IO)

    • 阻塞IO是指程序在执行IO操作时会被挂起(或阻塞),直到IO操作完成为止。
    • 当程序调用阻塞IO操作时,程序会一直等待,直到IO操作完成或超时才能继续执行后续代码。
    • 例如,当程序调用读取文件的操作时,在读取完成之前,程序会一直等待,直到文件中的数据读取完毕后才能继续执行后续代码。
  • 非阻塞IO(Non-blocking IO)

    • 非阻塞IO是指程序在执行IO操作时不会被挂起,而是可以立即返回,继续执行后续代码。
    • 当程序调用非阻塞IO操作时,如果IO操作无法立即完成,操作会立即返回一个错误码或特定的状态,而不是等待IO操作完成。
    • 例如,当程序调用非阻塞读取文件的操作时,如果文件中没有数据可读,读取操作会立即返回一个错误码或特定的状态,而不会一直等待数据可读。

这里需要注意一点,阻塞式IO和非阻塞式IO他们的IO效率是一样的,只是等待的方式不同而已。非阻塞再等待的时候可以做其他的事情。

关于非阻塞IO的详细介绍:非阻塞I/O - 掘金 (juejin.cn)