Nginx基础. epoll事件驱动模块

2024-06-22 19:48

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

关于epoll事件驱动模块, 这里不做过多分析. 主要着眼于事件添加和事件处理上.

添加事件

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{int                  op;uint32_t             events, prev;ngx_event_t         *e;ngx_connection_t    *c;struct epoll_event   ee;//之前有过了解, 每个事件的data可能存储的是连接结构体//每个连接都是一个"事件", 且每个连接对应于读写池的两个事件c = ev->data;//这里的event用于确定当前事件是读事件还是写事件. 即将来调用epoll_ctl是使用EPOLLIN还是EPOLLOUTevents = (uint32_t) event;//如果当前事件是以读事件插入, 那么事件对象e就会记录下此连接事件的写事件(当然, 写事件可能并不存在, 但即使写//事件不存在, 它的事件对象早已被创建好, 其中的active标志就反映了该事件是否被定义). //与此同时, 记住此时prev值是EPOLLOUT, events则为读标记if (event == NGX_READ_EVENT) {e = c->write;prev = EPOLLOUT;
#if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)events = EPOLLIN|EPOLLRDHUP;
#endif} else {e = c->read;prev = EPOLLIN|EPOLLRDHUP;
#if (NGX_WRITE_EVENT != EPOLLOUT)events = EPOLLOUT;
#endif} //上面, 如果某事件作为读事件插入, 那么e记录的是该连接写事件. //也说道, active记录了该事件是否激活(被定义).//如果我们在插入读事件的时候, 发现该连接的写事件曾经已经被插入到epoll中了, 也就是说该连接的描述符已经被插入了//那么, 现在我们可能就会重复插入. 为了避免这样的问题, 就有了下面的判断//如果该描述符已经被注册过了, 那么不是添加描述符而是修改描述符//且事件类型修改为读写if (e->active) {op = EPOLL_CTL_MOD;events |= prev;} else {//否则就是添加描述符op = EPOLL_CTL_ADD;}ee.events = events | (uint32_t) flags;//插入到epoll中的描述符对应的存储信息的结构体的data.ptr用于存储连接结构体//这里可能是个令人疑惑的地方, 但也是特别需要在意的地方.//这个instance标志是判断一个事件是否过期的标志. 下面会涉及到.ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,"epoll add event: fd:%d op:%d ev:%08XD",c->fd, op, ee.events);if (epoll_ctl(ep, op, c->fd, &ee) == -1) {...return NGX_OK;
}


收集分发事件

接下来就是分析实现了 收集分发事件的process_event方法.
每个worker子进程主要就是调用了这里的方法进行事件的收集分发
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{int                events;uint32_t           revents;ngx_int_t          instance, i;ngx_uint_t         level;ngx_err_t          err;ngx_event_t       *rev, *wev;ngx_queue_t       *queue;ngx_connection_t  *c;/* NGX_TIMER_INFINITE == INFTIM */ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"epoll timer: %M", timer);events = epoll_wait(ep, event_list, (int) nevents, timer);err = (events == -1) ? ngx_errno : 0;//还记得这里的ngx_event_timer_alarm吗, 就是利用setitimer开启的间歇定时器if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {ngx_time_update();}//如果发生了错误if (err) {//被信号打断. 有可能是被定时器信号打断的if (err == NGX_EINTR) {//每次定时器信号发生后的信号处理函数都会将这个变量置1.if (ngx_event_timer_alarm) {//表明的确是被定时器打断的//上面已经更新过时间了, 所以重置为0.ngx_event_timer_alarm = 0;return NGX_OK;}. . .}//因为epoll返回了0, 要么是因为epoll_wait的时间参数超时了, 要么就是出错了if (events == 0) {if (timer != NGX_TIMER_INFINITE) {return NGX_OK;}. . .}//处理每一个发生的事件for (i = 0; i < events; i++) {//得到该事件的连接(从连接中我们可以得到其对应的读写事件(如果存在的话))c = event_list[i].data.ptr;//取出存在地址最后一位的instance变量instance = (uintptr_t) c & 1;//还原连接结构体的原本地址c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);//得到连接对应的读写事件的instance成员的值rev = c->read;//如果发现fd变成-1那么说明这个连接已经过期了//如果发现此连接的fd并没有变成-1, 这说明此事件对应的连接可能已经过期了, 但此连接使用的结构体虽然被free但又被其他新连接使用了//这时候需要看instance是否发生变化. 如果不再与原来相同, 那么表明确实是被其他新连接复用了; 相同则表明没有过期//过期的事件就不再需要处理if (c->fd == -1 || rev->instance != instance) {/** the stale event from a file descriptor* that was just closed in this iteration*/ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"epoll: stale event %p", c);continue;}//得到此事件的事件类型revents = event_list[i].events;...if ((revents & EPOLLIN) && rev->active) {...//表示需要延后处理if (flags & NGX_POST_EVENTS) {//对于EPOLLIN事件, 其有可能是普通的读事件, 也有可能是有新连接到来 //之所以要将他们区别对待, 之后会讲到queue = rev->accept ? &ngx_posted_accept_events: &ngx_posted_events;ngx_post_event(rev, queue);} else {//否则直接调用回调函数处理rev->handler(rev);}}wev = c->write;if ((revents & EPOLLOUT) && wev->active) {if (c->fd == -1 || wev->instance != instance) {/** the stale event from a file descriptor* that was just closed in this iteration*/ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"epoll: stale event %p", c);continue;}wev->ready = 1;if (flags & NGX_POST_EVENTS) {ngx_post_event(wev, &ngx_posted_events);} else {wev->handler(wev);}}}return NGX_OK;
}



