- 网络IO模型浅析:编写一个web服务器应用程式(httpd、Nginx),几种常用的网络IO模型(IO模型为内核提供,开发的web应用程式调用这些模型来达到网络IO的目的,httpd调用实现的是IO多路复用):
-
-
数据的一次磁盘IO,都会由两阶段组成:
- 第一阶段:数据从磁盘复制到内核内存,即上图第3步
- 第二阶段:数据从内核内存复制到进程内存,即上图第4步
-
同步异步,阻塞非阻塞区别联系
-
实际上同步与异步是针对应用程式与内核的交互而言的,关注的是消息通知机制
-
同步:调用者进程发出调用后,调用者进程就处于等待状态,被调用者进程不会返回任何消息,但一旦返回结果,就是最终结果。
- 同步过程中进程触发IO操作并等待(也就是我们说的阻塞)或者轮询的去查看IO操作(也就是我们说的非阻塞)是否完成。 同步有阻塞和非阻塞之分
- 异步:调用者进程发出调用后,被调用进程立即返回消息,但返回的不是最终结果,即告诉调用者进程不必等待,调用者进程可以处理其他事情,被调用者进程处理完后,则将最终结果通过状态、通知机制等来通知调用者进程。异步一定是非阻塞的
-
同步:调用者进程发出调用后,调用者进程就处于等待状态,被调用者进程不会返回任何消息,但一旦返回结果,就是最终结果。
-
阻塞与非阻塞更关注的是调用者进程等待被调用者进程返回调用结果时的状态(即有无返回结果)
- 阻塞:调用结果返回之前,调用者进程会被挂起,调用者进程只有在得到返回结果之后才能继续后续的工作
- 非阻塞:调用者进程在结果返回之前,不会被挂起,即返回临时结果给调用者进程,让它继续处理后续的操作(隔断时间再来询问之前的操作是否完成。这样的非阻塞忙轮询,也算是非阻塞的一种,或者是直到被调用者进程处理完后,通知调用者进程,调用者进程才来拿结果,这样的完全非阻塞)
-
实际上同步与异步是针对应用程式与内核的交互而言的,关注的是消息通知机制
-
网络IO模型类型:(一个进程只能处理一个套接字的I/O事件)服务器端用户空间线程接到客户端发起的网络IO后,才会向内核空间发起网络IO,所以web服务器的进程需要处理这两路网络IO
-
阻塞I/O模型(BIO,Blocked IO):当用户空间进程在连接套接字中的缓冲区中接收到客户端的请求URL时,该程式(如httpd)在它所在的用户空间内存中分析后,即向内核发起read(socket, buffer)系统调用,这是用户空间进程被阻塞,内核开始准备数据,即将磁盘数据拷贝到内核socketbuffer中(此时用户空间是阻塞的),之后用户空间进程需要调用read读取socket中buffer的数据,内核read()函数会将数据返回(拷贝)给用户进程的内存空间(这个过程也是由内核来完成的拷贝,此时用户空间进程也是阻塞的)
- 内核空间的buffer和cache可以用free来查看
- 整个过程都是阻塞的,所以这样的IO模型效率很低
-
阻塞I/O模型(BIO,Blocked IO):当用户空间进程在连接套接字中的缓冲区中接收到客户端的请求URL时,该程式(如httpd)在它所在的用户空间内存中分析后,即向内核发起read(socket, buffer)系统调用,这是用户空间进程被阻塞,内核开始准备数据,即将磁盘数据拷贝到内核socketbuffer中(此时用户空间是阻塞的),之后用户空间进程需要调用read读取socket中buffer的数据,内核read()函数会将数据返回(拷贝)给用户进程的内存空间(这个过程也是由内核来完成的拷贝,此时用户空间进程也是阻塞的)
-
read(socket, buffer);
process(buffer);
- 在《Socket套接字.doc》中已经介绍了流和套接字的关系,我们知道内核将数据从磁盘拷贝到内核空间的过程中,因为数据都是流式的,所以每一次的磁盘IO的过程都是要通过内核来创建一个socket文件来表示这个流数据的,并返回一个fd给用户进程,故而在用户进程read(socket,buffer)时,通过fd就能知道读取的是哪个流数据
-
- 非阻塞I/O模型(NIO,Non-blocked IO):将 连接套接字 设置为NONBLOCK。当内核数据没有准备好时,内核立即返回EWOULDBLOCK错误给用户空间进程,此时用户空间进程便可以处理其他操作,但会不断发起系统调用read,一旦内核缓冲区的数据已经存在(即连接套接字中被写入数据),在用户进程发起最后一次read调用后,数据才复制到用户空间中处理封装应用层首部等,这样就是叫非阻塞忙轮询(polling)
-
while(read(socket, buffer) != SUCCESS)
process(buffer);
- 用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源
-
-
I/O复用模型(阻塞IO复用):httpd中的prefork和worker都是用的此模型
-
多路复用IO模型简介:
- 当web服务器进程向内核空间调用系统调用时,便处于阻塞状态被挂起,如果服务器端的磁盘读取很慢,客户端由于嫌响应速度过慢而取消这次操作(如ctrl+c),但是服务器进程处于挂起状态,便无法接收到客户端发出的取消信息的,所以是无法取消的。为了避免这种情况,后来在内核开发了一种多路IO复用的程式,便可以让用户及时取消操作
- 如果需要使用这种IO复用,用户空间进程不是调用read发起的IO请求交给内核,而是直接调用select或poll系统调用,用户空间进程会被阻塞等待select系统调用返回(用户空间进程阻塞在select上不同于阻塞在内核的磁盘IO上,它还可以接收其他信号进来比如网络IO请求,这样就可以让一个进程同时处理多个IO请求,select代理此时并不阻塞,可以接收其他请求),它会代替用户空间进程给内核发起IO请求,让内核准备数据,同时轮询所有socket是否有数据。当数据到达时,其中一个socket被激活,select或poll函数返回,用户空间进程才正式调用read读取socket中的数据,正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用。
- select代理在接收用户空间进程的请求时,请求的数量是有限制的,select程式要求不能超过1024个(这个数量是内核源码的合理设定,如果想要超过1024可以修改源码,不过超过1024未必有很好的性能),httpd程式的prefork模型中,只能承载1024个请求并发,就是因为prefork就是基于调用select来实现的,prefork的主进程将请求接入,然后生成一个子进程来响应,所以生成的子进程最多只能有1024个(当有1024个进程时,一个子进程刚好处理一个IO请求),如果请求超过上限就可能会拒绝掉请求
- 当一个用户进程处理多个网络连接IO时,就会等待多个流数据,而select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听。当select函数返回后,用户空间进程从select那里仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),用户空间进程只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
-
多路复用IO模型简介:
-
select(socket);
while(1) {
sockets = select();
for(socket in sockets) {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
- while循环前将socket添加到select监视中,然后在while内select一直调用read获取被激活的socket,一旦socket可读,read函数将socket中的数据读取出来
-
-
信号驱动I/O模型(signal driven I/O, SIGIO):虽然上述方式允许单线程内处理多个IO请求,但是用户空间进程将每个IO请求交给代理select后,用户空间进程还是阻塞的(阻塞在select函数上),平均时间甚至比同步阻塞IO模型还要长。那么就有了事件驱动型IO,httpd的event模型和nginx用的此种IO模型
-
IO多路复用模型使用了Reactor设计模式实现了这一机制
- 通过Reactor的方式,用户线程的IO请求统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步,用户空间进程非阻塞),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,Reactor线程会将被激活的socket标记处理,通知给用户空间进程,知道是哪个socket有数据,方便read数据。之后用户空间进程通过系统调用read来获取数据(此阶段还是阻塞的)
- 由于数据读取的第一阶段,用户空间进程是非阻塞的,这样一个进程就能处理多个客户端的IO请求
- epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知用户空间进程。所以说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
- select和epoll最大的区别就是:select只是告诉用户空间进程一定数目的流有事件了,至于哪个流有事件,还得一个一个地去轮询,而epoll会把发生的事件关联上fd之后告诉用户空间进程,通过发生的事件,就自然而然定位到哪个流了
-
当一个进程的第一次IO请求的数据准备好后,进程会进入调用read的阻塞状态,这时这个进程的第二次IO请求的数据也准备好了,那么内核在通知该进程就通知不到,进程如果没有接收消息,消息会自动消失,此时就有了水平触发和边缘触发两种通知机制:
- 水平触发通知多次,通知到进程来调用read处理数据为止
- 边缘触发通知一次,进程之后通过回调函数来调用read处理数据
-
IO多路复用模型使用了Reactor设计模式实现了这一机制
-
-
异步I/O模型(AIO, asynchronous I/O):
- 用户空间进程发起read操作之后,立刻就可以开始去做其它的事。而从kernel的角度看,收到用户空间进程的asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block
- 然后,kernel会将数据准备完成,即从磁盘拷贝到内核空间,然后再从内核空间拷贝到用户空间内存,当这一切都完成之后,kernel才会给用户进程发送一个signal,告诉它read操作完成了
- 整个过程用户空间进程没有任何阻塞
-
-
总结:
-
阻塞、非阻塞、多路IO复用,都是同步IO;异步必定是非阻塞的;
- 真正的异步IO需要CPU的深度参与。换句话说,只有用户线程在操作IO的时候根本不去考虑IO的执行全部都交给CPU去完成,而自己只等待一个完成信号的时候,才是真正的异步IO。所以,拉一个子线程去轮询、去死循环,或者使用select、poll、epool,都不是异步。
- 同步:不管是BIO,NIO,还是IO多路复用,第二步数据从内核缓存写入用户缓存,一定是由用户空间进程自行写入用户空间缓存,再处理数据。
- 异步:第二步数据是内核写入的,并放在了用户线程指定的缓存区,写入完毕后通知用户线程。
-
阻塞、非阻塞、多路IO复用,都是同步IO;异步必定是非阻塞的;
-
本文来自投稿,不代表Linux运维部落立场,如若转载,请注明出处:http://www.178linux.com/95575