趁着早起, 接着昨天
epoll
epoll
应该是代表单进程的极端表现,最大程度的发挥一个核的最大实力,但是对多核来说就有些无法触及,但是在此处我们可以考虑将 epoll
扩展出去。 epoll
的作用是监听已被注册到自身的那些文件描述符的各种事件(可读,可写等等)。我们可以考虑让 监听套接字 独享一个 epoll
(连接epoll),并且在其之下(逻辑中的下,实际上没有直接连接接触)用多线程/多进程建立几个处理新连接事物的专用 epoll
(事务epoll)。就是这么简单的思路。
针对上面的问题,可以参考 分布式系统 的设计过程中,有一种叫做 一致性哈希 的设计思想,也就是不要让 事务epoll 相互竞争,而是让 连接epoll 自己将新的到的连接,分发给这些固定数量的 事务epoll 中的某一个,并且应该形成 均衡发布 。
epoll
对于 epoll
而言, 连接epoll 所处理的事情十分简单,就是负责整个网络程序服务端工作中的第四个部分 accept
,只需要对监听套接字的 可读事件(EPOLLIN) 敏感就行,这样就讲现成的撰写难度降低。而对于 事务epoll 而言,事情会稍微复杂一些。
epoll
中,我们需要处理的就是 三种事件 : ( 可读 , 可写 , 错误 ),这里很多刚接触的人(包括我),都会将可读和可写放在一起处理,其实这是不怎么好的方法,试想这种情况: write
调用返回 -1
且 errno == EAGAIN
,由于你将读写放在一个事件里(读事件),所以你没办法在这种错误发生时有补救措施。要么你一直循环重试 write
知道其成功发送(或者对方突然关闭连接,返回 0
),这就会导致那个线程所在的CPU核使用率居高不下,都浪费在这里了。要么你就只能关闭连接,让 peer端 去负担这个后果( 这个由服务器端失策造成的后果! )。 连接epoll
获得新连接时,将其用 可读事件 注册到 事务epoll 中,一旦 事务epoll 被可读事件激活,就处理这个可读事件,并将需要发送给 peer端 的数据准备好,放在每个连接自己的缓冲区内,将这个连接重新用 可写事件 注册回自身。 epoll
有两种模式, LT 和 ET , 这两种的区别网上详细讲解的很多,不在赘述,我在这个软件中采用的是 ET 模式,且使用了 EPOLLONESHOT
选项,这个选项在我的设计方式中,实际上是没有什么必要(目前看来)
EPOLLONESHOT
最开始是为了防止使用 线程池技术 时候,对防止 对新连接的竞争 时的措施,也就是说,假设A线程在处理某个新连接(A连接)的某个事件(A事件)时,突然A连接的A事件又被触发了(这是可能的,例如读事件,突然又有新数据到来),那么B线程可能就接到了这个事件,也开始处理,这就产生了冲突,会导致垃圾数据的产生。 EPOLLONESHOT
的意义就在于,每次这个连接被处理了,那么就自动从这个epoll中除名,下次想用这个epoll监视这个连接,就需要重新注册(epoll_clt)。 epoll
而言,这个属性似乎没有什么必要,留下它是因为它并没有造成额外的工作,而且可以让后续的想法更流畅的实现,万一有新想法了呢:) 前提所有的 文件描述符 都是非阻塞的。
accept
由于 accept
是在 连接epoll 的 epoll_wait
成功时,才会调用,所以我们需要对这个 accept
一直循环,直到其返回`-1
while (is_work > 0) { /* New Connect */ sock = accept(new_client.data.fd, NULL, NULL); if (sock > 0) { fprintf(stderr, "There has a client(%d) Connected/n", sock); set_nonblock(sock); ... } else /* sock == -1 means nothing to accept */ break; }
之所以需要一直循环,是因为不一定只有一个新连接接上来。
read
read
函数返回值大于 0
,表明正确读到数据,继续循环读 read
函数返回值小于 0
,(1)且 errno == EAGAIN || errno == EWOULDBLOCK
代表缓冲区无数据可读了,注册写事件,(2)你需要关闭这个连接了 如果 read
函数返回值等于 0
,表明你需要关闭这个连接了。这代表 peer 端发了一个 FIN 给你。
while (1) { read_number = read(fd, buf+buf_index, BUF_SIZE-buf_index); if (0 == read_number) { /* We must close connection */ return READ_FAIL; } else if (-1 == read_number) { /* Nothing to read */ if (EAGAIN == errno || EWOULDBLOCK == errno) { buf[buf_index] = '/0'; return READ_SUCCESS; } return READ_FAIL; } else { /* Read Success */ ... } }
EAGAIN 和 EWOULDBLOCK 值实际上是一样的
write
write
函数返回值大于 0
,表明正确的写了数据,继续循环写 write
函数返回值小于 0
,(1)且 errno == EAGAIN
代表写缓冲满了,重新注册写事件,(2)且 errno == EPIPE
,表明你需要关闭这个连接了,这代表 peer 端 close
这个连接。(3) 表明你需要关闭这个连接了 如果 write
函数返回值等于 0
,这种情况应该不会发生,在系统层面来说这应该是不合法的。
while (nbyte > 0) { buf += count; count = write(fd, buf, 8192); if (count < 0) { if (EAGAIN == errno || EWOULDBLOCK == errno) { memcpy(client->write_buf, buf, strlen(buf)); client->write_offset = nbyte; return HANDLE_WRITE_AGAIN; } if (EPIPE == errno) return HANDLE_WRITE_FAILURE; } else if (0 == count) return HANDLE_WRITE_FAILURE; nbyte -= count; }
EPIPE 会和一个信号 SIGPIPE 一起出现,你需要(必须)处理它,至少在它发生前处理它,不然你的程序就会被中断,最简单的处理方式就是 忽略它 。
EINTR 这个 errno
值,在非阻塞的套接字中不需要太过关注,但是如果是阻塞型套接字编程,那就是一个十分重要的值,需要特别关注
gdb
很容易就定位出来了,还不懂怎么用的,可以参考我上一篇文章如何简洁地使用gdb。 GET
方法实现的时候,需要传递的参数很多,应该将这些信息包含进客户端连接的结构体中,而不是临时用变量存储,传递。 就算没有崩溃,每崩掉一个线程( 事务epoll 所在线程)整个服务器的性能将下降 20% , 如果 连接epoll 所在的线程崩溃,整个程序也就结束了。
具体项目源码 : httpd3 ·
可以读一读关于 Linux 环境中,线程和进程的区别和联系,其实两者十分相似(不止体现在功能上)
epoll
实例,且都注册了同一个 监听套接字 ,这样不就也达到了同样的并发目的。 4.5
之前没有系统提供的解决方案,而距离主流内核提升到 4.5
还有漫漫长路要走。至于惊群现象这里不给出赘述,网上的解释很多,简单来说就是一个新连接到来会唤醒所有进程中的 epoll_wait
,但只有一个 epoll_wait
会成功返回。 2) 负载不均衡 , 因为每次被 成功 唤醒的进程都不确定,完全是操作系统这个二愣子出的主意,所以有可能(很有可能,到最后会接近99%)会出现一个进程忙死了,有的进程闲死(专业一点叫做 饥饿现象 )。 nginx
的),就是用锁来解决,大概的意思就是每个进程持有自旋锁( 自己实现的 ),这个自旋锁的设计很巧妙,是有时间限制的自旋锁,且时间可自行调整,通过调整这个时间的值,来达到负载均衡的效果,即本次没有得到新连接的进程,下次锁的时间就减少,这样获得新连接的概率就增大,同时也解决了惊群现象。 惊群现象在内核 3.9
的时候,被提出解决,解决的方案是 EPOLLEXCLUSIVE
这个Event,而在最近发布的 Linux内核4.5 中被正式的修复(方案就是前面这个)。 其实在这之前还有一个系统调用会导致惊群,那就是 accept
,只不过被修复了,忘了是内核多少( 2.4
or 2.6
)。
epoll
实例,用于注册 监听套接字 , accept
新连接,并将新连接 均衡 的分给,处于逻辑下层的各个线程中的 epoll
实例。 epoll
在逻辑上层接待新连接,要是它崩溃了,那整个程序就完了。所以就健壮性而言,不如多进程的方案。而且要是任意一个线程因为某些原因死掉了,且不说程序是否能够运行的下去,就算程序能够苟活,整个服务器的性能一定会打一个折扣。相比之下,同种情况发生在多进程方案身上最多就是损失点性能,对整个服务器的运行而言,不会造成太大的波动。 所以在我的实现中,处于上层逻辑中的 epoll
实例,也被我写成了一个数组类型,只不过初始化大小为 1
,也就是暂时只有一个,我的想法是后期可以通过配置文件中添加新选项来进行更改。
nginx
,我也很认真的看了它的一些(头疼,战斗民族的代码,但是比德国佬的 libuv
好太多了…)实现源码。