11epoll事件驱动模型
一、项目代码树梳理



(i)ngx_master_process_cycle() //创建子进程等一系列动作
(i) ngx_setproctitle() //设置进程标题
(i) ngx_start_worker_processes() //创建worker子进程
(i) for (i = 0; i < threadnums; i++) //master进程在走这个循环,来创建若干个子进程
(i) ngx_spawn_process(i,"worker process");
(i) pid = fork(); //分叉,从原来的一个master进程(一个叉),分成两个叉(原有的master进程,以及一个新fork()出来的worker进程
(i) //只有子进程这个分叉才会执行ngx_worker_process_cycle()
(i) ngx_worker_process_cycle(inum,pprocname); //子进程分叉
(i) ngx_worker_process_init();
(i) sigemptyset(&set);
(i) sigprocmask(SIG_SETMASK, &set, NULL); //允许接收所有信号
(i) g_socket.ngx_epoll_init(); //初始化epoll相关内容,同时 往监听socket上增加监听事件,从而开始让监听端口履行其职责
(i) m_epollhandle = epoll_create(m_worker_connections);
(i) ngx_epoll_add_event((*pos)->fd....);
(i) epoll_ctl(m_epollhandle,eventtype,fd,&ev);
(i) ngx_setproctitle(pprocname); //重新为子进程设置标题为worker process
(i) for ( ;; )
(i) {
(i) //子进程开始在这里不断的死循环
(i)------------------------ngx_process_events_and_timers(); //处理网络事件和定时器事件
(i) g_socket.ngx_epoll_process_events(-1); //-1表示卡着等待吧
(i) }
(i) sigemptyset(&set);
(i) for ( ;; ) {}. //父进程[master进程]会一直在这里循环
二、epoll_create(), epoll_ctl(), epoll_wait()部署
2.1 epoll_create()
epoll_create()是创建出一个epoll的模型,作为初始化的,每个子进程中都有一个。
所以放在ngx_worker_process_init(inum);中,其中不仅包含子进程的信号集的处理,还包括对epoll的初始化g_socket.ngx_epoll_init();。
任务:
m_epollhandle = epoll_create(m_worker_connections)- 创建连接池【数组】
m_pconnections = new ngx_connection_t[m_connection_n] - 并且把 遍历所有监听socket【监听端口】,为每个监听socket增加一个 连接池中的连接。之前监听了80和443端口,这里就消耗了连接池中的两个连接。(把 套接字数字跟一块内存捆绑,达到的效果就是:将来我通过这个套接字,就能够把这块内存拿出来;)
- 在3中,还往这个监听socket上增加监听事件,
c为分配好的连接池连接,c连接上会注册好函数(函数指针ngx_event_accept())
ngx_epoll_add_event((*pos)->fd,1,0,0,EPOLL_CTL_ADD,c)
2.2 epoll_ctl()
这个函数在ngx_epoll_add_event()中调用。
使用下面这个语句,将 事件ev和 连接c 联系起来
ev.data.ptr = (void *)( (uintptr_t)c | c->instance);
调用:epoll_ctl(m_epollhandle,eventtype,fd,&ev)
还有另外的地方也会调用ngx_epoll_add_event(),在上面的ngx_event_accept()中。ngx_epoll_add_event(s,1,0,EPOLLET,EPOLL_CTL_ADD,newc),newc这个连接上会注册函数ngx_wait_request_handler()
2.3 epoll_wait()
调用位置:
ngx_worker_process_cycle()->while(1)->
ngx_process_events_and_timers()->g_socket.ngx_epoll_process_events(-1)
->epoll_wait(m_epollhandle,m_events,NGX_MAX_EVENTS,timer);
->for(int i = 0; i < events; ++i)有这么多事件呀->判断呀分类呀->
目前有读事件处理->(this->* (c->rhandler) )(c);
->这里就执行对应连接中的对应的注册函数啦啦啦(CSocekt::ngx_event_accept和CSocekt::ngx_wait_request_handler)
三、各种各样的细节
3.1 一个TCP连接的结构体
这是一个连接池中一个连接,包含各类信息,其中关联了它是 由原先那个监听套接字和端口 产生出来的。
struct ngx_connection_s
{
int fd; //套接字句柄socket
lpngx_listening_t listening; //如果这个链接被分配给了一个监听套接字,那么这个里边就指向监听套接字对应的那个lpngx_listening_t的内存首地址
//------------------------------------
unsigned instance:1; //【位域】失效标志位:0:有效,1:失效【这个是官方nginx提供,到底有什么用,ngx_epoll_process_events()中详解】
uint64_t iCurrsequence; //我引入的一个序号,每次分配出去时+1,此法也有可能在一定程度上检测错包废包,具体怎么用,用到了再说
struct sockaddr s_sockaddr; //保存对方地址信息用的
//char addr_text[100]; //地址的文本信息,100足够,一般其实如果是ipv4地址,255.255.255.255,其实只需要20字节就够
//和读有关的标志-----------------------
//uint8_t r_ready; //读准备好标记【暂时没闹明白官方要怎么用,所以先注释掉】
uint8_t w_ready; //写准备好标记
ngx_event_handler_pt rhandler; //读事件的相关处理方法
ngx_event_handler_pt whandler; //写事件的相关处理方法
//--------------------------------------------------
lpngx_connection_t data; //这是个指针【等价于传统链表里的next成员:后继指针】,指向下一个本类型对象,用于把空闲的连接池对象串起来构成一个单向链表,方便取用
};
3.2 一个监听端口有关的结构体
这个结构体是关联 监听端口和套接字 与 TCP连接结构
typedef struct ngx_listening_s //和监听端口有关的结构
{
int port; //监听的端口号
int fd; //套接字句柄socket
lpngx_connection_t connection; //连接池中的一个连接,注意这是个指针
}ngx_listening_t,*lpngx_listening_t;
3.3 技术:过期事件和过期连接的判断,“TCP连接结构体”上辈子和这辈子的区分
采用了两个过程的过滤:
ngx_epoll_process_events()-> epoll_wait()
是的,
epoll说:这么多事件已经放在待取的位置了,你判断下事件值不值得处理下去。
程序:好的,我会进行两个过程的过滤。
if(c->fd == -1)和if(c->instance != instance)
解释:
- 关闭连接时这个fd会被设置为-1。
用epoll_wait取得三个事件,处理第一个事件时,因为业务需要,我们把这个连接关闭,那我们应该会把c->fd设置为-1;第二个事件照常处理,第三个事件,假如这第三个事件,也跟第一个事件对应的是同一个连接,那这个条件就会成立;那么这种事件,属于过期事件,不该处理 - 上辈子和这辈子的连接区分:
存在这样的情况:
----a)处理第一个事件时,因为业务需要,我们把这个连接【假设套接字为50】关闭,同时设置c->fd = -1;并且调用ngx_free_connection将该连接归还给连接池;
----b)处理第二个事件,恰好第二个事件是建立新连接事件,调用ngx_get_connection从连接池中取出的连接(复用了就!instance)非常可能就是刚刚释放的第一个事件对应的连接池中的连接;
----c)又因为a中套接字50被释放了,所以会被操作系统拿来复用,复用给了b)【一般这么快就被复用也是醉了】;
----d)当处理第三个事件时,第三个事件其实是已经过期的,应该不处理,那怎么判断这第三个事件是过期的呢?
所以使用instance.
那为什么这样这个if(c->instance != instance)就可以判断呢。
c->instance:是此时此刻我们收到事件的(这辈子的)TCP连接结构体的instance的值,本身这个instance是存在c = (lpngx_connection_t)(m_events[i].data.ptr);地址空间中的,是一个连接的地址空间,在以上这种情况,连接是被复用了,是被清理过一次的,(喝过孟婆汤),在复用分配连接的时候c->instance = !instance;,是取过反的,所以是被标记了的。
instance:
//装
ev.data.ptr = (void *)( (uintptr_t)c | c->instance);//把对象弄进去,后续来事件时,用epoll_wait()后,
//这个对象能取出来用,但同时把一个 标志位【不是0就是1】弄进去
//取
c = (lpngx_connection_t)(m_events[i].data.ptr);//ngx_epoll_add_event()给进去的,这里能取出来,m_events[i].data.ptr
instance = (uintptr_t) c & 1; //将地址的最后一位取出来,用instance变量标识, 见ngx_epoll_add_event,该值是当时随着连接池中的连接一起给进来的
3.4 epoll的两种工作模式:LT和ET
LT: level trigged, 水平触发,这种工作模式 低速模式(效率差) -------------epoll缺省用此模式
ET: edge trigged, 边缘触发/边沿触发,这种工作模式 高速模式(效率好)
现状:所有的监听套接字用的都是 水平触发; 所有的接入进来的用户套接字都是 边缘触发
水平触发的意思: 来 一个事件,如果你不处理它,那么这个事件就会"一直"被触发;
边缘触发的意思: 只对非阻塞socket有用;来一个事件,内核只会通知你一次【不管你是否处理,内核都不再次通知你】;
边缘触发模式,提高系统运行效率,编码的难度加大;因为只通知一次,所以接到通知后,你"必须要保证把该处理的事情处理利索";
-------------------------------------------
"怎么收数据":
例,水平触发模式下,客户端给你发来的100字节,就会到达服务器的内核缓冲区,如果自己的写的代码中,调用epoll_wait,只从内核缓冲区拿出30字节,还有70字节没有去收,再次调用epoll_wait的时候,还能收到要我们去读数据的事件,读剩下的70个字节。而在边沿触发模式下,是收不到叫我们再去都剩下70字节的事件了。
-------------------------------------------
3.5 问题思考
问题:使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?
答案:
一种最普遍的方式:
需要向socket写数据的时候才把socket加入epoll【红黑树】,等待可写事件。接收到可写事件后,调用write或者send发送数据。当所有数据都写完后,把socket移出epoll。
这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。
一种改进的方式:
开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。

浙公网安备 33010602011771号