最近在学习Nginx,对其使用的异步非阻塞不太懂,查阅了许多关于同步异步,阻塞非阻塞的相关文章,以下作一个总结,以便以后查阅。参考文档地址位于文后。
本文讨论背景为Linux环境下的Network IO。
对于一个Network IO而言,应用程序process(or thread)向系统 内核kernel发起IO 调用,涉及到两个步骤:
- 等待数据准备完成(waiting for data for ready)
- 将数据从内核拷贝到进程中(copying data from the kernel to the process)
不同的IO模型在这两个步骤的处理上不同。
Richard Stevens在”Unix Network programing Volume 1: Third Edtion: The Sockets Networking”一书中介绍了五种IO模型:
- BIO: Blocking IO,阻塞IO
- NIO: Non-Blocking IO ,非阻塞IO
- IO Multiplexing: IO复用
- Signal driven IO: 信号驱动IO,
- AIO: Asynchronous IO,异步IO
以下将分作介绍。
linx中,默认所有的socket都是阻塞的,一个典型的读操作流程如下:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
IO multiplexing: IO多路复用,指单个进程(process or thread)能同时监控多个Socket。 上面的BIO,NIO都是一个process 管理 一个socket,但socket连接过多时,系统会产生多个process,一定程序上会耗费系统资源。
基本原理: select注册感兴趣的事件,监控多个socket,当有socket数据到达时,通知应用程序(process)进行处理。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
进程 在系统将数据从内核拷贝到内存时被阻塞,即process需要阻塞至系统IO操作完成。
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
linux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
阻塞与非阻塞: 针对以上第一个步骤而言的,应用程序发起recvfrom系统调用时,根据应用程序是否被阻塞来进行区分。阻塞情况下,process一旦发起recvfrom系统调用,立马被阻塞住;非阻塞情况下,process发起recvfrom系统调用时,kernel会立马返回数据是否准备好,此时process并不会被阻塞。
同步与异步:同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪;异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。
**同步非阻塞IO:**process发起recvfrom系统调用时,process此时不会阻塞住,kernel会立马返回数据是否准备好(没准备好返回-1,EWOULBLOCK/EAGIN),此时process可能需要不停的询问数据是否准备好。如果数据准备好了,此时 process阻塞住,直到kernel将数据从内核拷贝至内存(IO operation complete)。
**异步非阻塞IO:**process发起recvfrom系统调用时,kernel立马返回数据是否准备完成,此时process可以去做其他事情了,kernel会等待至数据准备成功,将数据 从内核拷贝至内存,完成后,向process发送信号,表明数据已经拷贝至内存了,可以去内存中取了,process直接从内存中取数据 即可。
select: 观察许多流的IO事件,空闲时,会将当前线程阻塞住,当有一个或多个流有IO事件时,从阻塞中醒来,伪代码如下:
while true {
select(streams[])
for i in streams[]{
if i has data
read until unavailable
}
}
特点: 无差别轮询,只是知道哪几个流有IO事件发生了,但并不知道是哪几个流,所以每次都得对所有流进行轮询,复杂度为o()n
select不是线程安全的,如果一个sock加入到select后, 突然另外一个线程发现这个sock不用,要收回,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
select有最大连接数限制,1024。
与select类似,解决了select中一些问题,没有最大连接数限制。
epoll: event poll, epoll也select类似,只不过epoll知道是哪几个流发生了IO事件,复杂度为o(k), k为发生IO事件的流的个数。
- epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
- epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
- epoll_wait(epollfd,…)等待直到注册的事件发生
(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)
伪代码如下
while true{
active_stream[]=epoll_wait(epollfd)
for i in active_stream[]{
read or write till unavailable
}
}
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
- epoll 现在是线程安全的。
- epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
- 可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。
epoll支持水平触发和边缘触发,最大特点在于边缘触发,只告诉进程哪些fd刚刚变为刚需态,并且只会通知一次,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
参考地址:
[1] 作者:蓝形参 链接:http://www.zhihu.com/question/20122137/answer/14049112
[2] 作者:罗志宇 链接:http://www.zhihu.com/question/32163005/answer/55772739
[3] http://blog.csdn.net/turkeyzhou/article/details/8504554
[4] http://blog.csdn.net/historyasamirror/article/details/5778378