网络 IO,会涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,另一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段:
-
等待数据准备就绪
-
将数据从内核拷贝到进程或者线程中。
因为在以上两个阶段上各有不同的情况,所以出现了五种网络 IO 模型。
- *定义*:在 linux 中,默认情况下所有的 socket 都是 blocking,例如listen()、send()、recv()等都是阻塞的。所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。
- *改进*:在服务器端使用多线程(或多进程),但没有特定的模式。进程的开销要远远大于线程,所以如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。使用 pthread_create ()创建新线程,fork()创建新进程。
- *问题:*上述多线程、多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。此时,很多人用线程池或连接池: “线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。 “连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。 但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,*所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。*
多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
*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()多路复用模式,可以一次检测多个连接是否活跃。
*(1)原理*:是 select/epoll 这个 function会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。 *(2)流程*:
这个图和 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。
Linux 下的 asynchronous IO 用在磁盘 IO 读写操作,不用于网络 IO,从内核 2.6 版本才开始引入。
用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel的角度,当它收到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。
***\*异步 IO 是真正非阻塞的,它不会对请求进程产生任何的阻塞,因此对高并发的网络服务器实现至关重要。\****
首先我们允许套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 read 读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它来读取数据报。
*优势*:在于等待数据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了 select 的阻塞与轮询,当有活跃套接字时,由注册的 handler 处理。
--------------------------------------------------------分割线---------------------------------------------------------------
- *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 操作的状态,也不需要主动的去拷贝数据。
原文作者:[当当响