# 【NO.473】常使用的网络IO管理

  网络 IO，会涉及到两个系统对象，一个是用户空间调用 IO 的进程或者线程，另一个是内核空间的内核系统，比如发生 IO 操作 read 时，它会经历两个阶段：

1. 等待数据准备就绪

2. 将数据从内核拷贝到进程或者线程中。

    因为在以上两个阶段上各有不同的情况，所以出现了五种网络 IO 模型。

## 1.阻塞IO（blocking IO）

1. ***\*定义\****：在 linux 中，默认情况下所有的 socket 都是 blocking，例如listen()、send()、recv()等都是阻塞的。所谓阻塞型接口是指系统调用（一般是 IO 接口）不返回调用结果并让当前线程一直阻塞，只有当该系统调用获得结果或者超时出错时才返回。
   ![img](https://img-blog.csdnimg.cn/0e323315ed994b0ca48b55847d9ee45b.png)
2. ***\*改进\****：在服务器端使用多线程（或多进程），但没有特定的模式。进程的开销要远远大于线程，所以如果需要同时为较多的客户机提供服务，则不推荐使用多进程；如果单个服务执行体需要消耗较多的 CPU 资源，譬如需要进行大规模或长时间的数据运算或文件访问，则进程较为安全。使用 pthread_create ()创建新线程，fork()创建新进程。
3. ***\*问题：\****上述多线程、多进程都会严重占据系统资源，降低系统对外界响应效率，而线程与进程本身也更容易进入假死状态。此时，很多人用线程池或连接池：
   “线程池”旨在减少创建和销毁线程的频率，其维持一定合理数量的线程，并让空闲的线程重新承担新的执行任务。
   “连接池”维持连接的缓存池，尽量重用已有的连接、减少创建和关闭连接的频率。
   但是，“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且，***\*所谓“池”始终有其上限，当请求大大超过上限时，“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模，并根据响应规模调整“池”的大小。\****

## 2.非阻塞 IO（non-blocking IO）

    多线程模型可以方便高效的解决小规模的服务请求，但面对大规模的服务请求，多线程模型也会遇到瓶颈，可以用非阻塞接口来尝试解决这个问题。
![img](https://img-blog.csdnimg.cn/9df1c5fc90094240aaf75a55e31b081d.png)

 

***\*1. 定义：\****

从用户进程角度讲 ，它发起一个read 操作后，并不需要等待，而是马上就得到了一个结果。所以，在非阻塞式 IO 中，用户进程其实是需要不断的主动询问 kernel数据准备好了没有。

***\*recv返回值：\****
在非阻塞状态下，recv() 接口在被调用后立即返回，返回值代表了不同的含义。如在本例中：

\* recv() 返回值大于 0，表示接受数据完毕，返回值即是接受到的字节数；

\* recv() 返回 0，表示连接已经正常断开；

\* recv() 返回 -1，且 errno 等于 EAGAIN，表示 recv 操作还没执行完成；

\* recv() 返回 -1，且 errno 不等于 EAGAIN，表示 recv 操作遇到系统错误 errno。

***\*2. 设置非阻塞：\****
    非阻塞的接口相比于阻塞型接口的显著差异在于，在被调用之后立即返回。使用如下的函数可以将某句柄 fd 设为非阻塞状态。
fcntl( fd, F_SETFL, O_NONBLOCK );

***\*3. 问题：\****
    上述非阻塞模型绝不被推荐。因为，循环调用 recv()将大幅度推高CPU占用率；此外，在这个方案中 recv()更多的是起到检测“操作是否完成”的作用，实际操作系统提供了更为高效的检测“操作是否完成“作用的接口，例如 select()多路复用模式，可以一次检测多个连接是否活跃。

## 3.多路复用 IO（IO multiplexing）

***\*（1）原理\****：是 select/epoll 这个 function会不断的轮询所负责的所有 socket，当某个 socket 有数据到达了，就通知用户进程。
***\*（2）流程\****：

![img](https://img-blog.csdnimg.cn/881aa7a86f21484191aaac44eb3bb5c4.png)

    这个图和 blocking IO 的图其实并没有太大的不同，事实上还更差一些。因为这里需要使用两个系统调用(select 和 read)，而 blocking IO 只调用了一个系统调用(read)。但是使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。
所以，如果处理的连接数不是很高的话，使用select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好，可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快，而是在于能处理更多的连接。
***\*（3）问题\****：

首先 select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时，select()接口本身需要消耗大量时间去轮询各个句柄。

其次，该模型将事件探测和事件响应夹杂在一起，一旦事件响应的执行体庞大，则对整个模型是灾难性的。

***\*（4）解决方案\****：

有很多高效的事件驱动库可以屏蔽上述的困难，常见的事件驱动库有libevent 库，还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口，并且加入了信号(signal) 等技术以支持异步响应，这使得这些库成为构建事件驱动模型的不二选择。

实际上，Linux 内核从 2.6 开始，也引入了支持异步响应的 IO 操作，如 aio_read, aio_write，这就是异步 IO。

## **4.异步 IO（Asynchronous I/O）**

    Linux 下的 asynchronous IO 用在磁盘 IO 读写操作，不用于网络 IO，从内核 2.6 版本才开始引入。

***\*1. 流程：\****
![img](https://img-blog.csdnimg.cn/9aa2853567f447a28c4dfa367ba57158.png)

    用户进程发起 read 操作之后，立刻就可以开始去做其它的事。而另一方面，从 kernel的角度，当它收到一个 asynchronous read 之后，首先它会立刻返回，所以不会对用户进程产生任何 block。然后，kernel 会等待数据准备完成，然后将数据拷贝到用户内存，当这一切都完成之后，kernel 会给用户进程发送一个 signal，告诉它 read 操作完成了。
    
    ***\*异步 IO 是真正非阻塞的，它不会对请求进程产生任何的阻塞，因此对高并发的网络服务器实现至关重要。\****

## **5.信号驱动 IO（signal driven I/O，SIGIO）**

![img](https://img-blog.csdnimg.cn/1873e0411c2f47978e39b2827ffb1977.png)

首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数，进程继续运行并不阻塞。当数据准备好时，进程会收到一个 SIGIO 信号，可以在信号处理函数中调用 I/O 操作函数处理数据。当数据报准备好读取时，内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 read 读取数据报，并通知主循环数据已准备好待处理，也可以立即通知主循环，让它来读取数据报。

***\*优势\****：在于等待数据报到达(第一阶段)期间，进程可以继续执行，不被阻塞。免去了 select 的阻塞与轮询，当有活跃套接字时，由注册的 handler 处理。

 

--------------------------------------------------------分割线---------------------------------------------------------------

 

## 6.总结

1. ***\*blocking 和 non-blocking 的区别在哪，synchronous IO 和 asynchronous IO 的区别在哪\*******\*？\*******\*non-blocking\*******\*和\*******\*asynchronous IO 的区别在哪\*******\*？\****
   （1）blocking 和 non-blocking：调用 blocking IO 会一直 block 住对应的进程直到操作完成，而non-blocking IO 在 kernel 还在准备数据的情况下会立刻返回。
   （2）synchronous IO 做”IO operation”的时候会将 process 阻塞。按照这个定义，之前所述的 blocking IO，non-blocking IO，IO multiplexing 都属于synchronous IO。但要注意：
       有人可能会说，non-blocking IO 并没有被 block 啊。这里有个非常“狡猾”的地方，定义中所指的”IO operation”是指真实的 IO 操作，就是例子中的 read 这个系统调用。non-blocking IO 在执行 read 这个系统调用的时候，如果 kernel 的数据没有准备好，这时候不会 block 进程。但是当 kernel 中数据准备好的时候，read 会将数据从 kernel 拷贝到用户内存中，这个时候进程是被 block 了，在这段时间内进程是被 block的。而 asynchronous IO 则不一样，当进程发起 IO 操作之后，就直接返回再也不理睬了，直到 kernel 发送一个信号，告诉进程说 IO 完成。在这整个过程中，进程完全没有被 block。
   （3）在non-blocking IO 中，虽然进程大部分时间都不会被 block，但是它仍然要求进程去主动的 check，并且当数据准备完成以后，也需要进程主动的再次调用 recvfrom 来将数据拷贝到用户内存。而 asynchronous IO 则完全不同。它就像是用户进程将整个 IO 操作交给了他人（kernel）完成，然后他人做完后发信号通知。在此期间，用户进程不需要去检查 IO 操作的状态，也不需要主动的去拷贝数据。

原文作者：[[当当响](https://blog.csdn.net/zhpcsdn921011)

原文链接：https://bbs.csdn.net/topics/606367359