关于ngx_epoll_add_event的一些解释

本文详细解析了nginx中的ngx_epoll_add_event函数,解释了如何避免对同一文件描述符重复注册事件,并介绍了epoll_ctl的add和mod操作的区别。
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;

    c = ev->data;

    events = (uint32_t) event;

    if (event == NGX_READ_EVENT) {
        e = c->write;
        prev = EPOLLOUT;
    } else {
        e = c->read;
        prev = EPOLLIN;
    }

    if (e->active) {
        op = EPOLL_CTL_MOD;
        events |= prev;
    } else {
        op = EPOLL_CTL_ADD;
    }

    ee.events = events | (uint32_t) flags;
    ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);


    if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
        return NGX_ERROR;
    }

    ev->active = 1;

    return NGX_OK;
}
这就是那个在群里被问了无数遍的函数,在讨论它之前,可以先来看下这个 http://blog.youkuaiyun.com/dingyujie/article/details/8570764,在这篇文章中特别强调了两个重要变量(即active和ready)的用途。看过之后,再来看这个函数。大家的问题主要是围绕在函数的开头:

为什么明明处理的是NGX_READ_EVENT,即所谓的读事件,为什么要去管c->write?即代码:
if (event == NGX_READ_EVENT) { 
    e = c->write;
    prev = EPOLLOUT;
} else {
    e = c->read;
    prev = EPOLLIN;
}
其实答案就跟在后面:
if (e->active) {
    op = EPOLL_CTL_MOD;
    events |= prev;
} else {
    op = EPOLL_CTL_ADD;
}
结合这两段代码,意图就很明显了(明显吗?既然明显,为什么还有不少人在这里被绊住了呢?)。在epoll中监控读写事件时,主要通过接口epoll_ctl来处理。注意使用时有两种操作add和mod,当一个fd第一次注册到epoll时,使用add方式。而如果之前已经添加过对该fd读写事件的监控,就要通过mod来修改原来的监控方式,告知epoll我们的需求。如果一个fd被重复执行add会报错。
 
man epoll:
Q:  What  happens  if you register the same file descriptor on an epoll
    instance twice?
 
A: You will probably get EEXIST.  However, it is  possible  to  add  a
    duplicate  (dup(2),  dup2(2),  fcntl(2)  F_DUPFD) descriptor to the
    same epoll instance.  This can be a useful technique for  filtering
    events,  if the duplicate file descriptors are registered with dif-
    ferent events masks.
 
