文章目录
- 1. 基本概念
- 1.1 阻塞与非阻塞
- 1.2 同步与异步
- 1.3 为什么没有“异步阻塞”
- 2. 五种IO模型
- 2.1 阻塞 blocking
- 2.2 非阻塞 non-blocking
- 2.3. IO复用(IO multiplexing)
- 2.4 信号驱动(signal-driven)
- 2.5 异步(asynchronous)
1. 基本概念
首先回顾并理解一下网络IO中阻塞、非阻塞;同步、异步的概念。
首先我们要知道:一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”。
阻塞与非阻塞是“数据就绪”阶段的概念,同步与异步是“数据读写”阶段的概念。
1.1 阻塞与非阻塞
所谓阻塞,就是IO在没有准备好数据的时候,调用IO方法的线程会被阻塞,直至收到数据时解除阻塞;而非阻塞IO在没有准备好数据的时候IO方法一样会返回,线程不会被阻塞。以recv方法为例,我们一般这样调用:
int size = recv(sockfd, buf, 1024, 0);
我们创建的sockfd默认是阻塞的,那么当执行这个函数时,如果sockfd对应的socket上数据没有就绪(即TCP接收缓存区中无数据),则该线程会阻塞,再往下的代码不会执行;但如果我们使用fcntl设置sockfd对应的socket为非阻塞,该socket工作在非阻塞状态下,那么执行该函数时,无论是否有数据准备好,该函数都会返回,然后我们可以通过返回的size值和errno来判断是否读到数据:
size == -1 && errno == EAGAIN: 表示没有读到数据,是因为非阻塞才返回;
size == 0:表示对端断开连接;
size > 0 : 表示读到数据,size为数据的长度;
1.2 同步与异步
再来说网络IO的同步和异步。刚才我们讨论阻塞和非阻塞时,关注的是数据准备阶段且数据没有准备好的情况,现在我们讨论同步和异步,关注的是数据已经准备好(TCP接收缓存区中有数据),进入数据读写阶段时的情况。
同步:同步表示A向B请求调用一个网络IO接口时,数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞)
比如调用recv方法(recv是同步IO方法)的时候其检测到数据已经准备好,然后把数据从内核的TCP接受缓存区拷贝到我们传入的buf(recv方法的第二个参数)中,这一拷贝过程耗费的是调用recv的线程的时间,直至拷贝结束,recv才返回;
异步:异步表示A向B请求调用一个网络IO接口时,向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。
比如某个应用程序调用了一个异步IO的API来处理数据读入(如aio_read),那么应用程序只需把需要监听的sockfd给内核传入,内核来负责数据的准备和读入。在此期间,应用程序可以做自己的事情。数据读入完毕以后,再通过信号等方式来通知应用程序。
1.3 为什么没有“异步阻塞”
把阻塞、非阻塞,同步、异步两两组合有:
- 同步阻塞
- 同步非阻塞
- 异步阻塞
- 异步非阻塞
其中,所有平时使用的Linux网络IO的API都是同步的(如write、read、recv等等),我们可以通过设置其关注的socket为阻塞或者非阻塞,这样就有了同步阻塞和同步非阻塞的API。
但是异步的API一定是非阻塞的,因为异步IO是要让内核帮我们完成数据的读写,让调用它的应用程序可以去做自己的事情,如果异步的同时又阻塞,那么应用程序无法做自己的事情,异步就失去了意义。
2. 五种IO模型
有了以上的知识铺垫,再学习Linux的五种IO模型就通畅很多。
2.1 阻塞 blocking
调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。下图以read为例。
2.2 非阻塞 non-blocking
非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN。
2.3. IO复用(IO multiplexing)
Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数据可读或可写时,才真正调用IO操作函数。
2.4 信号驱动(signal-driven)
Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO 信号,然后处理 IO 事件。
注意区分信号驱动和异步IO的区别:信号驱动中,内核在数据准备阶段是异步,但在数据读写阶段是同步,仍然要花费应用程序自己的时间;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。
2.5 异步(asynchronous)
Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。从图中可以看出,异步IO是内核负责把数据从内核空间拷贝到用户空间,不耗费应用程序自己的时间,而前面四种IO方式在数据读写阶段都需要应用程序自己负责。