ngx_event_process_events方法会收集当前触发的所有事件. 对于不需要加入到post队列延后处理的事件, 该方法会立即执行他们的回调方法. 这其实是在做分发事件的工作, 只是它会在自己的进程中调用这些方法而已, 因此每个回调方法都不能导致进程休眠或者消耗太多时间, 以免epoll不能及时处理其他事件.


关于ngx_event_t中的instance标志位

它为什么能判断事件是否过期呢?

1.instance放在哪里 ?
它利用了指针最后一位一定是0的特性. 既然最后一位始终是0, 那么就可以用来包含一些信息.
这样, 在使用ngx_epoll_add_event方法向epoll中添加事件时, 就把epoll_event中data成员的ptr指针指向连接结构体的同时,把最后一位置为这个事件的instance标志.
在ngx_event_process_events方法中取出指向连接结构体的指针时, 先把instance取出, 再把最后一位还原成原本连接结构体的地址.
2. 为什么一定要放在这一位?
因为ptr指针已经指向了连接结构体, 然而连接结构体中并没有这个成员变量. 但这个变量又是必需的, 所以得找个地方存它
3. 什么是过期事件?
举个例子, 假设epoll_wait一次返回了3个事件. 在第一个事件处理的过程中,  由于业务的需要, 所以关闭了一个连接. 而这个连接恰好是第三个事件的连接, 这样一来, 在处理到第三个事件的时候, 这个事件已经是过期事件了, 一旦处理就肯定会出错了. 既然如此, 解决的方案可能并不需要instance, 如果把该连接的fd设置为-1是否就能解决这个问题呢?
假设第三个事件对应的连接结构体中的fd套接字原先是50, 处理第一个事件时把这个连接套接字关闭了, 并将fd设置为-1, 并调用ngx_free_connection将该连接归还给连接池. 当我们处理第二个连接的时候, 恰好第二个事件是建立新的连接(属于监听套接字的事件), 调用ngx_get_connection从连接池取出的连接结构体恰好就是刚刚归还给连接池的第三个事件的连接.  又由于套接字50刚刚被释放了, linux内核很可能把这个刚释放的套接字50又分配给新建立的连接. 因此, 在处理到第三个事件的时候, 这个连接应该是过期的而不能被处理!
如何解决这个问题就是依靠instance了.
当调用ngx_get_connection从连接池获取一个新连接时, instance位就会被置反:
ngx_connection_t *
ngx_get_connection(ngx_socket_t s, ngx_log_t *log)
{...c = ngx_cycle->free_connections;...ngx_cycle->free_connections = c->data;ngx_cycle->free_connection_n--;...//可以看到, 新获得的连接却依然持有以前的连接对应的读写事件rev = c->read;wev = c->write;ngx_memzero(c, sizeof(ngx_connection_t));//并在初始化连接结构体后依然把原本的事件放回去.c->read = rev;c->write = wev;c->fd = s;c->log = log;instance = rev->instance;//初始化读写事件ngx_memzero(rev, sizeof(ngx_event_t));ngx_memzero(wev, sizeof(ngx_event_t));//将读写事件的instance置反rev->instance = !instance;wev->instance = !instance;...return c;
}
这样处理后, 当某个连接结构体被重复使用后, 它的instance位一定是不同的. 因此, 在ngx_epoll_process_events方法中一旦判断instance发生了变化, 就认为这是过期事件而不处理.

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



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

