所选代码为nginx1.13.12。各个版本会有差别,但是原理都差不多,就是在单进程(也是单线程)上,使用多路复用,把I/O,timer与信号处理都统一起来。
主流程
一旦worker开始运行,就要进入一个循环,除非要退出,否则一直都在执行ngx_process_events_and_timer()函数。
for ( ;; ) {
......
ngx_process_events_and_timers(cycle);
....
}
看看ngx_process_events_and_timers。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
}
if (ngx_use_accept_mutex) {
.......
}
delta = ngx_current_msec;
(void) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
if (delta) {
ngx_event_expire_timers();
}
ngx_event_process_posted(cycle, &ngx_posted_events);
}
由于整体流程都是通过event事件来驱动的,event的实现无论是select, poll, epoll还是kqueue, IOCP都会在I/O事件没有的时候,指定一个阻塞时间值,这个时间不能太大,否则就会阻塞其它要处理的事情,也不能太小,容易退化成轮询。一般的做法,都是做成定时器中最小(最早超时)的,并且给这个值设定一个最大值。
实现中,还有一个例外就是如果定义了timer_resolution,如果有定义,会使用一个系统定时器,每定时器其间都会给进程发送一个SIGALARM的信号,在这种情况下,就把event有阻塞时间设置为-1,一般实现为无限阻塞。但是放心,因为定时器会给进程发送信号,导致进程从阻塞中退出的。
接着worker会进行accept_mutex争抢,这个功能往简单地说,就是只让一个进程调用accept,避免出现惊群。如果worker争抢accept_mutex失败,那么会在ngx_accept_mutex_delay之后再度抢夺,这是在下面这块代码中实现的,所以即使是因为使用timer_resolution时,也会再加上一个保险,让阻塞调用限制在ngx_accept_mutex_delay内。其实可以通过检查timer_resolution的值,来确定是否有必要限定阻塞时间为ngx_accept_mutex_delay,但是也可以考虑不加入这种复杂性,直接上ngx_accept_mutex_delay。
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
然后主要的三个调用就是
ngx_process_events;
ngx_event_process_posted;
ngx_event_expire_timers;
ngx_event_process_posted;
其中ngx_process_events完成类似select的调用,然后根据某种原则,决定是否将select后的处理(调用读、写函数收发数据,或者调用accept接收用户的连接)延后处理。所以我们可以看到后面有两个ngx_event_process_posted的调用,其中前一个就是延迟处理accept的操作,后一个就是读写操作。在两者蹭的ngx_event_expire_timers时则是对定时器超时进行处理。
延迟处理的原则
刚才说到的要根据某种原则来延迟处理,其实也没有那么神秘,就是如果要处理客户连接,也就是要进行accept操作的,就需要对读写进行延迟处理。这里面的逻辑是这样的,为了能够尽快地accept,必须select操作以及相关的操作尽快完成,所以nginx的代码中,就是如果获得了accept_mutex的锁的进程,在ngx_process_events中,只是检查各个soekct fd,看看是否有accept, write, read操作要处理,并且把要accept操作的放到一个队列,write/read的放到另外一个队列。完成 了退出ngx_process_events之后,先调用ngx_event_process_posted进行一次连接处理,然后调用ngx_event_expire_timer进行定时喊叫处理,随后再调用ngx_event_process_posted进行读写。这里也说明了在nginx的眼中,处理连接第一,定时器随后,读写就优先级稍稍低一点了。
那么为什么不在ngx_process_events一发现有客户连接就处理掉呢,也不是不可以,就是一种选择而已,其实逻辑上算是差不多的。
类似系统
其实有很多的系统都有这种类似的机制,就是通过多路复用,事件驱动,来达到单线程下的高吞吐量。有时候,也会启动一些辅助的线程,但是主线程的模式还是完全一样的。
异步的文件IO处理
为了能够处理异步文件IO,需要将文件异步IO与多路复用绑定,因为多路复用一般也是基于文件描述符的(socket也要与文件描述符关联之后进入到多路复用的世界的,对应到windows的世界就是HANDLE啥的,换汤不换药,都是一路的货色),所以要将文件异步操作与一个文件描述符关联。一种做法是直接将要操作的文件的描述符拿来用,而在linux的epoll实现中,选择了另外一种方式,可以认为是分了一下层,异步IO完成后用于通知的句柄是另外一个句柄(通过在io_submit提交时的io_cb结构体中指定),这另外一个句柄交给epoll监视。
如果说真心话,这
网友评论