1、用于代理与反代理,处理大量请求的工具。
2、主要有三大模块:handle、upstream、过滤模块。handle用于在nginx内部接到请求并进行处理的状况;upstream用于需要nginx接受请求并传递给处理端的状况;过滤模块则处理过滤任务。
3、事件驱动的典范。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
一、Nginx的模块与工作原理
1、模块
Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block(location是Nginx配置中的一个指令,用于URL匹配),而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。
Nginx的模块从结构上分为核心模块、基础模块和第三方模块:
核心模块:HTTP模块、EVENT模块和MAIL模块
基础模块:HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块,
第三方模块:HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块。
用户根据自己的需要开发的模块都属于第三方模块。正是有了这么多模块的支撑,Nginx的功能才会如此强大。
Nginx的模块从功能上分为如下三类。
Handlers(处理器模块)。此类模块直接处理请求,并进行输出内容和修改headers信息等操作。Handlers处理器模块一般只能有一个。
Filters (过滤器模块)。此类模块主要对其他处理器模块输出的内容进行修改操作,最后由Nginx输出。
Proxies (代理类模块)。此类模块是Nginx的HTTP Upstream之类的模块,这些模块主要与后端一些服务比如FastCGI等进行交互,实现服务代理和负载均衡等功能。
图1-1展示了Nginx模块常规的HTTP请求和响应的过程。
图1-1展示了Nginx模块常规的HTTP请求和响应的过程。
Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。
Nginx的模块直接被编译进Nginx,因此属于静态编译方式。启动Nginx后,Nginx的模块被自动加载,不像Apache,首先将模块编译为一个so文件,然后在配置文件中指定是否进行加载。在解析配置文件时,Nginx的每个模块都有可能去处理某个请求,但是同一个处理请求只能由一个模块来完成。
2、工作原理
在工作方式上,Nginx分为单工作进程和多工作进程两种模式。在单工作进程模式下,除主进程外,还有一个工作进程,工作进程是单线程的;在多工作进程模式下,每个工作进程包含多个线程。Nginx默认为单工作进程模式。
Nginx在启动后,会有一个master进程和多个worker进程。
master进程
主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。
我们要控制nginx,只需要通过kill向master进程发送信号就行了。比如kill -HUP pid,则是告诉nginx,从容地重启nginx,我们一般用这个信号来重启nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的。master进程在接收到HUP信号后是怎么做的呢?首先master进程在接到信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以光荣退休了。新的worker在启动后,就开始接收新的请求,而老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出。当然,直接给master进程发送信号,这是比较老的操作方式,nginx在0.8版本之后,引入了一系列命令行参数,来方便我们管理。比如,./nginx -s reload,就是来重启nginx,./nginx -s stop,就是来停止nginx的运行。如何做到的呢?我们还是拿reload来说,我们看到,执行命令时,我们是启动一个新的nginx进程,而新的nginx进程在解析到reload参数后,就知道我们的目的是控制nginx来重新加载配置文件了,它会向master进程发送信号,然后接下来的动作,就和我们直接向master进程发送信号一样了。
worker进程:
而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。
worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
nginx的进程模型,可以由下图来表示:
二、基础架构
1、内存池
简介:
Nginx里内存的使用大都十分有特色:申请了永久保存,抑或伴随着请求的结束而全部释放,还有写满了缓冲再从头接着写.这么做的原因也主要取决于Web Server的特殊的场景,内存的分配和请求相关,一条请求处理完毕,即可释放其相关的内存池,降低了开发中对内存资源管理的复杂度,也减少了内存碎片的存在.
所以在Nginx使用内存池时总是只申请,不释放,使用完毕后直接destroy整个内存池.我们来看下内存池相关的实现。
结构:
1 struct ngx_pool_s {
2 ngx_pool_data_t d;
3 size_t max;
4 ngx_pool_t *current;
5 ngx_chain_t *chain;
6 ngx_pool_large_t *large;
7 ngx_pool_cleanup_t *cleanup;
8 ngx_log_t *log;
9 };
10
11 struct ngx_pool_large_s {
12 ngx_pool_large_t *next;
13 void *alloc;
14 };
15
16 typedef struct {
17 u_char *last;
18 u_char *end;
19 ngx_pool_t *next;
20 ngx_uint_t failed;
21 } ngx_pool_data_t;
实现:
这三个数据结构构成了基本的内存池的主体.通过ngx_create_pool可以创建一个内存池,通过ngx_palloc可以从内存池中分配指定大小的内存。
1 ngx_pool_t *
2 ngx_create_pool(size_t size, ngx_log_t *log)
3 {
4 ngx_pool_t *p;
5
6 p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
7 if (p == NULL) {
8 return NULL;
9 }
10
11 p->d.last = (u_char *) p + sizeof(ngx_pool_t);
12 p->d.end = (u_char *) p + size;
13 p->d.next = NULL;
14 p->d.failed = 0;
15
16 size = size - sizeof(ngx_pool_t);
17 p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
18
19 p->current = p;
20 p->chain = NULL;
21 p->large = NULL;
22 p->cleanup = NULL;
23 p->log = log;
24
25 return p;
26 }
这里首申请了一块大小为size的内存区域,其前sizeof(ngx_pool_t)字节用来存储ngx_pool_t这个结构体自身自身.所以若size小于sizeof(ngx_pool_t)将会有coredump的可能性。
Nginx的内存池不仅用于内存方面的管理,还可以通过`ngx_pool_cleanup_add`来添加内存池释放时的回调函数,以便用来释放自己申请的其他相关资源。
从代码中可以看出,这些由自己添加的释放回调是以链表形式保存的,也就是说你可以添加多个回调函数来管理不同的资源。
2、共享内存
在Nginx里,一块完整的共享内存以数据结构ngx_shm_zone_t来封装表示。
1 typedef struct {
2 u_char *addr; // 分配的共享内存的实际地址(这里实际共享内存的分配,根据当前系统可提供的接口,可以调用mmap或者shmget来进行分配,具体的用法,自己man吧)
3 size_t size; // 共享内存的大小
4 ngx_str_t name; // 该字段用作共享内存的唯一标识,能让Nginx知道想使用哪个共享内存
5 ngx_log_t *log;
6 ngx_uint_t exists; /* unsigned exists:1; */
7 } ngx_shm_t;
8
9 typedef struct ngx_shm_zone_s ngx_shm_zone_t;
10
11 typedef ngx_int_t (*ngx_shm_zone_init_pt) (ngx_shm_zone_t *zone, void *data);
12
13 struct ngx_shm_zone_s {
14 void *data; // 指向自定义数据结构,一般用来初始化时使用,可能指向本地地址
15 ngx_shm_t shm; // 真正的共享内存所在
16 ngx_shm_zone_init_pt init; // 这里有一个钩子函数,用于实际共享内存进行分配后的初始化
17 void *tag; // 区别于shm.name,shm.name没法让Nginx区分到底是想新创建一个共享内存,还是使用已存在的旧的共享内存
18 // 因此这里引入tag字段来解决该问题,tag一般指向当前模块的ngx_module_t变量,见:...
19 };
要在Nginx里使用一个共享内存,需要在配置文件里加上该共享内存的相关信息(添加一条指令),如:共享内存的名称,共享内存的大小等。因此在配置解析阶段,解析到相应的指令时,会创建对应的共享内存(此时创建的仅仅是代表共享内存的结构体:ngx_shm_zone_t,真实共享内存的分配在ngx_init_cycle(&init_cycle)解析完配置后进行并初始化)。见:
1 int ngx_cdecl
2 main(int argc, char *const *argv) // 在master进程中
3 {
4 cycle = ngx_init_cycle(&init_cycle);
5 {
6 if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { // 解析配置
7 {
8 解析到http指令(进入ngx_http_block())
9 {
10 // 会依次执行
11 typedef struct {
12 ngx_int_t (*preconfiguration)(ngx_conf_t *cf); /* 执行顺序4 */
13 ngx_int_t (*postconfiguration)(ngx_conf_t *cf); /* 执行顺序8 */
14
15 void *(*create_main_conf)(ngx_conf_t *cf); /* 执行顺序1 */
16 char *(*init_main_conf)(ngx_conf_t *cf, void *conf); /* 执行顺序5 */
17
18 void *(*create_srv_conf)(ngx_conf_t *cf); /* 执行顺序2 */
19 char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf); /* 执行顺序6 */
20
21 void *(*create_loc_conf)(ngx_conf_t *cf); /* 执行顺序3 */
22 char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); /* 执行顺序7 */
23 } ngx_http_module_t;
24 同时,还有个执行顺序4.5:
25 struct ngx_command_s { /* 执行顺序4.5 */
26 ngx_str_t name;
27 ngx_uint_t type;
28 char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
29 ngx_uint_t conf;
30 ngx_uint_t offset;
31 void *post;
32 };
33
34
35 for (m = 0; ngx_modules[m]; m++) {
36 if (module->create_main_conf) {ctx->main_conf[mi] = module->create_main_conf(cf);}
37 if (module->create_srv_conf) {ctx->srv_conf[mi] = module->create_srv_conf(cf);}
38 if (module->create_loc_conf) {ctx->loc_conf[mi] = module->create_loc_conf(cf);}
39 }
40 for (m = 0; ngx_modules[m]; m++) {
41 if (module->preconfiguration) {if (module->preconfiguration(cf) != NGX_OK) {}
42 }
43
44 rv = ngx_conf_parse(cf, NULL);
45 {
46 /*
47 * 指令的解析
48 * 共享内存配置相关的指令也在这里进行解析
49 * 详细见:
50 * ngx_shm_zone_t *
51 * ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)
52 */
53 }
54
55 for (m = 0; ngx_modules[m]; m++) {
56 if (module->init_main_conf) {rv = module->init_main_conf(cf, ctx->main_conf[mi]);}
57 rv = ngx_http_merge_servers(cf, cmcf, module, mi);
58 }
59 for (m = 0; ngx_modules[m]; m++) {
60 if (module->postconfiguration) {if (module->postconfiguration(cf) != NGX_OK)}
61 }
62 }
63 }
64
65 // in ngx_init_cycle(&init_cycle)
66 line: 462 if (ngx_shm_alloc(&shm_zone[i].shm) != NGX_OK) /* 实际共享内存分配的地方 */
67 line: 466 if (ngx_init_zone_pool(cycle, &shm_zone[i]) != NGX_OK)
68 /* 共享内存管理机制的初始化
69 * 共享内存的使用涉及另外两个主题:
70 * 1、多进程共同使用时之间的互斥问题
71 * 2、引入特定的使用方式(slab机制,这在下一个主题:“Nginx源码分析(2)之——共享内存管理之slab机制”中进行介绍),以提高性能
72 */
73 line: 470 if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) /* 分配之后的初始化 */
74 }
75 }
76
77 ngx_shm_zone_t *
78 ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)
79 {
80 ngx_uint_t i;
81 ngx_shm_zone_t *shm_zone;
82 ngx_list_part_t *part;
83
84 /*
85 * Nginx中所有的共享内存都以list链表的形式组织在全局变量cf->cycle->shared_memory中
86 * 在创建新的共享内存之前,会对该链表进行遍历查找以及冲突检测,
87 * 对于已经存在且不存在冲突时,对共享内存直接进行返回并引用
88 * 存在且不存在冲突:共享内存的名称相同,大小相同,且tag指向的是同一个模块
89 * 有冲突,则报错
90 * 否则,重新分配ngx_shm_zone_t,并挂到全局链表cf->cycle->shared_memory中,最后进行结构初始化
91 * shm_zone = ngx_list_push(&cf->cycle->shared_memory);
92 * 至此:
93 * 仅仅是创建了共享内存的结构体:ngx_shm_zone_t,ngx_shm_zone_t.shm.addr指向的真实共享内存并没有进行实际的分配
94 */
95 part = &cf->cycle->shared_memory.part;
96 shm_zone = part->elts;
97
98 for (i = 0; /* void */ ; i++) {
99
100 if (i >= part->nelts) {
101 if (part->next == NULL) {
102 break;
103 }
104 part = part->next;
105 shm_zone = part->elts;
106 i = 0;
107 }
108
109 if (name->len != shm_zone[i].shm.name.len) {
110 continue;
111 }
112
113 if (ngx_strncmp(name->data, shm_zone[i].shm.name.data, name->len)
114 != 0)
115 {
116 continue;
117 }
118
119 if (tag != shm_zone[i].tag) {
120 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
121 "the shared memory zone \"%V\" is "
122 "already declared for a different use",
123 &shm_zone[i].shm.name);
124 return NULL;
125 }
126
127 if (size && size != shm_zone[i].shm.size) {
128 ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
129 "the size %uz of shared memory zone \"%V\" "
130 "conflicts with already declared size %uz",
131 size, &shm_zone[i].shm.name, shm_zone[i].shm.size);
132 return NULL;
133 }
134
135 return &shm_zone[i];
136 }
137
138 shm_zone = ngx_list_push(&cf->cycle->shared_memory);
139
140 if (shm_zone == NULL) {
141 return NULL;
142 }
143
144 shm_zone->data = NULL;
145 shm_zone->shm.log = cf->cycle->log;
146 shm_zone->shm.size = size;
147 shm_zone->shm.name = *name;
148 shm_zone->shm.exists = 0;
149 shm_zone->init = NULL;
150 shm_zone->tag = tag;
151
152 return shm_zone;
153 }
三、工作模型中的基础架构及原理
由master进程分配接到的任务给worker进程
1、惊群现象
主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。
那么,由于所有子进程都继承了父进程的 sockfd,那么当连接进来时,所有子进程都将收到通知并“争着”与它建立连接,这就叫“惊群现象”。大量的进程被激活又挂起,只有一个进程可以accept() 到这个连接,这当然会消耗系统资源。
2、锁机制
Nginx 提供了一个 accept_mutex 这个东西,这是一个加在accept上的一把互斥锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept()。有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),这样就不会有惊群问题了。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。
Nginx事件处理的入口函数是ngx_process_events_and_timers(),下面是部分代码,可以看到其加锁的过程:
1 if (ngx_use_accept_mutex) {
2 if (ngx_accept_disabled > 0) {
3 ngx_accept_disabled--;
4
5 } else {
6 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
7 return;
8 }
9
10 if (ngx_accept_mutex_held) {
11 flags |= NGX_POST_EVENTS;
12
13 } else {
14 if (timer == NGX_TIMER_INFINITE
15 || timer > ngx_accept_mutex_delay)
16 {
17 timer = ngx_accept_mutex_delay;
18 }
19 }
20 }
21 }
在ngx_trylock_accept_mutex()函数里面,如果拿到了锁,Nginx会把listen的端口读事件加入event处理,该进程在有新连接进来时就可以进行accept了。注意accept操作是一个普通的读事件。下面的代码说明了这点:
1 (void) ngx_process_events(cycle, timer, flags);
2
3 if (ngx_posted_accept_events) {
4 ngx_event_process_posted(cycle, &ngx_posted_accept_events);
5 }
6
7 if (ngx_accept_mutex_held) {
8 ngx_shmtx_unlock(&ngx_accept_mutex);
9 }
ngx_process_events()函数是所有事件处理的入口,它会遍历所有的事件。抢到了accept锁的进程跟一般进程稍微不同的是,它被加上了NGX_POST_EVENTS标志,也就是说在ngx_process_events() 函数里面只接受而不处理事件,并加入post_events的队列里面。直到ngx_accept_mutex锁去掉以后才去处理具体的事件。为什么这样?因为ngx_accept_mutex是全局锁,这样做可以尽量减少该进程抢到锁以后,从accept开始到结束的时间,以便其他进程继续接收新的连接,提高吞吐量。
ngx_posted_accept_events和ngx_posted_events就分别是accept延迟事件队列和普通延迟事件队列。可以看到ngx_posted_accept_events还是放到ngx_accept_mutex锁里面处理的。该队列里面处理的都是accept事件,它会一口气把内核backlog里等待的连接都accept进来,注册到读写事件里。
而ngx_posted_events是普通的延迟事件队列。一般情况下,什么样的事件会放到这个普通延迟队列里面呢?我的理解是,那些CPU耗时比较多的都可以放进去。因为Nginx事件处理都是根据触发顺序在一个大循环里依次处理的,因为Nginx一个进程同时只能处理一个事件,所以有些耗时多的事件会把后面所有事件的处理都耽搁了。
除了加锁,Nginx也对各进程的请求处理的均衡性作了优化,也就是说,如果在负载高的时候,进程抢到的锁过多,会导致这个进程被禁止接受请求一段时间。
比如,在ngx_event_accept函数中,有类似代码:
1 ngx_accept_disabled = ngx_cycle->connection_n / 8
2 - ngx_cycle->free_connection_n;
表明,当已使用的连接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,而且本次不再处理新连接。
3、AIO(异步IO)
监听accept后建立的连接,对读写事件进行添加删除。事件处理模型和Nginx的非阻塞IO模型结合在一起使用。当IO可读可写的时候,相应的读写事件就会被唤醒,此时就会去处理事件的回调函数。
特别对于Linux,Nginx大部分event采用epoll EPOLLET(边沿触发)的方法来触发事件,只有listen端口的读事件是EPOLLLT(水平触发)。对于边沿触发,如果出现了可读事件,必须及时处理,否则可能会出现读事件不再触发,连接饿死的情况。
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树),其工作流程分为三部分:
1 1、调用 int epoll_create(int size)建立一个epoll对象,内核会创建一个eventpoll结构体,用于存放通过epoll_ctl()向epoll对象中添加进来的事件,这些事件都会挂载在红黑树中。
2 2、调用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 对象中为 fd 注册事件,所有添加到epoll中的事件都会与设备驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个sockfd的回调方法,将sockfd添加到eventpoll 中的双链表。
3 3、调用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 来等待事件的发生,timeout 为 -1 时,该调用会阻塞知道有事件发生
这样,注册好事件之后,只要有 fd 上事件发生,epoll_wait() 就能检测到并返回给用户,用户就能”非阻塞“地进行 I/O 了。
epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。(epoll 与 select 相比最大的优点是不会随着 sockfd 数目增长而降低效率,使用 select() 时,内核采用轮训的方法来查看是否有fd 准备好,其中的保存 sockfd 的是类似数组的数据结构 fd_set,key 为 fd,value 为 0 或者 1。)
能达到这种效果,是因为在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。那么,某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。在这点上,epoll 实现了一个”伪”AIO。但是如果绝大部分的 I/O 都是“活跃的”,每个 socket 使用率很高的话,epoll效率不一定比 select 高(可能是要维护队列复杂)。
可以看出,因为一个进程里只有一个线程,所以一个进程同时只能做一件事,但是可以通过不断地切换来“同时”处理多个请求。
例子:Nginx 会注册一个事件:“如果来自一个新客户端的连接请求到来了,再通知我”,此后只有连接请求到来,服务器才会执行 accept() 来接收请求。又比如向上游服务器(比如 PHP-FPM)转发请求,并等待请求返回时,这个处理的 worker 不会在这阻塞,它会在发送完请求后,注册一个事件:“如果缓冲区接收到数据了,告诉我一声,我再将它读进来”,于是进程就空闲下来等待事件发生。
这样,基于 多进程+epoll, Nginx 便能实现高并发。
使用 epoll 处理事件的一个框架
1 for( ; ; ) // 无限循环
2 {
3 nfds = epoll_wait(epfd,events,20,500); // 最长阻塞 500s
4 for(i=0;i<nfds;++i)
5 {
6 if(events[i].data.fd==listenfd) //有新的连接
7 {
8 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
9 ev.data.fd=connfd;
10 ev.events=EPOLLIN|EPOLLET;
11 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
12 }
13 else if( events[i].events&EPOLLIN ) //接收到数据,读socket
14 {
15 n = read(sockfd, line, MAXLINE)) < 0 //读
16 ev.data.ptr = md; //md为自定义类型,添加数据
17 ev.events=EPOLLOUT|EPOLLET;
18 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
19 }
20 else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
21 {
22 struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
23 sockfd = md->fd;
24 send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
25 ev.data.fd=sockfd;
26 ev.events=EPOLLIN|EPOLLET;
27 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
28 }
29 else
30 {
31 //其他的处理
32 }
33 }
34 }
参考:
http://tengine.taobao.org/book/chapter_02.html:Nginx开发从入门到精通