所以nginx这里就是为了避免这种情况,当要在epoll中加入对一个fd读事件(即NGX_READ_EVENT)的监听时,nginx先看一下与这个fd相关的写事件的状态,即e=c->write,如果此时e->active为1,说明该fd之前已经以NGX_WRITE_EVENT方式被加到epoll中了,此时只需要使用mod方式,将我们的需求加进去,否则才使用add方式,将该fd注册到epoll中。反之处理NGX_WRITE_EVENT时道理是一样的。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> //uintptr_t #include <stdarg.h> //va_start.... #include <unistd.h> //STDERR_FILENO等 #include <sys/time.h> //gettimeofday #include <time.h> //localtime_r #include <fcntl.h> //open #include <errno.h> //errno //#include <sys/socket.h> #include <sys/ioctl.h> //ioctl #include <arpa/inet.h> #include "ngx_c_conf.h" #include "ngx_macro.h" #include "ngx_global.h" #include "ngx_func.h" #include "ngx_c_socket.h" //建立新连接专用函数,当新连接进入时,本函数会被ngx_epoll_process_events()所调用 void CSocekt::ngx_event_accept(lpngx_connection_t oldc) { struct sockaddr mysockaddr; //远端服务器的socket地址 socklen_t socklen; int err; int level; int s; static int use_accept4 = 1; //我们先认为能够使用accept4()函数 lpngx_connection_t newc; //代表连接池中的一个连接【注意这是指针】 //ngx_log_stderr(0,"这是几个\n"); 这里会惊群,也就是说,epoll技术本身有惊群的问题 socklen = sizeof(mysockaddr); do //用do,跳到while后边去方便 { if(use_accept4) { //以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept4()也不会卡在这里; s = accept4(oldc->fd, &mysockaddr, &socklen, SOCK_NONBLOCK); //从内核获取一个用户端连接,最后一个参数SOCK_NONBLOCK表示返回一个非阻塞的socket,节省一次ioctl【设置为非阻塞】调用 } else { //以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept()也不会卡在这里; s = accept(oldc->fd, &mysockaddr, &socklen); } if(s == -1) { err = errno; //对accept、send和recv而言,事件未发生时errno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期待阻塞”) if(err == EAGAIN) //accept()没准备好,这个EAGAIN错误EWOULDBLOCK是一样的 { //除非你用一个循环不断的accept()取走所有的连接,不然一般不会有这个错误【我们这里只取一个连接,也就是accept()一次】 return ; } level = NGX_LOG_ALERT; if (err == ECONNABORTED) //ECONNRESET错误则发生在对方意外关闭套接字后【您的主机中的软件放弃了一个已建立的连接--由于超时或者其它失败而中止接连(用户插拔网线就可能有这个错误出现)】 { level = NGX_LOG_ERR; } else if (err == EMFILE || err == ENFILE) //EMFILE:进程的fd已用尽【已达到系统所允许单一进程所能打开的文件/套接字总数】。可参考:https://blog.youkuaiyun.com/sdn_prc/article/details/28661661 以及 https://bbs.youkuaiyun.com/topics/390592927 //ulimit -n ,看看文件描述符限制,如果是1024的话,需要改大; 打开的文件句柄数过多 ,把系统的fd软限制和硬限制都抬高. //ENFILE这个errno的存在,表明一定存在system-wide的resource limits,而不仅仅有process-specific的resource limits。按照常识,process-specific的resource limits,一定受限于system-wide的resource limits。 { level = NGX_LOG_CRIT; } //ngx_log_error_core(level,errno,"CSocekt::ngx_event_accept()中accept4()失败!"); if(use_accept4 && err == ENOSYS) //accept4()函数没实现,坑爹? { use_accept4 = 0; //标记不使用accept4()函数,改用accept()函数 continue; //回去重新用accept()函数搞 } if (err == ECONNABORTED) //对方关闭套接字 { //这个错误因为可以忽略,所以不用干啥 //do nothing } if (err == EMFILE || err == ENFILE) { //do nothing,这个官方做法是先把读事件从listen socket上移除,然后再弄个定时器,定时器到了则继续执行该函数,但是定时器到了有个标记,会把读事件增加到listen socket上去; //我这里目前先不处理吧【因为上边已经写这个日志了】; } return; } //end if(s == -1) //走到这里的,表示accept4()/accept()成功了 if(m_onlineUserCount >= m_worker_connections) //用户连接数过多,要关闭该用户socket,因为现在也没分配连接,所以直接关闭即可 { //ngx_log_stderr(0,"超出系统允许的最大连入用户数(最大允许连入数%d),关闭连入请求(%d)。",m_worker_connections,s); close(s); return ; } //如果某些恶意用户连上来发了1条数据就断,不断连接,会导致频繁调用ngx_get_connection()使用我们短时间内产生大量连接,危及本服务器安全 if(m_connectionList.size() > (m_worker_connections * 5)) { //比如你允许同时最大2048个连接,但连接池却有了 2048*5这么大的容量,这肯定是表示短时间内 产生大量连接/断开,因为我们的延迟回收机制,这里连接还在垃圾池里没有被回收 if(m_freeconnectionList.size() < m_worker_connections) { //整个连接池这么大了,而空闲连接却这么少了,所以我认为是 短时间内 产生大量连接,发一个包后就断开,我们不可能让这种情况持续发生,所以必须断开新入用户的连接 //一直到m_freeconnectionList变得足够大【连接池中连接被回收的足够多】 close(s); return ; } } //ngx_log_stderr(errno,"accept4成功s=%d",s); //s这里就是 一个句柄了 newc = ngx_get_connection(s); //这是针对新连入用户的连接,和监听套接字 所对应的连接是两个不同的东西,不要搞混 if(newc == NULL) { //连接池中连接不够用,那么就得把这个socekt直接关闭并返回了,因为在ngx_get_connection()中已经写日志了,所以这里不需要写日志了 if(close(s) == -1) { ngx_log_error_core(NGX_LOG_ALERT,errno,"CSocekt::ngx_event_accept()中close(%d)失败!",s); } return; } //...........将来这里会判断是否连接超过最大允许连接数,现在,这里可以不处理 //成功的拿到了连接池中的一个连接 memcpy(&newc->s_sockaddr,&mysockaddr,socklen); //拷贝客户端地址到连接对象【要转成字符串ip地址参考函数ngx_sock_ntop()】 if(!use_accept4) { //如果不是用accept4()取得的socket,那么就要设置为非阻塞【因为用accept4()的已经被accept4()设置为非阻塞了】 if(setnonblocking(s) == false) { //设置非阻塞居然失败 ngx_close_connection(newc); //关闭socket,这种可以立即回收这个连接,无需延迟,因为其上还没有数据收发,谈不到业务逻辑因此无需延迟; return; //直接返回 } } newc->listening = oldc->listening; //连接对象 和监听对象关联,方便通过连接对象找监听对象【关联到监听端口】 //newc->w_ready = 1; //标记可以写,新连接写事件肯定是ready的;【从连接池拿出一个连接时这个连接的所有成员都是0】 newc->rhandler = &CSocekt::ngx_read_request_handler; //设置数据来时的读处理函数,其实官方nginx中是ngx_http_wait_request_handler() newc->whandler = &CSocekt::ngx_write_request_handler; //设置数据发送时的写处理函数。 //客户端应该主动发送第一次的数据,这里将读事件加入epoll监控,这样当客户端发送数据来时,会触发ngx_wait_request_handler()被ngx_epoll_process_events()调用 if(ngx_epoll_oper_event( s, //socekt句柄 EPOLL_CTL_ADD, //事件类型,这里是增加 EPOLLIN|EPOLLRDHUP, //标志,这里代表要增加的标志,EPOLLIN:可读,EPOLLRDHUP:TCP连接的远端关闭或者半关闭 ,如果边缘触发模式可以增加 EPOLLET 0, //对于事件类型为增加的,不需要这个参数 newc //连接池中的连接 ) == -1) { //增加事件失败,失败日志在ngx_epoll_add_event中写过了,因此这里不多写啥; ngx_close_connection(newc);//关闭socket,这种可以立即回收这个连接,无需延迟,因为其上还没有数据收发,谈不到业务逻辑因此无需延迟; return; //直接返回 } /* else { //打印下发送缓冲区大小 int n; socklen_t len; len = sizeof(int); getsockopt(s,SOL_SOCKET,SO_SNDBUF, &n, &len); ngx_log_stderr(0,"发送缓冲区的大小为%d!",n); //87040 n = 0; getsockopt(s,SOL_SOCKET,SO_RCVBUF, &n, &len); ngx_log_stderr(0,"接收缓冲区的大小为%d!",n); //374400 int sendbuf = 2048; if (setsockopt(s, SOL_SOCKET, SO_SNDBUF,(const void *) &sendbuf,n) == 0) { ngx_log_stderr(0,"发送缓冲区大小成功设置为%d!",sendbuf); } getsockopt(s,SOL_SOCKET,SO_SNDBUF, &n, &len); ngx_log_stderr(0,"发送缓冲区的大小为%d!",n); //87040 } */ if(m_ifkickTimeCount == 1) { AddToTimerQueue(newc); } ++m_onlineUserCount; //连入用户数量+1 break; //一般就是循环一次就跳出去 } while (1); return; } 给你这段代码,详细讲一讲如果面试官问我延迟回收技术怎么做的,我应该怎么回答
最新发布
09-29
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值