网络IO模型

✏️ 1、网络IO

网络IO的本质就是socket流的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作,在网络通信中,对数据进行读写的过程中,数据在发送端和接收端的相应设备上进行如下传递:

发送端发送缓冲区->发送端驱动程序->发送端网卡->接收端网卡->接收端驱动程序->接收端接收缓冲区

对数据进行写入时,应用程序调用write/sendto等相关系统调用将数据发送到接收端的接收缓冲区;在读取数据时,应用程序调用read/recvfrom等相关系统调用将数据从接收缓冲区搬运到用户区。

通常一次IO读操作会涉及到两个对象和两个阶段。

两个对象分别是:

  • 用户进程(线程)Process(Thread)

  • 内核对象 Kernel

以数据的读取为例,在调用read等系统调用时,会经历以下两个阶段:

  • 等待流数据准备(wating for the data to be ready);

  • 从内核向进程复制数据(copying the data from the kernel to the process);

对于socket流而已:

  • 第一步通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。

  • 第二步把数据从内核缓冲区复制到应用进程缓冲区。

Richard Stevens的经典书籍UNP,书中给出5种IO模型:

  • blocking IO - 阻塞IO

  • non-blocking IO - 非阻塞IO

  • IO multiplexing - IO多路复用

  • signal driven IO - 信号驱动IO

  • asynchronous IO - 异步IO

✏️ 2、五种IO模型

🖋️ 2.1、阻塞IO

阻塞式IO在进行数据读取时,如果内核中没有数据(发送端可能还没有发送数据或者发送的数据还没有到达),此时内核就开始等待数据,同时用户进程也进入阻塞状态,整个进程就会被挂起等待,不能做其他的事情。当有数据到达内核时,内核等待结束,将数据从内核拷贝到用户区,用户进程结束阻塞,从挂起状态转为运行状态。

所以,阻塞式IO在进行数据读取时,上述两个阶段都会阻塞(内核等待数据,用户进程阻塞)。

在Linux中,默认所有的socket都是阻塞式的。阻塞式接口是指当进行系统调用时,如果数据没有准备好,该应用进程就会被挂起,系统调用不会返回,直到有数据到达或者调用出错时,系统调用才会返回,进程才会结束阻塞状态。

实际上几乎所有的IO接口(包含socket)都是阻塞的。如果在一个网络编程中,一个服务器需要处理多客户端的情形,如果是单进程的服务器。在与一个客户端连接建立之后,服务器就会使用read等系统调用对客户端进行数据读取来处理请求。当该客户端没有发送数据或者发送的数据还没有达到时,服务器就会进入阻塞状态,此时整个服务器进程就会挂起。当其他客户端连接请求达到时,服务器由于处于挂起状态爱,什么也不能做,所以也不能对其他客户端进行处理。因此,上述所说的单进程的阻塞式IO的服务器只能处理一个客户端的情况,所以这样的服务器没有任何的实用性。

所以,可以创建多进程或多线程(多个执行流)来处理多客户端的情形。如果在对一个客户端进行读取时导致一个进程被挂起,可以创建其他的进程来处理其他客户端的请求。但是多进程和多线程的创建也要浪费一定的资源,也有一定的局限性,所以一般适用于中小型应用场景。

🖋️ 2.2、非阻塞IO

在Linux中默认所有的套接字都是阻塞的,可以通过特定的系统调用来实现非阻塞的文件描述符。非阻塞式IO在使用recvfrom等系统调用进行数据读取时,如果内核中没有数据到达,此时内核会进行等待。但是与阻塞式IO不同的是,此时的用户进程并不会被阻塞,不会被挂起,而是立刻返回一个EWOULDBLOCK错误,并且errno被设置为EAGAIN。出错返回之后进程可以做其他的事情,但是之后内核将数据准备好之后,由于该系统调用已经返回,所以进程无法得到数据已经准备好并且无法将数据由内核拷贝到用户区。所以,此时还需要使用系统调用进行数据的拷贝。因为不知道内核什么时候将数据准备好,所以就需要不断的使用系统调用来询问内核有没有将数据准备好,一旦准备好就进行数据的拷贝。

非阻塞式IO中,一般需要循环的对文件描述符进行读写,不断的询问数据有没有准备好。这个过程就称为轮询。因此,在非阻塞IO中,内核在等待数据,用户进程在轮询访问数据有无准备好。在该模型中,进程大部分的工作都是在轮询访问,并没有发挥实际有效的作用,所以这样做实际是对CPU资源的一种浪费。一般在特定场景下才会使用该模型。

阻塞IO和非阻塞IO:

  • 区别:等待方式的不同。阻塞IO是进程挂起等待,非阻塞IO是进程轮询式访问等待

  • 相同:二者都是自己主动去查看就绪条件是否满足(数据是否达到内核),主动进行数据的拷贝。

🖋️ 2.3、 信号驱动IO

信号驱动IO中,当内核将数据准备好时,不是由进程调用read等系统调用来查看就绪条件是否满足,而是通过内核进程发送SIGIO信号通知应用进程数据已经准备好,此时应用进程就可以调用read等来进行数据的读取。所以,应用进程首先使用系统调用sigaction来建立SIGIO信号处理程序。然后该系统调用返回,内核进行数据的等待。此时应用进程可以做其他的事情。当内核中有数据到达时,操作系统会向进程发送SIGIO信号给应用进程,该信号被捕捉,此时应用进程便知道内核中有数据达到,所以在信号捕捉处理程序中调用read等系统调用进行数据的读取,将数据从内核拷贝到用户区。(注意,此时的文件描述符是阻塞式的)。

