[C++项目] Web Server(8):网络IO模型

一个典型的网络IO可以分为两个阶段

  • 数据就绪
  • 数据读写

阻塞/非阻塞同步/异步IO的区别就是在这两个阶段体现的

注意 阻塞/非阻塞同步/异步 两者之间是相互独立

阻塞并不代表同步,异步也不一定非阻塞(不过一般来说,异步IO接口都会设计为非阻塞)

阻塞/非阻塞

在数据就绪阶段,根据数据未到达前的处理方式可以分为阻塞与非阻塞

  • 阻塞方式:若数据未就绪,调用IO方法的线程进入阻塞状态

  • 非阻塞方式:不会改变线程的状态,通过返回值判断是否有数据到达

通过对文件描述符fd可以设置其阻塞与非阻塞属性

以recv函数为例

// 函数声明
ssize_ t recv(int sockfd, void *buf, size_ t len, int flags);

//使用函数
int size = recv(sockfd, buf, 1024, 0);

阻塞情况下,若没有数据到达,则程序被挂起,直至有数据到达

  • size==-1, 表示出错了
  • size==0, 对方连接关闭
  • size>0, 读取到了多少的数据

非阻塞情况下,无论有没有数据都会直接返回,根据其返回值判断数据就绪情况:

  • size==-1, 表示出错了。但是有三种错误号并不是错误,需要单独判断:
    • EINTR:表示程序中断返回,并不属于错误
    • EAGAIN/EWOULDBLOCK:表示非阻塞情况下的数据已读完,并不属于错误
  • size==0, 对方连接关闭
  • size>0, 读取到了多少的数据

同步/异步

在数据读写阶段,根据读写数据是否需要等待,也即根据数据读写的方式分为同步和异步方式

  • 同步方式:代码中主动调用同步接口读写,在读写完成前程序都必须等待。recv函数就是一个典型的同步接口。同步的代码编写相对简单。注意,IO多路复用(selcet/poll/epoll)都是同步的,它们是针对数据就绪阶段的设计。

  • 非阻塞方式:代码中调用异步接口读写,将读写的任务委托给操作系统,程序可以继续往下执行,在读写任务完成后系统会按照传递的通知方式(一般为信号)主动通知程序。比如,aio_readaio_write就是linux下常用的异步接口。

在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用特殊的API接口的才是异步IO。这种特殊的IO在Linux中一般为AIO,在windows下为IOCP,在.NET中为BeginInvoke/EndInvoke

具体来说:同步表示A向B请求调用一个网络IO接口时 (或者调用某个业务逻辑API接口时),数据的读写都是 由请求方A自己来完成的(不管是阻塞还是非阻塞);

异步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。

Linux上的五种IO模型

在Linux系统中,常用的五种IO模型包括:

  • 阻塞IO模型(Blocking IO Model):在阻塞IO模型中,当应用程序发起IO操作时,程序会一直等待,直到IO操作完成并返回结果。在此期间,程序无法执行其他任务。阻塞IO模型是最简单的IO模型,但是会导致程序的性能较低。

  • 非阻塞IO模型(Non-blocking IO Model):在非阻塞IO模型中,当应用程序发起IO操作时,程序会立即返回,而不是等待IO操作完成。在此期间,程序可以执行其他任务。应用程序需要不断地轮询IO操作的状态,直到IO操作完成并返回结果。非阻塞IO模型可以提高程序的性能,但是需要编写复杂的轮询代码。

  • IO多路复用模型(IO Multiplexing Model):在IO复用模型中,应用程序使用select或poll等系统调用来监听多个IO操作的状态。当有IO操作完成时,程序会被唤醒,并处理已完成的IO操作。IO复用模型可以同时处理多个IO操作,提高程序的性能。

  • 信号驱动IO模型(Signal-driven IO Model):在信号驱动IO模型中,应用程序使用sigaction系统调用来注册一个信号处理函数。当IO操作完成时,系统会向应用程序发送一个信号,唤醒信号处理函数来处理已完成的IO操作。信号驱动IO模型可以提高程序的性能,但是需要编写复杂的信号处理代码。

  • 异步IO模型(Asynchronous IO Model):在异步IO模型中,应用程序发起IO操作后,程序可以继续执行其他任务,而不需要等待IO操作完成。当IO操作完成时,系统会通知应用程序,并处理已完成的IO操作。异步IO模型可以提高程序的性能,但是需要编写复杂的异步回调函数。

下面具体介绍下每种IO模型

阻塞IO

调用者调用了某个函数,然后就等待这个函数返回,期间什么也不做,系统内部将会不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。

非阻塞IO

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。

非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据errno区分这两种情况

对于accept, recvsend, 事件未发生时, errno通常被设置成EAGAIN

IO复用

Linux用select/poll/epoll函数实现IO复用模型,这些函数也会使进程阻塞,但是和阻塞IO不同的是这些函数可以同时阻塞多个IO操作

而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或可写时,才真正调用IO操作函数。

IO复用并不能用于处理高并发的情况,它的作用是在单进程单线程的情况下能一次检测多个IO事件。

解决高并发还是得靠多进程/多线程。

信号驱动IO

Linux用套接口进行信号驱动IO,通过注册安装一个信号处理函数,数据就绪前进程继续运行并不阻塞,当IO事件就绪时,进程会收到SIGIO信号,接着在信号处理函数中处理IO事件。

内核在第一个阶段是异步, 在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需
要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

异步IO

Linux中,可以调用异步IO接口来执行异步IO。比如,调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

这里介绍下aio_read

#include <aio.h>

int aio_read(struct aiocb *aiocbp);


struct aiocb {
  /* The order of these fields is implementation-dependent */
  int             aio_fildes;     /* File descriptor */
  off_t           aio_offset;     /* File offset */
  volatile void  *aio_buf;        /* Location of buffer */
  size_t          aio_nbytes;     /* Length of transfer */
  int             aio_reqprio;    /* Request priority */
  struct sigevent aio_sigevent;   /* Notification method */
  int             aio_lio_opcode; /* Operation to be performed; lio_listio() only */

  /* Various implementation-internal fields not shown */
};

/* Operation codes for 'aio_lio_opcode': */
enum { LIO_READ, LIO_WRITE, LIO_NOP };