在之前的综述中提到过两篇不错的博客,对nginx的http的处理过程分析的很到位,这里还有一些细节需要拿出来跟大家分享一下。
题外话:
“在认真思考过每一行代码之前,不要说真正理解了它!” ---(这是我说的,大家觉得搞笑就来喷我吧。-_-!)
这句话实际上是在告诉我们,隐藏在一份代码中的诸多细节,是需要你去仔细翻阅的,许多地方并不是你想当然那样子的。今天在一个群里讨论nginx代码的时候,我花了不少时间却没有把一个地方给别人讲明白,而事实上,这里并不是原理有多么的复杂,而且除了我讲述内容的80%是遵循代码之外,剩余那20%是想当然的东西。当我再次扎进代码中,却发现了代码作者对自己的嘲讽,“其实并不是你想象的那样子,年轻人!”,到底发生了什么呢?
某同学问道: 为什么ngx_http_read_request_header会调用ngx_handle_read_event呢?
当时提问的同学在抱怨,“不知道为什么他还要调用一次”。
我当时的反映想当然的认为,可能在某个地方把read event移除了,所以在需要下次继续读之前要先注册一个读事件。事实上这种假设合理吗?但是当时却是寻着这样的一个思路在找代码,后来呢?这位同学晕了,我也晕了!到处都是死胡同,讲不通。
那问题出在哪儿呢?
“不知道为什么他还要调用一次”,言外之意是前面某处已经添加过事件了?事实上这位同学跟我一样,犯了想当然的毛病。可惜啊,当我发现这一点的时候,已经耗费了不少时间。那么我们寻着这个思路追究下去,会不会有合理的解释呢,为什么这里会调用ngx_handle_read_event。
为了这个问题,我翻开了这个请求的处理历程,缕一缕到这之前都发生过什么。
1. 在配置解析结束的前夕,会为每个配置的listen端口申请一个ngx_listening_t结构,同时设置一个响应的处理函数:ls->handler = ngx_http_init_connection;
2. 在ngx_event_process_init里面,为每一个listen fd从连接池中获取一个ngx_connection_t结构,这个结构中有个读事件结构rev = c->read;而这个读事件中的一个handler被设置为rev->handler = ngx_event_accept;
3. 在处理连接之前我们需要首先将listen fd添加到epoll中,在ngx_trylock_accept_mutex中我们看到当一个进程获得了accept锁之后,就通过ngx_enable_accept_events将该listen fd添加进epoll里面,ngx_add_event(c->read, NGX_READ_EVENT, 0),而第三个参数为0,也就意味着,listen fd是以LT模式注册的,而之后读请求数据使用的fd则是ET模式注册的epoll事件,也促成了ngx_event_accept中可以自由控制accept的数量,accept调用指定次数之后,可以自由放弃accept,而均衡地交给其他进程来accept,若不是LT,那么一旦该进程自主放弃accept(即没有发生EAGAIN),其他进程就得不到epoll的通知了。
4. 一般情况下accept返回的fd是需要放到epoll中的,nginx有些特殊的处理,导致了不同的处理路径。在ls->handler(c),即ngx_http_init_connection的调用中,一般的处理会在此时添加epoll事件(ready为0的),而对于一些特殊处理(如deferred accept, rtsig, aio, iocp),此时的ready为1,这样nginx就会通过ngx_post_event(rev, &ngx_posted_events),将刚才封装的读事件结构(而非真正的读事件)加到ngx_posted_events里面,并没有立即去添加epoll事件。
5.在ngx_event_process_posted(cycle, &ngx_posted_events)里面,含有我们在accept里面封装的读事件,注意这个读事件所在连接的fd是accept返回的,而当ngx_event_process_posted中执行ev->handler(ev)时,ngx_http_init_request就会被调用了。
6. 在ngx_http_init_request里面,设置了rev->handler = ngx_http_process_request_line。但是到目前为止,还没有对accept获得的新fd,注册读事件,认识到这点很重要!那么这个新设置的rev->handler既然还没有注册事件,那么如何才会被调用到呢?事实上,在函数的最后,它就是直接调的,rev->handler(rev)。。。。
7. 在ngx_http_process_request_line里面会调用ngx_http_read_request_header来读请求数据,当然这次的读数据是没有通过事件驱动来通知的,只是在这个fd上直接recv,这样当返回EAGAIN时,之前通过被post event里面来直接读数据的过程就结束了,接下来才需要针对该fd注册真正的ET模式的读事件了,这里我们终于回到了问题的起点,“为什么要在这里调用ngx_handle_read_event?”
所以这种延迟添加epoll事件的机制,注意是处理那些需要延迟接收数据的情况,大家可以去查查deferred accept,就大概明白了。