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;
}
nginx&http 第三章 ngx 事件event epoll 处理的更多相关文章
- nginx&http 第三章 ngx 事件event accept epoll /init
tcp 三次握手成功后,listen fd 可读,在process_event_timer 中调用rev->handler(rev)处理: 其回调函数为: ngx_event_accept / ...
- nginx&http 第三章 ngx 事件http 初始化1
在 http 配置块中,我们配置了 http 连接相关的信息,HTTP 框架也正是从这里启动的 在 nginx 初始化的过程中,执行了 ngx_init_cycle 函数,其中进行了配置文件解析,调用 ...
- nginx&http 第二章 ngx 事件event处理 数据结构
ngx_event.c :这个文件主要放置Nginx事件event模块的核心代码. 包含:进程事件分发器(ngx_process_events_and_timers).事件模块的模块和配置.模块初始化 ...
- nginx&http 第二章 ngx 事件event配置等初始化
event事件模块,配置分为两层:ngx_events_module 事件模块 和 ngx_event_core_module 事件核心模块.ngx_events_module:模块类型NGX_COR ...
- nginx&http 第三章 ngx http ngx_http_process_request_line读取和处理HTTP头部的行
在 ngx_http_wait_request_handler 的最后调用了 ngx_http_process_request_line 函数用来处理和解析这次请求的全文 在读事件被触发时,内核套接字 ...
- nginx&http 第二章 ngx 事件event初始化 ngx_event_process_init
|----------(ngx_worker_process_cycle->ngx_worker_process_init) |--------->for(;;) {ngx_process ...
- nginx&http 第三章 ngx 1-http ngx_http_wait_request_handler
对于活跃的 HTTP 连接,在执行连接建立回调函数 ngx_http_init_connection 的过程中会执行 ngx_http_wait_request_handler 回调函数, 负责 HT ...
- nginx&http 第三章 ngx http 框架处理流程
1. nginx 连接结构 ngx_connection_t 这个连接表示是客户端主动发起的.Nginx服务器被动接受的TCP连接,我们可以简单称其为被动连接.同时,在有些请求的处理过程中,Nginx ...
- nginx&http 第三章 ngx 请求处理的 11 个阶段 --ngx_http_process_request& ngx_http_handler
ngx_http_process_request如果设置了定时器则删除,既然所有的请求已经接收完毕,就不会再发生超时了 重设连接的读写回调函数 重设请求读事件回调函数 调用 ngx_http_hand ...
随机推荐
- vector专题
<C++程序设计语言(第4部分:标准库)> 31.4 容器 31.4.1 vector 31.4.1.1 vector和增长 重要知识点:vector的内存布局 vector不会在添加每个 ...
- 多测师讲解接口测试 _postman(下)_高级讲师肖sir
关联接口 定义:上个接口返回的参数作为下一个接口的入参 1)接口1:查询出所有的州,自治区,直辖市,省(且发送请求不需要入参) 接口url地址: http://www.webxml.com.cn/We ...
- iot平台
iot平台 iot平台卓岚云是一个免费的物联网云平台,用户只需按步骤完成注册即可免费使用卓岚云 .支持PC.Android.iOS多平台终端.用户可以在任何地方远程访问任何地方的串口设备,并对远程设备 ...
- 微信聊天记录导出为csv,并生成词云图
微信聊天记录生成特定图片图云 首先贴上github地址 https://github.com/ghdefe/WechatRecordToWordCloud 来个效果图 提取聊天记录到csv参考教程 h ...
- C语言实现表达式求值,支持+、-、*、/四则运算,并且支持多级括号,自定义了栈的操作。
以下是代码的实现使用gcc已经成功运行了,下面是效果图 #include <stdio.h> #include <stdlib.h> #define OPT_ADD 43 /* ...
- Convert to Ones CodeForces(超水题)
题目大意:给你几个数,这些数里面只有0或1,你有两种操作:1.把一段区域内的所有数前后交换位置.2.把一段区域内所有数取反.(区域可大可小,可以是所有数也 ...
- linux-设置hostname
设置linux root后面的名字
- TCP/IP的十个问题
一.TCP/IP模型 TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的 ...
- wifi - 无线相关命令
1.Linux环境下的无线相关操作命令 interface 指代当前网卡 一般是 wifi0 , eth0 , ath1等 ifconfig - 常用查看网络设定及控制网卡(Windows下是ip ...
- 从Linux源码看Socket(TCP)的listen及连接队列
从Linux源码看Socket(TCP)的listen及连接队列 前言 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 今天笔者就来从Linux源码的角度看 ...