IO模型及Nginx架构流程概述
文章目录
第一章 概述
首先来说nginx高性能的其中一个原因,就是使用了一种高效的I/O模型,先来梳理一下所有的网络模型,本章节图文都是参考Stevens的《Unix Network Programming》,英文好的可以直接看原版I/O Models章节,网络模型总共分一下五种类型:
第二章 五种IO 网络模型
1. 阻塞IO(blocking I/O)
从图宏观来看,左侧为应用程序,右侧为内核,当应用程序的进程使用recvform函数发起对内核的调用,内核开始准备数据,一般来说内核需要收到一个完整的数据包(如UDP),不会很快,因此应用程序的进程处于阻塞等待的状态,待内核将数据准备好,它将数据从内核拷贝到用户内存,内核才会返回结果,然后应用程序解除阻塞状态,去处理下一步请求,从本次过程,做过开发的基本都能理解,平时程序都是属于这种阻塞的同步调用方式。
2. 非阻塞IO(nonblocking I/O)
从图可以看出,应用程序进程当使用recvform函数对内核发起调用时,内核立即返回一个数据没有准备好的结果,进程通过轮询不断的去请求内核,询问数据是否给老子准备好了,知道内核返回结果,说已经准备好了,进程才结束轮询。
跟阻塞IO对比,进程本地不必阻塞等待,而是隔三差五的通过轮询调用,在这个过程中,会大量的占用CPU的时间,所以一般Web服务器都不使用这种I/O模型。
3. 多路复用IO(I/O multiplexing (select、poll、epoll))
多路复用模型新增了几个函数:select、poll、epoll(Linux2.6以后的内核开始支持),这几个函数也会使进程阻塞,但是和阻塞I/O所不同的,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行监测,直到有数据可读或可写时,才真正调用I/O操作函数,说直白一点就是他们可以监控多个内核的IO操作,一个顶仨,一个select会监测多个socket,当内核准备好其中一个数据时,select会立即通知进行,赶快使用recvform调用内核,让内核抓紧开始拷贝数据到用户内存,如此,它比阻塞IO好处就是一次能处理多个连接,而阻塞IO只能处理一个。
其中还有一个点,就是epoll、poll要比select要高级一点,他们是无轮询的,因为他们用callback,select需要通过遍历Socket来完成调度,如果socket多,那肯定是需要浪费CPU时间的,而epoll和poll使用回调函数,给套接字注册个回调函数,当他们活跃时,自动完成相关操作,就避免了轮询,这些函数实际是阻塞进程的。
-
select模型:
说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。PHP 中有内置的函数来完成 select 系统调用(socket_select)。
缺点:有打开文件限制,通过轮训检测文件状态
-
poll模型:
poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。
缺点: 同select一样,通过轮训来检测文件状态
-
epoll模型:
epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用我们提前设置的回调函数。而 poll 和 select 都是遍历。
解决了select和poll模型的缺点
4. 信号驱动IO(signal driven I/O (SIGIO))
信号驱动IO,应用程序进程建立SIGIO处理函数调用内核,内核会立即返回数据没有准备好的信号,进程不再阻塞,待内核准备好后,发送SIGIO信号给用户进程,然后用户进程通过阻塞的方式使用recvform函数调用内核,让内核把数据拷贝到用户内存,并返回拷贝结果。看到此处是不是似曾相识,其实这有点回调的意思了,赶快去翻翻本博客Java回调机制解析的知识点吧。
5. 异步IO(asynchronous I/O)
异步IO,也就是AIO,这个之前有篇文章MINA时,也提过异步IO的,看图说话,这个过程其实就非常简单,应用程序进程调用内核,内核返回数据没有准备好消息,进程继续去干别的事情,待内核准备好数据,并且拷贝到用户内存,才通知进程已完成数据拷贝,因此,可以看出这种IO方式是效率最高的。
第三章 汇总
综上所述,可以看出,越往后,阻塞越少,理论上效率也是最好的,其中五种I/O模型中,前三种属于同步I/O,后两者属于异步I/O,到此同步和异步的含义也就不言而喻。目前nginx主要是基于多路复用IO来处理,至于为啥不能使用异步IO,这得看操作系统的支持程度的。
第四章 libevent
1. libevent外链知识点
Libevent友情知识:https://github.com/libevent/libevent
2. libevent的运转流程
支持Libevent运转的就是一个大循环,这个主循环体现在event_base_loop(Event.c/1533)函数里,该函数的执行流程如下:
图1 event_base_loop主循环
上图的简单描述就是:
(1)校正系统当前时间。
(2)将当前时间与存放时间的最小堆中的时间依次进行比较,将所有时间小于当前时间的定时器事件从堆中取出来加入到活动事件队列中。
(3)调用I/O封装(比如:Epoll)的事件分发函数dispatch函数,以当前时间与时间堆中的最小值之间的差值(最小堆取最小值复杂度为O(1))作为Epoll/epoll_wait(Epoll.c/dispatch/407)的timeout值,在其中将触发的I/O和信号事件加入到活动事件队列中。
(4)调用函数event_process_active(Event.c/1406)遍历活动事件队列,依次调用注册的回调函数处理相应事件。
3. 在 PHP 中 libevent 拓展实现 epoll。
libevent 是一个用C语言写的,基于事件驱动的高性能网络库。支持多种 I/O 多路复用技术,epoll、 poll、 dev/poll、 select 和 kqueue 等。 libevent 同时为文件描述符、信号、超时设定等事件提供了监听回调。所以这种编程方式也可以说是事件编程。
附上event_base_loop源码如下:
int event_base_loop(struct event_base *base, int flags){
const struct eventop *evsel = base->evsel;
struct timeval tv;
struct timeval *tv_p;
int res, done, retval = 0;
/* Grab the lock. We will release it inside evsel.dispatch, and again
* as we invoke user callbacks. */
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
if (base->running_loop) {
event_warnx("%s: reentrant invocation. Only one event_base_loop"
" can run on each event_base at once.", __func__);
EVBASE_RELEASE_LOCK(base, th_base_lock);
return -1;
}
base->running_loop = 1;
clear_time_cache(base);
if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
evsig_set_base(base);
done = 0;
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
base->th_owner_id = EVTHREAD_GET_ID();
#endif
base->event_gotterm = base->event_break = 0;
while (!done) {
base->event_continue = 0;
/* Terminate the loop if we have been asked to */
if (base->event_gotterm) {
break;
}
if (base->event_break) {
break;
}
timeout_correct(base, &tv);
tv_p = &tv;
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
/*
* if we have active events, we just poll new events
* without waiting.
*/
evutil_timerclear(&tv);
}
/* If we have no events, we just exit */
if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
event_debug(("%s: no events registered.", __func__));
retval = 1;
goto done;
}
/* update last old time */
gettime(base, &base->event_tv);
clear_time_cache(base);
res = evsel->dispatch(base, tv_p);
if (res == -1) {
event_debug(("%s: dispatch returned unsuccessfully.",
__func__));
retval = -1;
goto done;
}
update_time_cache(base);
timeout_process(base);
if (N_ACTIVE_CALLBACKS(base)) {
int n = event_process_active(base);
if ((flags & EVLOOP_ONCE)
&& N_ACTIVE_CALLBACKS(base) == 0
&& n != 0)
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
event_debug(("%s: asked to terminate loop.", __func__));
done:
clear_time_cache(base);
base->running_loop = 0;
EVBASE_RELEASE_LOCK(base, th_base_lock);
return (retval);
}