相关文章

C#基础之委托详解(Delegate)

《C#基础之委托详解(Delegate)》:本文主要介绍C#基础之委托(Delegate),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 委托定义2. 委托实例化3. 多播委托(Multicast Delegates)4. 委托的用途事件处理回调函数LINQ

如何自定义Nginx JSON日志格式配置

《如何自定义NginxJSON日志格式配置》Nginx作为最流行的Web服务器之一,其灵活的日志配置能力允许我们根据需求定制日志格式,本文将详细介绍如何配置Nginx以JSON格式记录访问日志,这种... 目录前言为什么选择jsON格式日志?配置步骤详解1. 安装Nginx服务2. 自定义JSON日志格式各

Python使用date模块进行日期处理的终极指南

《Python使用date模块进行日期处理的终极指南》在处理与时间相关的数据时,Python的date模块是开发者最趁手的工具之一,本文将用通俗的语言,结合真实案例,带您掌握date模块的六大核心功能... 目录引言一、date模块的核心功能1.1 日期表示1.2 日期计算1.3 日期比较二、六大常用方法详

Nginx指令add_header和proxy_set_header的区别及说明

《Nginx指令add_header和proxy_set_header的区别及说明》:本文主要介绍Nginx指令add_header和proxy_set_header的区别及说明,具有很好的参考价... 目录Nginx指令add_header和proxy_set_header区别如何理解反向代理?proxy

python中time模块的常用方法及应用详解

《python中time模块的常用方法及应用详解》在Python开发中,时间处理是绕不开的刚需场景,从性能计时到定时任务,从日志记录到数据同步,时间模块始终是开发者最得力的工具之一,本文将通过真实案例... 目录一、时间基石:time.time()典型场景:程序性能分析进阶技巧:结合上下文管理器实现自动计时

Nginx配置location+rewrite实现隐性域名配置

《Nginx配置location+rewrite实现隐性域名配置》本文主要介绍了Nginx配置location+rewrite实现隐性域名配置,包括基于根目录、条件和反向代理+rewrite配置的隐性... 目录1、配置基于根目录的隐性域名(就是nginx反向代理)2、配置基于条件的隐性域名2.1、基于条件

Ubuntu中Nginx虚拟主机设置的项目实践

《Ubuntu中Nginx虚拟主机设置的项目实践》通过配置虚拟主机,可以在同一台服务器上运行多个独立的网站,本文主要介绍了Ubuntu中Nginx虚拟主机设置的项目实践,具有一定的参考价值,感兴趣的可... 目录简介安装 Nginx创建虚拟主机1. 创建网站目录2. 创建默认索引文件3. 配置 Nginx4

Nginx如何进行流量按比例转发

《Nginx如何进行流量按比例转发》Nginx可以借助split_clients指令或通过weight参数以及Lua脚本实现流量按比例转发,下面小编就为大家介绍一下两种方式具体的操作步骤吧... 目录方式一:借助split_clients指令1. 配置split_clients2. 配置后端服务器组3. 配

Nginx实现前端灰度发布

《Nginx实现前端灰度发布》灰度发布是一种重要的策略,它允许我们在不影响所有用户的情况下,逐步推出新功能或更新,通过灰度发布,我们可以测试新版本的稳定性和性能,下面就来介绍一下前端灰度发布的使用,感... 目录前言一、基于权重的流量分配二、基于 Cookie 的分流三、基于请求头的分流四、基于请求参数的分

一文详解Nginx的强缓存和协商缓存

《一文详解Nginx的强缓存和协商缓存》这篇文章主要为大家详细介绍了Nginx中强缓存和协商缓存的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、强缓存(Strong Cache)1. 定义2. 响应头3. Nginx 配置示例4. 行为5. 适用场景二、协商缓存(协