Ngixn基础. 认识Nginx事件模块(一)

2024-06-22 19:48

本文主要是介绍Ngixn基础. 认识Nginx事件模块(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对于Nginx的整体框架, 尚且只能了解个大概, 并不能弄清除整个流程. 索性先放着, 先了解其他组件.
事件处理框架所要解决的问题是 如何收集, 管理, 分发事件. 且事件类型主要为网络事件和定时器事件.
既然需要支持跨平台, 那么就肯定要封装不同平台的事件驱动机制. 当然, 我只能看懂select, poll, epoll这几个... 那么Nginx是如何选择的呢?
    1 . 之前在对框架作了解时, 知道了整个Nginx框架为了不那么复杂, 只定义配置模块和核心模块. 而核心模块中又有其他模块的代言模块.
        对于事件模块而言, 其在核心模块中的代言模块就是ngx_events_module模块. 所以可以这样理解, ngx_events_module不是事件模块而是核心模块, 但它定义了事件模块.
        在Nginx启动过程中调用的ngx_cycle_init方法解析配置项时, 一旦在配置文件中找到ngx_events_module感兴趣的"event{}"配置项, 该核心模块就开始工作了. 此模块定义了事件类型的模块, 它的全部工作就是为所有事件模块解析"event{}"中的配置项, 同时管理这些事件模块存储配置项的结构体.
    2 . 只是, 除了ngx_events_module对于事件模块很重要外, 还有一个事件模块 ngx_event_core_module也十分重要. 这个模块会决定使用哪种事件驱动机制, 以及如何管理事件
    3 . 当然, 其他就是一系列不同系统不同内核版本的事件驱动模块的封装而成的模块了.


下面开始分析Nginx的事件模块:
关于事件模块中的事件
在Nginx中, 每个事件都由ngx_event_t结构体来表示. 所以以此作为切入点.
struct ngx_event_s{//事件相关对象, 通常data指向ngx_connection_t连接对象. (开启异步IO后可能指向ngx_event_aio_t结构体)void *data;  ...//为1时表示可以建立连接. 通常在ngx_cycle_t中的listening动态数组中, 每一个监听对象ngx_listening_t对应的读事件中的accept才会是1unsigned accept;//这个标志位用于区分当前事件是否过期. 它仅仅是给事件驱动模块使用的(epoll等模块), 无关事件消费模块(http等模块)//当需要处理一批事件时, 处理这批事件前面的事件可能导致关闭后面的连接, 而这些被关闭的连接可能影响到这批事件中还未处理的事件.  //这时, 可以通过此标志位来处理来避免处理后面已经过期的事件//看不懂很正常, 下面会细谈.unsigned instance;...//标志, 为1表示延迟建立TCP连接, 即经过三次握手后并不建立连接, 而是正真数据包来后才建立TCP连接unsigned deferred_accept;...//在epoll事件驱动机制下表示一次尽可能多的建立TCP连接, 与配置文件中的配置项"multi_accept"对应unsigned available;//这个事件发生时的处理方法, 每个事件消费模块都会重新实现它, 以决定这个事件最终如何消费ngx_event_handler_pt handler;//定时器节点, 用于定时器红黑树中ngx_rbtree_node_t timer;...//post事件将构成一个队列再统一处理, 队列以next和prev作为链表指针, 以此构成一个简易的双向链表ngx_event_t *next;ngx_event_t **prev;
};

以上并没有完整的列出该结构体中的每个元素, 是为了更容易专注当前分析的部分.
对于每个事件, 其最核心的部分就是handler回调方法, 所有的Nginx模块只要处理事件就必然要设置handler回调方法.
既然handler决定如何消费事件, 那么如何触发这个消费事件呢?
这时候就是将事件放到事件驱动机制中去(epoll..), 等待被触发之后调用handler函数. 那么如何将事件添加到epoll中呢?
按照没有被封装过的epoll, 我们立即能想到调用epoll_ctl函数. 所以肯定会有结构体封装这类操作函数:
下面结构体定义了每个事件模块都要实现的接口(ngx_event_core_module和几个事件驱动模块):

typedef struct {ngx_str_t              *name;         //事件模块的名称void                 *(*create_conf)(ngx_cycle_t *cycle);            //创建存储配置项的结构体char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);    //解析完配置项后, 此函数用于综合处理当前模块感兴趣的全部配置项ngx_event_actions_t     actions;      //每个事件驱动机制都要实现的10个方法. 比如添加事件, 删除事件, 初始化等等
} ngx_events_module_t;
从ngx_event_actions_t这个结构体就可以看出, 这就是封装操作函数的结构体:

typedef struct {//添加和删除事件ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);...//向事件驱动机制中添加或删除一个连接的读写事件ngx_int_t  (*add_conn)(ngx_connection_t *c);ngx_int_t  (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);...//在正常的工作循环中, 将通过调用此方法来处理事件, 此方法是处理, 分发事件的核心ngx_int_t  (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,ngx_uint_t flags);//初始化事件驱动模块ngx_int_t  (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);//退出事件驱动模块前调用的方法void       (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;


所以, 如果想要往epoll中添加事件的话, 可以调用封装好的add函数.  只是因为平台不同, Nginx提供了两个简单的方法用于在事件驱动模块中添加删除事件
下面是这两个函数的原型:
ngx_int_t ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags);
ngx_int_t ngx_handle_write_event(ngx_event_t *wev, size_t lowat);
第一个函数用于将读事件添加到事件驱动模块中, 这样该事件对应的TCP连接一旦出现可读事件, 就会回调该事件的handler方法.
        其第二个参数可以是0或NGX_CLOSE_EVENT(但NGX_CLOSE_EVENT只在epoll的LT模式有效, 但epoll在Nginx中默认使用ET模式, 所以这个标志一般不用)
第二个函数将写事件添加到事件驱动模块中.
        第二个参数lowat表示只有当连接对应的套接字缓冲区中必须有lowat大小的可用空间时, 事件收集器才处理这个可写事件(lowat为0表示不考虑可写缓冲区大小)


关于事件中的连接事件
对于web服务器 , 每个用户请求至少对应一个TCP连接; 为了处理这个事件, 每个连接至少需要一个读事件和一个写事件, 这样才能根据触发事件调度相应模块读取请求或是发送响应.
因此, Nginx定义了结构体 ngx_connection_t来表示连接, 表示被动接收的连接. 要注意的是, 这种连接不能随意创建, 必须从连接池(数组)中获取.
谈到连接池, 那么我们可以想象, 某个连接创建某个连接释放都是不固定的, 所以每次想要向连接池申请连接时, 都需要遍历一遍连接池寻找空闲连接. 这种效率自然很差, 所以Nginx定义了空闲连接池用以提供空闲连接. 空闲连接池的实现是利用了ngx_connection_t中的某个暂时不用的指针来作为连接下一个空闲连接的枢纽的.
struct ngx_connection_s {//因为Nginx采用了事先创建好连接池的策略, 所以当连接未使用时, data成员用于充当连接池中空闲连接链表中的next指针. //当连接被使用时, data的意义由使用的模块而定. 比如在HTTP模块中, data指向ngx_http_request_t请求void               *data;//对应的读写事件ngx_event_t        *read;ngx_event_t        *write;//套接字句柄ngx_socket_t        fd;//直接接收/发送网络字节流的方法ngx_recv_pt         recv;ngx_send_pt         send;//以ngx_chain_t链表为参数来接收/发送网络字符流的方法ngx_recv_chain_pt   recv_chain;ngx_send_chain_pt   send_chain;//这个连接对应的ngx_listening_t对象, 此连接由listening监听端口的事件建立ngx_listening_t    *listening;//这个连接已经发送出去的字节数off_t               sent;ngx_log_t          *log;//内存池, 一般在accept一个新连接时, 会创建一个内存池, 而在连接结束时会销毁内存池. //内存池大小将由listening监听对象中的pool_size来指定ngx_pool_t         *pool;...struct sockaddr    *local_sockaddr;socklen_t           local_socklen;//用于接收, 缓存来自客户发来的字节流. 每个事件消费模块可以自由决定从连接池中分配多大空间给buffer这个字段//比如HTTP模块, 它的大小决定于client_header_buffer_size配置项ngx_buf_t          *buffer;//该字段用来将当前连接以双向链表元素的形式添加到ngx_cycle_t核心结构体的reusable_connections_queue双向链表中//表示可重用的连接ngx_queue_t         queue;//连接使用次数ngx_atomic_uint_t   number;//处理的请求次数ngx_uint_t          requests;...//表示TCP连接被销毁了, 此时此结构体仍可用, 但其套接字, 内存池已不可用了unsigned            destroyed:1;unsigned            idle:1;unsigned            reusable:1;unsigned            close:1;unsigned            sendfile:1;//表示发送缓冲区的阀值unsigned            sndlowat:1;unsigned            tcp_nodelay:2;   /* ngx_connection_tcp_nodelay_e */unsigned            tcp_nopush:2;    /* ngx_connection_tcp_nopush_e */...
};
这其中的4个关于接收, 发送网络字节流的方法以指针的形式出现, 说明每个连接都可以采用不同的接收方法, 每个事件消费模块都可以灵活决定其行为.
不同的事件驱动机制需要使用的发送接收的方法也多是不同的
关于连接池, 上面谈到一点, 这里再做补充.
从下图可以看出, 在ngx_cycle_t中的connections和free_connections两个成员构成了一个连接池. 其中connections指向连接池首部, free_connections则指向第一个空闲连接.
所有空闲连接都以data成员作为next指针串联成一个单链表. 一旦有申请连接, 就返回free_connections指向的空闲连接, free_connections继续指向下一个空闲连接.
归还连接时则只需把连接插入到free_connections链表的表头即可.(有点静态链表的感觉)

除了分配连接, Nginx还认为每个连接至少需要一个读事件和写事件, 于是在ngx_cycle_t结构体中可以发现read_events和write_events这两个数组. 其数组大小和连接池大小相同. 所以根据下标就能将一个连接与两种事件相联系起来
Nginx也定义了两个用于申请回收连接的函数:
ngx_connection_t *ngx_get_connection(ngx_socket_t s, ngx_log_t *log);
void ngx_free_connection(ngx_connection_t *c);

                         


ngx_events_module核心模块
文章一开始也提到了, Nginx框架定义了配置模块与核心模块, 某些核心模块又定义了事件模块, HTTP模块等.
ngx_events_module就是定义了事件模块的核心模块. 此模块定义了上面涉及的事件类型, 每个事件模块都要实现的ngx_events_module_t接口, 管理这些事件模块生成的配置项结构体, 并解析事件类型配置项, 同时, 会调用其在ngx_commands_t数组中定义的回调方法
既然ngx_events_module定义了事件模块, 且有了以上结构体的认识, 那么就从ngx_events_module模块开始对事件模块进行整体的认识
static ngx_command_t  ngx_events_commands[] = {{ ngx_string("events"),NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,ngx_events_block,0,0,NULL },ngx_null_command
};
可见, 在配置文件中遇到"events{}"后, ngx_events_module就会调用ngx_events_block来处理.
作为核心模块, ngx_events_module还要实现核心模块的共同接口:
static ngx_core_module_t  ngx_events_module_ctx = {ngx_string("events"),NULL,ngx_event_init_conf
};
如果还有印象的话, 可以想起在Nginx框架中, 在解析配置文件前, 有这么一段代码:
    for (i = 0; ngx_modules[i]; i++) {if (ngx_modules[i]->type != NGX_CORE_MODULE) {continue;}module = ngx_modules[i]->ctx;if (module->create_conf) {rv = module->create_conf(cycle);if (rv == NULL) {ngx_destroy_pool(pool);return NULL;}cycle->conf_ctx[ngx_modules[i]->index] = rv;}}
不过, 在ngx_events_module中实现的核心模块却没有实现此函数, ngx_event_init_conf函数也只是简单进行正确性判断. 这是因为ngx_events_module模块并不会解析配置项的参数, 只是在出现events配置项后会调用各事件模块去解析events{...}块内的配置项, 所以也就不需要什么存储结构体了.
除了上面两个结构体, 每个模块都必须实现的模块接口:
ngx_module_t  ngx_events_module = {NGX_MODULE_V1,&ngx_events_module_ctx,                /* module context */ngx_events_commands,                   /* module directives */NGX_CORE_MODULE,                       /* module type */NULL,                                  /* init master */NULL,                                  /* init module */NULL,                                  /* init process */NULL,                                  /* init thread */NULL,                                  /* exit thread */NULL,                                  /* exit process */NULL,                                  /* exit master */NGX_MODULE_V1_PADDING
};
可见, 除了对events{...}内的配置项进行解析外, 此模块并没有做其他事情. 所以重点就关注ngx_events_block函数.
但是, 在关注此函数之前, 我们需要理清思路, 每个事件模块都有为其自己创建的存储配置项的结构体(就是利用ngx_events_module_t中的craete_conf函数). 事件模块只需要在被调用时为其结构体分配内存即可, 那么这么多事件模块的配置项结构体的指针是如何被ngx_events_module管理/存储的呢?
        每一个事件模块产生的配置项结构体都会被存放到ngx_events_module模块创建的 指针数组中, 既然如此, 该指针数组又是被存放在了哪里呢?
        该指针数组被存放在ngx_cycle_t结构体的conf_ctx成员中. 这个成员是四级指针, 即为存放((指针数组)的地址)的数组, 粗略的看就是指针数组 (或者说, 它首先指向一个存放指针的数组, 这个数组中的指针成员同时又指向另外一个存放指针的数组). 这个指针数组就依次存放着所有Nginx模块关于配置项方面的指针. 顺序在ngx_modules.c文件中已经被定义好了. 可以看到, ngx_events_module被放在第4个位置, 那么所有进程的conf_ctx数组的第4个指针就保存着ngx_events_module产生的指针数组.
如下, 是Nginx为了方便获取某个事件模块结构体而定义的宏:
#define ngx_event_get_conf(conf_ctx, module)                                  \(*(ngx_get_conf(conf_ctx, ngx_events_module))) [module.ctx_index];
#define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]
看到这个, 我们就容易理解为什么在ngx_module_t中要定义index和ctx_index这两个索引了.
也可以发现, 没有给该核心模块创建存储配置项的结构体, 此时就被用来存放所有事件模块的结构体了.
如果我们想要获得某事件模块的结构体, 那么只要传入ngx_cycle_t的conf_ctx成员, 与对应模块的名称就可以了.
                      

接下来就回到ngx_events_block函数.
static char *
ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{char                 *rv;void               ***ctx;ngx_uint_t            i;ngx_conf_t            pcf;ngx_events_module_t   *m;//虽然没能联系上下文, 但这里可以猜想就是ngx_cycle_t中的ctx_conf数组中的某一个元素(的指针? )//如果已经出现过"events{...}"了, 就不会再重复处理第二次if (*(void **) conf) {return "is duplicate";}/* count the number of the event modules and set up their indices *///为每个事件模块赋予一个ctx_index, 表示在事件模块中的顺序(index表示在所有模块中的顺序)ngx_event_max_module = 0;for (i = 0; ngx_modules[i]; i++) {if (ngx_modules[i]->type != ngx_events_module) {continue;}ngx_modules[i]->ctx_index = ngx_event_max_module++;}//以下几个步骤就是创建存储所有事件模块的存储配置项的结构体指针的数组//而ctx就是一个指向指针数组的指针. (可以这样理解, 一个指向指针的指针就是二级指针, 那么指向指针数组的指针那自然是三级指针)ctx = ngx_pcalloc(cf->pool, sizeof(void *));if (ctx == NULL) {return NGX_CONF_ERROR;}//分配指针数组所需空间*ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));if (*ctx == NULL) {return NGX_CONF_ERROR;}*(void **) conf = ctx;//为每个事件模块创建其所需要的存储配置项的结构体for (i = 0; ngx_modules[i]; i++) {...m = ngx_modules[i]->ctx;if (m->create_conf) {(*ctx)[ngx_modules[i]->ctx_index] = m->create_conf(cf->cycle);...}}pcf = *cf;cf->ctx = ctx;cf->module_type = ngx_events_module;cf->cmd_type = NGX_EVENT_CONF;//解析"events{...}"中的内容rv = ngx_conf_parse(cf, NULL);*cf = pcf;if (rv != NGX_CONF_OK)return rv;//在解析完"events{...}“中的内容后, 为每个模块调用init函数, 最终处理配置项for (i = 0; ngx_modules[i]; i++) {...m = ngx_modules[i]->ctx;if (m->init_conf) {rv = m->init_conf(cf->cycle, (*ctx)[ngx_modules[i]->ctx_index]);...}}return NGX_CONF_OK;
}
可以发现, 这里最主要的工作落在了ngx_conf_parse函数上, 只是我对配置模块的理解不够, 所以这里没有能力继续分析了...
不过, 对于events{...}块来说, 解析的配置项可能就是worker_connection指令设置每个worker的连接数, 可能是accept_mutex指令就是指定是否使用accept锁等等...

参考博客:

      《深入理解NGINX》

      http://blog.csdn.net/lengzijian/article/details/7598996
      http://blog.csdn.net/chosen0ne/article/details/7741482

这篇关于Ngixn基础. 认识Nginx事件模块(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085245

相关文章

RedHat运维-Linux文本操作基础-AWK进阶

你不用整理,跟着敲一遍,有个印象,然后把它保存到本地,以后要用再去看,如果有了新东西,你自个再添加。这是我参考牛客上的shell编程专项题,只不过换成了问答的方式而已。不用背,就算是我自己亲自敲,我现在好多也记不住。 1. 输出nowcoder.txt文件第5行的内容 2. 输出nowcoder.txt文件第6行的内容 3. 输出nowcoder.txt文件第7行的内容 4. 输出nowcode

Vim使用基础篇

本文内容大部分来自 vimtutor,自带的教程的总结。在终端输入vimtutor 即可进入教程。 先总结一下,然后再分别介绍正常模式,插入模式,和可视模式三种模式下的命令。 目录 看完以后的汇总 1.正常模式(Normal模式) 1.移动光标 2.删除 3.【:】输入符 4.撤销 5.替换 6.重复命令【. ; ,】 7.复制粘贴 8.缩进 2.插入模式 INSERT

零基础STM32单片机编程入门(一)初识STM32单片机

文章目录 一.概要二.单片机型号命名规则三.STM32F103系统架构四.STM32F103C8T6单片机启动流程五.STM32F103C8T6单片机主要外设资源六.编程过程中芯片数据手册的作用1.单片机外设资源情况2.STM32单片机内部框图3.STM32单片机管脚图4.STM32单片机每个管脚可配功能5.单片机功耗数据6.FALSH编程时间,擦写次数7.I/O高低电平电压表格8.外设接口

ps基础入门

1.基础      1.1新建文件      1.2创建指定形状      1.4移动工具          1.41移动画布中的任意元素          1.42移动画布          1.43修改画布大小          1.44修改图像大小      1.5框选工具      1.6矩形工具      1.7图层          1.71图层颜色修改          1

iptables(7)扩展模块state

简介         前面文章我们已经介绍了一些扩展模块,如iprange、string、time、connlimit、limit,还有扩展匹配条件如--tcp-flags、icmp。这篇文章我们介绍state扩展模块  state          在 iptables 的上下文中,--state 选项并不是直接关联于一个扩展模块,而是与 iptables 的 state 匹配机制相关,特

vue+elementui分页输入框回车与页面中@keyup.enter事件冲突解决

解决这个问题的思路只要判断事件源是哪个就好。el分页的回车触发事件是在按下时,抬起并不会再触发。而keyup.enter事件是在抬起时触发。 so,找不到分页的回车事件那就拿keyup.enter事件搞事情。只要判断这个抬起事件的$event中的锚点样式判断不等于分页特有的样式就可以了 @keyup.enter="allKeyup($event)" //页面上的//js中allKeyup(e

python 在pycharm下能导入外面的模块,到terminal下就不能导入

项目结构如下,在ic2ctw.py 中导入util,在pycharm下不报错,但是到terminal下运行报错  File "deal_data/ic2ctw.py", line 3, in <module>     import util 解决方案: 暂时方案:在终端下:export PYTHONPATH=/Users/fujingling/PycharmProjects/PSENe

[FPGA][基础模块]跨时钟域传播脉冲信号

clk_a 周期为10ns clk_b 周期为34ns 代码: module pulse(input clk_a,input clk_b,input signal_a,output reg signal_b);reg [4:0] signal_a_widen_maker = 0;reg signal_a_widen;always @(posedge clk_a)if(signal_a)

00 - React 基础

1. React 基础 安装react指令 可参考: 官网官网使用教程 如: npx create-react-app 项目名如:npx create-react-app react-redux-pro JSX JSX 是一种 JavaScript 的语法扩展,类似于 XML 或 HTML,允许我们在 JavaScript 代码中编写 HTML。 const element =

AI赋能天气:微软研究院发布首个大规模大气基础模型Aurora

编者按:气候变化日益加剧,高温、洪水、干旱,频率和强度不断增加的全球极端天气给整个人类社会都带来了难以估计的影响。这给现有的天气预测模型提出了更高的要求——这些模型要更准确地预测极端天气变化,为政府、企业和公众提供更可靠的信息,以便做出及时的准备和响应。为了应对这一挑战,微软研究院开发了首个大规模大气基础模型 Aurora,其超高的预测准确率、效率及计算速度,实现了目前最先进天气预测系统性能的显著