• nginx&http 第三章 ngx 事件event epoll 处理


    1. epoll模块命令集 ngx_epoll_commands  epoll模块上下文 ngx_epoll_module_ctx  epoll模块配置 ngx_epoll_module

    static ngx_command_t  ngx_epoll_commands[] = {
        /*
        在调用epoll_wait时,将由第2和第3个参数告诉Linux内核一次最多可返回多少个事件。
        这个配置项表示调用一次epoll_wait时最多可返回
        的事件数,当然,它也会预分配那么多epoll_event结构体用于存储事件
         */
        { ngx_string("epoll_events"),
          NGX_EVENT_CONF|NGX_CONF_TAKE1,
          ngx_conf_set_num_slot,
          0,
          offsetof(ngx_epoll_conf_t, events),
          NULL },
    
        /*
        在开启异步I/O且使用io_setup系统调用初始化异步I/O上下文环境时,初始分配的异步I/O事件个数
         */
        { ngx_string("worker_aio_requests"),
          NGX_EVENT_CONF|NGX_CONF_TAKE1,
          ngx_conf_set_num_slot,
          0,
          offsetof(ngx_epoll_conf_t, aio_requests),
          NULL },
    
          ngx_null_command
    };
    ngx_event_module_t  ngx_epoll_module_ctx = {
        &epoll_name,
        ngx_epoll_create_conf,               /* create configuration */
        ngx_epoll_init_conf,                 /* init configuration */
    
        {
            ngx_epoll_add_event,             /* add an event */  //ngx_add_event
            ngx_epoll_del_event,             /* delete an event */
            ngx_epoll_add_event,             /* enable an event */ //ngx_add_conn
            ngx_epoll_del_event,             /* disable an event */
            ngx_epoll_add_connection,        /* add an connection */
            ngx_epoll_del_connection,        /* delete an connection */
    #if (NGX_HAVE_EVENTFD)
            ngx_epoll_notify,                /* trigger a notify */
    #else
            NULL,                            /* trigger a notify */
    #endif
            ngx_epoll_process_events,        /* process the events */
            ngx_epoll_init,                  /* init the events */ //在创建的子进程中执行
            ngx_epoll_done,                  /* done the events */
        }
    };
    
    ngx_module_t  ngx_epoll_module = {
        NGX_MODULE_V1,
        &ngx_epoll_module_ctx,               /* module context */
        ngx_epoll_commands,                  /* module directives */
        NGX_EVENT_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
    };

    2.初始化

    epool模块属于Event模块下面的子模块,配置文件初始化的时候,在Event解析配置文件的核心函数:ngx_events_block 中处理。

    epool模块属于Event模块下面的子模块,所以没有设置独立的init_process初始化回调函数。

    epoll事件模块的初始化放在:ngx_event_process_init中进行;最后调用module->actions.init(cycle, ngx_timer_resolution)----->ngx_epoll_init

    static ngx_int_t
    ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
    {
        ngx_epoll_conf_t  *epcf;
    
        epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);
    
        if (ep == -1) {
           /*
            epoll_create返回一个句柄,之后epoll的使用都将依靠这个句柄来标识。参数size是告诉epoll所要处理的大致事件数目。不再使用epoll时,
            必须调用close关闭这个句柄。注意size参数只是告诉内核这个epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在Linux蕞
            新的一些内核版本的实现中,这个size参数没有任何意义。
    
            调用epoll_create在内核中创建epoll对象。上文已经讲过,参数size不是用于指明epoll能够处理的最大事件个数,因为在许多Linux内核
            版本中,epoll是不处理这个参数的,所以设为cycle->connectionn/2(而不是cycle->connection_n)也不要紧
            */
            ep = epoll_create(cycle->connection_n / 2);
    
            if (ep == -1) {
                ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
                              "epoll_create() failed");
                return NGX_ERROR;
            }
    
    #if (NGX_HAVE_EVENTFD)
            if (ngx_epoll_notify_init(cycle->log) != NGX_OK) {
                ngx_epoll_module_ctx.actions.notify = NULL;
            }
    #endif
    
    #if (NGX_HAVE_FILE_AIO)
    
            ngx_epoll_aio_init(cycle, epcf);
    
    #endif
        }
    
        if (nevents < epcf->events) {
            if (event_list) {
                ngx_free(event_list);
            }
    
            event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events,
                                   cycle->log);
            if (event_list == NULL) {
                return NGX_ERROR;
            }
        }
    
        nevents = epcf->events;//nerents也是配置项epoll_events的参数
    
        ngx_io = ngx_os_io;
    
        ngx_event_actions = ngx_epoll_module_ctx.actions;
    
    #if (NGX_HAVE_CLEAR_EVENT)
        //默认是采用LT模式来使用epoll的,NGX USE CLEAR EVENT宏实际上就是在告诉Nginx使用ET模式
        ngx_event_flags = NGX_USE_CLEAR_EVENT
    #else
        ngx_event_flags = NGX_USE_LEVEL_EVENT
    #endif
                          |NGX_USE_GREEDY_EVENT
                          |NGX_USE_EPOLL_EVENT;
    
        return NGX_OK;
    }

    核心函数:epool process

    //ngx_epoll_process_events注册到ngx_process_events的  
    //和ngx_epoll_add_event配合使用
    //该函数在ngx_process_events_and_timers中调用
    static ngx_int_t
    ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)//flags参数中含有NGX_POST_EVENTS表示这批事件要延后处理
    {
        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;
        char epollbuf[256];
        
        /* NGX_TIMER_INFINITE == INFTIM */
    
        //ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "begin to epoll_wait, epoll timer: %M ", timer);
    
        /*
         调用epoll_wait获取事件。注意,timer参数是在process_events调用时传入的,在9.7和9.8节中会提到这个参数
         */
        //The call was interrupted by a signal handler before any of the requested events occurred or the timeout expired;
        //如果有信号发生(见函数ngx_timer_signal_handler),如定时器,则会返回-1
        //需要和ngx_add_event与ngx_add_conn配合使用        
        //event_list存储的是就绪好的事件,如果是select则是传入用户注册的事件,需要遍历检查,而且每次select返回后需要重新设置事件集,epoll不用
        /*
        这里面等待的事件包括客户端连接事件(这个是从父进程继承过来的ep,然后在子进程while前的ngx_event_process_init->ngx_add_event添加),
        对已经建立连接的fd读写事件的添加在ngx_event_accept->ngx_http_init_connection->ngx_handle_read_event
        */
    
    /*
    ngx_notify->ngx_epoll_notify只会触发epoll_in,不会同时引发epoll_out,如果是网络读事件epoll_in,则会同时引起epoll_out
    */
        events = epoll_wait(ep, event_list, (int) nevents, timer);  //timer为-1表示无限等待   nevents表示最多监听多少个事件,必须大于0
        //EPOLL_WAIT如果没有读写事件或者定时器超时事件发生,则会进入睡眠,这个过程会让出CPU
    
        err = (events == -1) ? ngx_errno : 0;
    
        //当flags标志位指示要更新时间时,就是在这里更新的
        //要摸ngx_timer_resolution毫秒超时后跟新时间,要摸epoll读写事件超时后跟新时间
        if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
            ngx_time_update();
        }
    
        if (err) {
            if (err == NGX_EINTR) {
    
                if (ngx_event_timer_alarm) { //定时器超时引起的epoll_wait返回
                    ngx_event_timer_alarm = 0;
                    return NGX_OK;
                }
    
                level = NGX_LOG_INFO;
    
            } else {
                level = NGX_LOG_ALERT;
            }
    
            ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
            return NGX_ERROR;
        }
    
        if (events == 0) {
            if (timer != NGX_TIMER_INFINITE) {
                return NGX_OK;
            }
    
            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                          "epoll_wait() returned no events without timeout");
            return NGX_ERROR;
        }
    
        //遍历本次epoll_wait返回的所有事件
        for (i = 0; i < events; i++) { //和ngx_epoll_add_event配合使用
            /*
            对照着上面提到的ngx_epoll_add_event方法,可以看到ptr成员就是ngx_connection_t连接的地址,但最后1位有特殊含义,需要把它屏蔽掉
              */
            c = event_list[i].data.ptr; //通过这个确定是那个连接
    
            instance = (uintptr_t) c & 1; //将地址的最后一位取出来,用instance变量标识, 见ngx_epoll_add_event
    
            /*
              无论是32位还是64位机器,其地址的最后1位肯定是0,可以用下面这行语句把ngx_connection_t的地址还原到真正的地址值
              */ //注意这里的c有可能是accept前的c,用于检测是否客户端发起tcp连接事件,accept返回成功后会重新创建一个ngx_connection_t,用来读写客户端的数据
            c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
    
            rev = c->read; //取出读事件 //注意这里的c有可能是accept前的c,用于检测是否客户端发起tcp连接事件,accept返回成功后会重新创建一个ngx_connection_t,用来读写客户端的数据
    
            if (c->fd == -1 || rev->instance != instance) { //判断这个读事件是否为过期事件
              //当fd套接字描述符为-l或者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; //取出事件类型
            ngx_epoll_event_2str(revents, epollbuf);
    
            memset(epollbuf, 0, sizeof(epollbuf));
            ngx_epoll_event_2str(revents, epollbuf);
            ngx_log_debug4(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll: fd:%d %s(ev:%04XD) d:%p",
                           c->fd, epollbuf, revents, event_list[i].data.ptr);
    
            if (revents & (EPOLLERR|EPOLLHUP)) { //例如对方close掉套接字,这里会感应到
                ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                               "epoll_wait() error on fd:%d ev:%04XD",
                               c->fd, revents);
            }
    
    
            if ((revents & (EPOLLERR|EPOLLHUP)) 
                 && (revents & (EPOLLIN|EPOLLOUT)) == 0)
            {
                /*
                 * if the error events were returned without EPOLLIN or EPOLLOUT,
                 * then add these flags to handle the events at least in one
                 * active handler
                 */
    
                revents |= EPOLLIN|EPOLLOUT; //epoll EPOLLERR|EPOLLHUP实际上是通过触发读写事件进行读写操作recv write来检测连接异常
            }
    
            if ((revents & EPOLLIN) && rev->active) { //如果是读事件且该事件是活跃的
    
    #if (NGX_HAVE_EPOLLRDHUP)
                if (revents & EPOLLRDHUP) {
                    rev->pending_eof = 1;
                }
    #endif
                //注意这里的c有可能是accept前的c,用于检测是否客户端发起tcp连接事件,accept返回成功后会重新创建一个ngx_connection_t,用来读写客户端的数据
                rev->ready = 1; //表示已经有数据到了这里只是把accept成功前的 ngx_connection_t->read->ready置1,
    //accept返回后会重新从连接池中获取一个ngx_connection_t
    //flags参数中含有NGX_POST_EVENTS表示这批事件要延后处理 if (flags & NGX_POST_EVENTS) { /* 如果要在post队列中延后处理该事件,首先要判断它是新连接事件还是普通事件,以决定把它加入 到ngx_posted_accept_events队列或者ngx_postedL events队列中。关于post队列中的事件何时执行 */ queue = rev->accept ? &ngx_posted_accept_events : &ngx_posted_events; ngx_post_event(rev, queue); } else { //如果接收到客户端数据,这里为ngx_http_wait_request_handler rev->handler(rev); //如果为还没accept,则为ngx_event_process_init中设置为ngx_event_accept。如果已经建立连接
    //则读数据为ngx_http_process_request_line
    } } wev = c->write; if ((revents & EPOLLOUT) && wev->active) { if (c->fd == -1 || wev->instance != instance) { //判断这个读事件是否为过期事件 //当fd套接字描述符为-1或者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); //将这个事件添加到post队列中延后处理 } else { //立即调用这个写事件的回调方法来处理这个事件 wev->handler(wev); } } } return NGX_OK; }

    (1)epoll_ctl系统调用
    epoll_ctl在C库中的原型如下。
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)j
    epoll_ctl向epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返
    回一1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过
    epoll_ctl添加刮epoll中的。参数epfd是epoll_create返回的句柄,而op参数的意义见表
    9-2。
    ┏━━━━━━━━━━┳━━━━━━━━━━━━━┓
    ┃ ‘ op的取值 ┃ 意义 ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL_ADD ┃ 添加新的事件到epoll中 ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL MOD ┃ 修改epoll中的事件 ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL_DEL ┃ 删除epoll中的事件 ┃
    ┗━━━━━━━━━━┻━━━━━━━━━━━━━┛
    第3个参数fd是待监测的连接套接字,第4个参数是在告诉epoll对什么样的事件感
    兴趣,它使用了epoll_event结构体,在上文介绍过的epoll实现机制中会为每一个事件创
    建epitem结构体,而在epitem中有一个epoll_event类型的event成员。下面看一下epoll_
    event的定义。
    struct epoll_event{
    _uint32 t events;
    epoll data_t data;
    };
    events的取值见表9-3。
    表9-3 epoll_event中events的取值意义
    ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
    ┃ events取值 ┃ 意义 ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLIN ┃ ┃
    ┃ ┃ 表示对应的连接上有数据可以读出(TCP连接的远端主动关闭连接,也相当于可读事 ┃
    ┃ ┃件,因为需要处理发送来的FIN包) ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLOUT ┃ 表示对应的连接上可以写入数据发送(主动向上游服务器发起非阻塞的TCP连接,连接 ┃
    ┃ ┃建立成功的事件相当于可写事件) ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLRDHUP ┃ 表示TCP连接的远端关闭或半关闭连接 ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLPRI ┃ 表示对应的连接上有紧急数据需要读 ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLERR ┃ 表示对应的连接发生错误 ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLHUP ┃ 表示对应的连接被挂起 ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLET ┃ 表示将触发方式设置妁边缘触发(ET),系统默认为水平触发(LT’) ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLONESHOT ┃ 表示对这个事件只处理一次,下次需要处理时需重新加入epoll ┃
    ┗━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
    而data成员是一个epoll_data联合,其定义如下。

    typedef union epoll_data
        void *ptr;
        int fd;
        uint32_t u32 ;
        uint64_t u64 ;
    } epoll_data_t;

    可见,这个data成员还与具体的使用方式相关。例如,ngx_epoll_module模块只使用了

    联合中的ptr成员,作为指向ngx_connection_t连接的指针

    (2) epoll_wait系统调用

        epoll_wait在C库中的原型如下。
    int  epoll_wait (int  epfd, struct  epoll_event*  events, int  maxevents, int  timeout) ;
        收集在epoll监控的事件中已经发生的事件,如果epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回。epoll_wait的返回值表示当前发生的事件个数,如果返回0,则
    表示本次调用中没有事件发生,如果返回一1,则表示出现错误,需要检查errno错误码判断错误类型。第1个参数epfd是epoll的描述符。第2个参数events则是分配好的epoll_event
    结构体数组,如ou将会把发生的事件复制到eVents数组中(eVents不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存。内核这种做法
    效率很高)。第3个参数maxevents表示本次可以返回的最大事件数目,通常maxevents参数与预分配的eV|ents数组的大小是相等的。第4个参数timeout表示在没有检测到事件发生时
    最多等待的时间(单位为毫秒),如果timeout为0,则表示ep01l—wait在rdllist链表中为空,立刻返回,不会等待。
        ep011有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。默认情况下,ep011采用LT模式工作,这时可以处理阻塞和非阻塞套接字,而表9—3中的EPOLLET表示
    可以将一个事件改为ET模式。ET模式的效率要比LT模式高,它只支持非阻塞套接字。ET模式与LT模式的区别在于,当一个新的事件到来时,ET模式下当然可以从epoU.wait调用
    中获取到这个事件,可是加果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在ET模式下是无法再次从epoll__Wait调用中获取这个事件的;
    而LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll-wait中获取这个事件。因此,在LT模式下开发基于epoll的应用要简单一些,不太容易出错,而在
    ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。默认情况下,Nginx是通过ET模式使用epoU的,在下文中就可以看到相关
    内容。
    
    1)、epoll_create函数
        函数声明:int epoll_create(int size)
       该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。
    2)、epoll_ctl函数
       函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
       该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
    参数:
       epfd:由 epoll_create 生成的epoll专用的文件描述符;
       op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、   EPOLL_CTL_DEL 删除;
       fd:关联的文件描述符;
       event:指向epoll_event的指针;
       如果调用成功则返回0,不成功则返回-1。
    3)、epoll_wait函数
       函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
       该函数用于轮询I/O事件的发生。
       参数:
       epfd:由epoll_create 生成的epoll专用的文件描述符;
       epoll_event:用于回传代处理事件的数组;
       maxevents:每次能处理的事件数;
       timeout:等待I/O事件发生的超时值;
    #define EPOLLIN        0x001
    #define EPOLLPRI       0x002
    #define EPOLLOUT       0x004
    #define EPOLLRDNORM    0x040
    #define EPOLLRDBAND    0x080
    #define EPOLLWRNORM    0x100
    #define EPOLLWRBAND    0x200
    #define EPOLLMSG       0x400
    #define EPOLLERR       0x008 
    //EPOLLERR|EPOLLHUP都表示连接异常情况 fd用完或者其他吧
    //epoll EPOLLERR|EPOLLHUP实际上是通过触发读写事件进行读写操作recv write来检测连接异常 #define EPOLLHUP 0x010 #define EPOLLRDHUP 0x2000 //当对端已经关闭,本端写数据,会引起该事件 #define EPOLLET 0x80000000 //表示将触发方式设置妁边缘触发(ET),系统默认为水平触发(LT’) //设置该标记后读取完数据后,如果正在处理数据过程中又有新的数据到来,不会触发epoll_wait返回,
    //除非数据处理完毕后重新add epoll_ctl操作,参考<linux高性能服务器开发> #define EPOLLONESHOT 0x40000000

    在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block,perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket, read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。 所以,对于阻塞socket,read/write返回-1代表网络出错了。但对于非阻塞socket,read/write返回-1不一定网络真的出错了。 可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。 综上,对于non-blocking的socket,正确的读写操作为: 读:忽略掉errno = EAGAIN的错误,下次继续读 写:忽略掉errno = EAGAIN的错误,下次继续写 对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。 epoll的两种模式LT和ET 二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket; 而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。 所以,在epoll的ET模式下,正确的读写方式为: 读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN 写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN 正确的读 n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { perror("read error"); } 正确的写 int nwrite, data_size = strlen(buf); n = data_size; while (n > 0) { nwrite = write(fd, buf + data_size - n, n); if (nwrite < n) { if (nwrite == -1 && errno != EAGAIN) { perror("write error"); } break; } n -= nwrite; } 正确的accept,accept 要考虑 2 个问题 (1) 阻塞模式 accept 存在的问题 考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出, 如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单 纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。 解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的 实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。 (2)ET模式下accept存在的问题 考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理 一个连接,导致TCP就绪队列中剩下的连接都得不到处理。 解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept 返回-1并且errno设置为EAGAIN就表示所有连接都处理完。 综合以上两种情况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用方式为: while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { handle_client(conn_sock); } if (conn_sock == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); } 一道腾讯后台开发的面试题 使用Linuxepoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理? 第一种最普遍的方式: 需要向socket写数据的时候才把socket加入epoll,等待可写事件。 接受到可写事件后,调用write或者send发送数据。 当所有数据都写完后,把socket移出epoll。 这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。 一种改进的方式: 开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的 驱动下写数据,全部数据发送完毕后,再移出epoll。 这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。
    epoll使用范例
    #include <sys/socket.h>
     #include <sys/wait.h>
     #include <netinet/in.h>
     #include <netinet/tcp.h>
     #include <sys/epoll.h>
     #include <sys/sendfile.h>
     #include <sys/stat.h>
     #include <unistd.h>
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <strings.h>
     #include <fcntl.h>
     #include <errno.h> 
     
     #define MAX_EVENTS 10
     #define PORT 8080
     
     //设置socket连接为非阻塞模式
     void setnonblocking(int sockfd) {
         int opts;
     
         opts = fcntl(sockfd, F_GETFL);
         if(opts < 0) {
             perror("fcntl(F_GETFL)
    ");
             exit(1);
         }
         opts = (opts | O_NONBLOCK);
         if(fcntl(sockfd, F_SETFL, opts) < 0) {
             perror("fcntl(F_SETFL)
    ");
             exit(1);
         }
     }
     
     int main(){
         struct epoll_event ev, events[MAX_EVENTS];
         int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
         struct sockaddr_in local, remote;
         char buf[BUFSIZ];
     
         //创建listen socket
         if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
             perror("sockfd
    ");
             exit(1);
         }
         setnonblocking(listenfd);
         bzero(&local, sizeof(local));
         local.sin_family = AF_INET;
         local.sin_addr.s_addr = htonl(INADDR_ANY);;
         local.sin_port = htons(PORT);
         if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {
             perror("bind
    ");
             exit(1);
         }
         listen(listenfd, 20);
     
         epfd = epoll_create(MAX_EVENTS);
         if (epfd == -1) {
             perror("epoll_create");
             exit(EXIT_FAILURE);
         }
     
         ev.events = EPOLLIN;
         ev.data.fd = listenfd;
         if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
             perror("epoll_ctl: listen_sock");
             exit(EXIT_FAILURE);
         }
     
         for (;;) {
             nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
             if (nfds == -1) {
                 perror("epoll_pwait");
                 exit(EXIT_FAILURE);
             }
     
             for (i = 0; i < nfds; ++i) {
                 fd = events[i].data.fd;
                 if (fd == listenfd) {
                     while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, 
                                     (size_t *)&addrlen)) > 0) {
                         setnonblocking(conn_sock);
                         ev.events = EPOLLIN | EPOLLET;
                         ev.data.fd = conn_sock;
                         if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
                                     &ev) == -1) {
                             perror("epoll_ctl: add");
                             exit(EXIT_FAILURE);
                         }
                     }
                     if (conn_sock == -1) {
                         if (errno != EAGAIN && errno != ECONNABORTED 
                                 && errno != EPROTO && errno != EINTR) 
                             perror("accept");
                     }
                     continue;
                 }  
                 if (events[i].events & EPOLLIN) {
                     n = 0;
                     while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
                         n += nread;
                     }
                     if (nread == -1 && errno != EAGAIN) {
                         perror("read error");
                     }
                     ev.data.fd = fd;
                     ev.events = events[i].events | EPOLLOUT;
                     if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {
                         perror("epoll_ctl: mod");
                     }
                 }
                 if (events[i].events & EPOLLOUT) {
                     sprintf(buf, "HTTP/1.1 200 OK
    Content-Length: %d
    
    Hello World", 11);
                     int nwrite, data_size = strlen(buf);
                     n = data_size;
                     while (n > 0) {
                         nwrite = write(fd, buf + data_size - n, n);
                         if (nwrite < n) {
                             if (nwrite == -1 && errno != EAGAIN) {
                                 perror("write error");
                             }
                             break;
                         }
                         n -= nwrite;
                     }
                     close(fd);
                 }
             }
         }
     
         return 0;
     } 

     ngx_epoll_add_event 添加一个事件

    /*
    epoll_ctl系统调用
        epoll_ctl在C库中的原型如下。
        int epoll_ctl(int  epfd, int  op, int  fd, struct  epoll_event*  event)j
        epoll_ctl向epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返
    回一1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过
    epoll_ctl添加刮epoll中的。参数epfd是epoll_create返回的句柄,而op参数的意义见表
    9-2。
    ┏━━━━━━━━━━┳━━━━━━━━━━━━━┓
    ┃    ‘    op的取值  ┃    意义                  ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL_ADD     ┃    添加新的事件到epoll中 ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL MOD     ┃    修改epoll中的事件     ┃
    ┣━━━━━━━━━━╋━━━━━━━━━━━━━┫
    ┃I EPOLL_CTL_DEL     ┃    删除epoll中的事件     ┃
    ┗━━━━━━━━━━┻━━━━━━━━━━━━━┛
        第3个参数fd是待监测的连接套接字,第4个参数是在告诉epoll对什么样的事件感
    兴趣,它使用了epoll_event结构体,在上文介绍过的epoll实现机制中会为每一个事件创
    建epitem结构体,而在epitem中有一个epoll_event类型的event成员。下面看一下epoll_
    event的定义。
    struct epoll_event{
            _uint32 t events;
            epoll data_t data;
    };
    events的取值见表9-3。
    表9-3 epoll_event中events的取值意义
    ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
    ┃    events取值  ┃    意义                                                                      ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLIN         ┃                                                                              ┃
    ┃                ┃  表示对应的连接上有数据可以读出(TCP连接的远端主动关闭连接,也相当于可读事   ┃
    ┃                ┃件,因为需要处理发送来的FIN包)                                               ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLOUT        ┃  表示对应的连接上可以写入数据发送(主动向上游服务器发起非阻塞的TCP连接,连接 ┃
    ┃                ┃建立成功的事件相当于可写事件)                                                ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLRDHUP      ┃  表示TCP连接的远端关闭或半关闭连接                                           ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLPRI        ┃  表示对应的连接上有紧急数据需要读                                            ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLERR        ┃  表示对应的连接发生错误                                                      ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLHUP        ┃  表示对应的连接被挂起                                                        ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLET         ┃  表示将触发方式设置妁边缘触发(ET),系统默认为水平触发(LT’)                  ┃
    ┣━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
    ┃EPOLLONESHOT    ┃  表示对这个事件只处理一次,下次需要处理时需重新加入epoll                     ┃
    ┗━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
    而data成员是一个epoll_data联合,其定义如下。
    typedef union epoll_data
           void        *ptr;
          int           fd;
         uint32_t            u32 ;
         uint64_t            u64 ;
    } epoll_data_t;
        可见,这个data成员还与具体的使用方式相关。例如,ngx_epoll_module模块只使用了
    联合中的ptr成员,作为指向ngx_connection_t连接的指针。
    */ 
    //ngx_epoll_add_event表示添加某种类型的(读或者写,通过flag指定促发方式,NGX_CLEAR_EVENT为ET方式,NGX_LEVEL_EVENT为LT方式)事件,
    //ngx_epoll_add_connection(读写一起添加上去, 使用EPOLLET边沿触发方式)
    static ngx_int_t      //通过flag指定促发方式,NGX_CLEAR_EVENT为ET方式,NGX_LEVEL_EVENT为LT方式
    ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags) //该函数封装为ngx_add_event的,使用的时候为ngx_add_event
    { //一般网络事件中的报文读写通过ngx_handle_read_event  ngx_handle_write_event添加事件
        int                  op;
        uint32_t             events, prev;
        ngx_event_t         *e;
        ngx_connection_t    *c;
        struct epoll_event   ee;
    
        c = ev->data; //每个事件的data成员都存放着其对应的ngx_connection_t连接
    
        /*
        下面会根据event参数确定当前事件是读事件还是写事件,这会决定eventg是加上EPOLLIN标志位还是EPOLLOUT标志位
         */
        events = (uint32_t) event;
    
        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
        }
    
        //第一次添加epoll_ctl为EPOLL_CTL_ADD,如果再次添加发现active为1,则epoll_ctl为EPOLL_CTL_MOD
        if (e->active) { //根据active标志位确定是否为活跃事件,以决定到底是修改还是添加事件
            op = EPOLL_CTL_MOD; 
            events |= prev; //如果是active的,则events= EPOLLIN|EPOLLRDHUP|EPOLLOUT;
    
        } else {
            op = EPOLL_CTL_ADD;
        }
    
        ee.events = events | (uint32_t) flags; //加入flags参数到events标志位中
        /* ptr成员存储的是ngx_connection_t连接,可参见epoll的使用方式。*/
        ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);
    
        if (e->active) {//modify
            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                       "epoll modify read and write event: fd:%d op:%d ev:%08XD", c->fd, op, ee.events);
        } else {//add
            if (event == NGX_READ_EVENT) {
                ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                           "epoll add read event: fd:%d op:%d ev:%08XD", c->fd, op, ee.events);
            } else
                ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                           "epoll add write event: fd:%d op:%d ev:%08XD", c->fd, op, ee.events);
        }
      
        //EPOLL_CTL_ADD一次后,就可以一直通过epoll_wait来获取读事件,除非调用EPOLL_CTL_DEL,不是每次读事件触发epoll_wait返回后都要重新添加EPOLL_CTL_ADD,
        //之前代码中有的地方好像备注错了,备注为每次读事件触发后都要重新add一次
        if (epoll_ctl(ep, op, c->fd, &ee) == -1) {//epoll_wait() 系统调用等待由文件描述符 c->fd 引用的 epoll 实例上的事件
            ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
                          "epoll_ctl(%d, %d) failed", op, c->fd);
            return NGX_ERROR;
        }
        //后面的ngx_add_event->ngx_epoll_add_event中把listening中的c->read->active置1, ngx_epoll_del_event中把listening中置read->active置0
        //第一次添加epoll_ctl为EPOLL_CTL_ADD,如果再次添加发现active为1,则epoll_ctl为EPOLL_CTL_MOD
        ev->active = 1; //将事件的active标志位置为1,表示当前事件是活跃的   ngx_epoll_del_event中置0
    #if 0
        ev->oneshot = (flags & NGX_ONESHOT_EVENT) ? 1 : 0;
    #endif
    
        return NGX_OK;
    }


  • 相关阅读:
    使用VMWare实现主机一拖二(笔记本分身术)
    Implementing RelativeSource binding in Silverlight
    CLRProfiler V4 Released
    Host WCF on IIS 7.5
    增加智能感知的RichTextBox扩展控件(WPF)
    Troubleshooting Visual Studio 2010 and QT 4.7 Integration
    windows命令行下如何查看磁盘空间大小
    模拟谷歌今日使用的css动画
    粗聊Mysql——你会建库建表么?
    彩票项目难点分析
  • 原文地址:https://www.cnblogs.com/codestack/p/12153425.html
Copyright © 2020-2023  润新知