nginx是个高性能的服务器,一个重要的因素是利用了I/O复用(在linux2.6以后的epoll,freebsd的kqueue,solaris上的eventport,在windows上用iocp,本文以epoll为例说明),epoll比poll性能提升太多,不需要一些浪费cpu的轮询操作。但这只是nginx性能高的沧海一粟,它的代码底层做了大量的优化,比如在比较4个字符的字符串的时候,nginx把这个4个字符转化为32位整型然后做整型的比较,以减少底层指令数。除了某些外部的库(openssl,pcre),所有的轮子都是自己造的,比如内存池,字符串,链表,队列,哈希表,红黑树等。
任何server本质来说无非就做了这么一件事:创建socket,接着bind,接着listen,最后accept等待tcp连接,从fd里读读写写。随着服务器编程模型的不同,会在不同的线程/进程里accept然后处理请求。本文尝试在框架上说明nginx是怎么做这件事情的。
nginx在大多数情况下是master-worker的形式工作的,因为要使多个worker共享监听同一个端口,所以在fork之前需要把这个socket初始化,然后子进程继承这个socket。
在fork之前,nginx在main函数中调用了一个重要的函数叫ngx_init_cycle
,它主要用来初始化ngx_cycle_t
类型的一个全局变量,这个函数接着调用ngx_open_listening_sockets
,这个函数做的是创建一个新的socket,接着调用bind,接着在这个listenfd上listen,最后将这个listenfd保存在listenngx_cycle_t
类型全局变量中的listening数组里。
在fork以后,worker将调用所有注册模块的init_process
钩子函数(就是回调函数),其中有个核心模块叫ngx_event_core_module
,它的init_process
这个钩子被注册为ngx_event_process_init
(src/event/ngx_event.c),在这个函数中,它为每一个在listening数组中的listenfd创建一个connection,以及这个connection对应的读写事件。
然后做了非常重要的一步:将读事件的回调函数设置为ngx_event_accept
,接着把这个listenfd放到了epoll里。这代表什么意思呢,在epoll_wait
返回listenfd可读的时候,就回调注册的钩子函数ngx_event_accept
,这个函数的大致代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
和预期的差不多,accpet一个连接fd,然后通过某个回调函数将fd的读写回调函数设置为http模块相关的函数,接着再放入epoll中。这里比较重要的是ls->handler(c)
,这个handler通过下面这系列调用(ngx_http_block->ngx_http_optimize_servers->ngx_http_add_listening->ngx_http_init_listenin)被设置为ngx_http_init_connection
。接下来来看看这个函数中设置事件回调的部分:
1 2 3 4 5 |
|
也就是说,当这个fd可读时,调用ngx_http_wait_request_handler
来处理这个请求,之后的事情就是http模块的解析过程了,这个放在以后在写。随着请求的越来越多,epoll里的事件也越来越多,每次可读或者可写都调用相应的回调函数,并且epoll保证了高并发。以上就是从创建一个listen socket到接受一个http请求的框架,其中省略了很多很多的细节。