• Nginx随笔


    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 }

     3、内存管理之slab分配机制

    三、工作模型中的基础架构及原理

    由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 处理事件的一个框架,代码转自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html

     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://blog.csdn.net/stfphp/article/details/52936490:初步探索Nginx高并发原理

    https://www.cnblogs.com/linguoguo/p/5511293.htmlNginx工作原理和优化

    http://blog.csdn.net/russell_tao/article/details/7204260:“惊群”,看看nginx是怎么解决它的

    http://tengine.taobao.org/book/chapter_02.htmlNginx开发从入门到精通

  • 相关阅读:
    并发容器和框架之ConcurrentHashMap
    Java的LockSupport工具,Condition接口和ConditionObject
    从源码来看ReentrantLock和ReentrantReadWriteLock
    VMWARE虚拟机上Terminal中使用sudo出现”** 不在sudoers文件中,此事将被警告 “错误
    mac下idea运行项目慢问题解决
    Idea 只修改编辑区主题
    redis内部数据结构的数据结构
    mysql存储过程详解
    HashMap中resize()剖析
    谈Redis的refash的增量式扩容
  • 原文地址:https://www.cnblogs.com/zl1991/p/8204647.html
Copyright © 2020-2023  润新知