信号驱动IO和非阻塞IO:

  • 区别:就绪条件满足时的通知方式不同。信号驱动IO是通过信号来告知,非阻塞式IO要循环使用系统调用进行轮询查看。

  • 相同:二者在数据读取时都不会阻塞进程(信号驱动IO不阻塞是因为read时一定有数据,非阻塞IO不阻塞是因为没有数据时会直接出错返回)。并且二者都需要主动使用read等系统调用进行数据的拷贝。

🖋️ 2.4、 IO多路复用

IO多路复用与上述三种方式最大的不同在于它一次等待多个文件描述符,并且它将等待和数据的拷贝分隔开。在该方式下,应用进程首先使用select/epoll等系统调用等待多个文件描述符。select等系统调用可以设置阻塞和非阻塞(或者阻塞的时间)。如果是阻塞方式,若等待的所有文件描述符的数据均未达到,此时进程会阻塞在select处。当至少有一个文件描述符就绪条件满足时,该系统调用就会返回。(这里与阻塞方式的read非常类似,只是read还有进行数据拷贝,select只是进行等待,返回时将满足就绪条件的文件描述符返回,并不进行数据的拷贝)此时应用进程在调用read等分别对就绪的文件描述符进行数据的拷贝。如果是非阻塞方式或者设置了阻塞的时间,当没有调用select时或者在规定时间内没有文件描述符满足就绪条件,此时select会出错返回-1,此时为了对数据进行操作,所以必须循环的调用select来判断有无文件描述符满足就绪条件。(这里与非阻塞方式的read类似,不过与上述类似,select只负责等待,不负责拷贝)。

当等待的多个文件描述符中至少有一个满足就绪条件时,select返回。此时应用进程调用read(此时的套接字是阻塞的)等对数据进行拷贝。此时,read一定不会阻塞。因为内核中一定有数据到达。

如果在网络编程中,服务器采用的是IO多路复用的方式。当服务器分别将多个客户端的数据请求读取处理之后,有可能客户端还会就绪发送数据请求,所以服务器还需要在使用select来判断多个文件描述符上是否还有数据到达。因此,服务器必须循环的调用select等来不断的查看等待的文件描述符上是否有数据到达。然后进行不断地处理。

所以,对于单进程的服务器可以通过IO多路转接的方式来处理多客户端的情况,此时避免由于多进程或多线程的创建造成的资源方面的问题。但是两种方式各有优缺点,在具体使用时应择优选择。

信号驱动IO和IO多路复用:

  • 相同:就绪条件满足时,都不是自己去探测的,同时,都是主动调用read等进行数据拷贝。

  • 区别:信号驱动IO通过信号告知就绪条件满足,IO多路转接通过系统调用的返回值判断就绪条件是否满足。信号驱动IO一次只等待一个文件描述符,IO多路转接一次处理多个文件描述符。

🖋️ 2.5、异步IO

异步IO,应用进程提供一片缓冲区,使用特殊的系统调用aio_read。内核没有将数据准备好时,该系统调用直接返回。进程继续做其他的事情。此时内核等待数据,在数据到达后,将数据拷贝到用户空间完成后,再递交在aio_read中指定的信号,然后在对该信号进行捕捉时对数据进行处理。

该方式与上述几种方式不同点在于,进程使用的系统调用既没有进行数据的等待,又没有进行数据的拷贝。只是在数据拷贝完成之后通知应用进程,然后应用进程在进行处理。

注意与信号驱动IO的区别:信号驱动IO是在数据准备好之后通知应用进程调用相关的函数进行数据的拷贝。而该方式是在数据拷贝完成之后通知应用进程直接对数据进行处理。

在实际应用中尤其是在网络中进行数据通信时(长距离传输),等待消耗的时间往往高于拷贝的时间。所以在进行IO时大部分的时间都在等待。因此,要提高IO的效率,就要减少IO过程中等的比重。

✏️ 3、概念

🖋️ 3.1、阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果时的状态。

阻塞调用在没有等到调用结果时,会将整个进程挂起等待,此时进程不能干其他的事情。在得到调用结果之后,进程才会返回。非阻塞调用在没有等到调用结果时,会直接返回,不会挂起进程。此时进程可以做其他的事情。

🖋️ 3.2、同步通信与异步通信

同步和异步关注的是消息通信进制,通过什么方式回答结果。

  • 同步,调用后,如果没有结果,调用不返回任何消息。

  • 异步,调用后,直接返回,但并不是返回结果,被调用者,通过状态通知来通知调用者。

POSIX把这两个术语定义如下:

  • 同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成。

  • 异步I/O操作(asynchronous I/O operation)不导致请求进程阻塞。

同步通信和异步通信两者的区别在于在进行IO操作时,同步操作会阻塞。在上述的阻塞IO,非阻塞IO,信号驱动IO,IO多路转接中,都需要主动调用read等进行数据的拷贝,此时,在数据拷贝时进程是阻塞的。注意:非阻塞IO在等待的时候是不阻塞的,但调用read等进行数据拷贝时,也需要花费时间,此时进程也是阻塞的。只有异步IO在数据的等待和拷贝期间,进程都不阻塞。

所以,前四种模型都是同步通信,而异步IO是异步通信。

注意:在多线程和多进程中也有同步的概念。二者是完全不同的概念。线程和进程中的同步指的是多个线程或进程访问临界资源时,在执行顺序上有一定的次序。通过协调执行次序所产生的制约关系。

Last updated