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)\n"); exit(1); } opts = (opts | O_NONBLOCK); if(fcntl(sockfd, F_SETFL, opts) < 0) { perror("fcntl(F_SETFL)\n"); 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\n"); 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\n"); 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\r\nContent-Length: %d\r\n\r\nHello 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; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战