在高并发网络服务开发中,Reactor事件驱动思想是解决高并发IO问题的核心方法,配合Linux高性能IO多路复用epoll的ET模式,演进为主从Reactor多线程架构,构成了Muduo、大型网关、分布式服务的底层核心架构。今天来给大家讲讲这个主从Reactor架构+epoll-ET架构。
在Linux高并发网络编程领域,epoll是离不开的核心技术,主要是通过部分读部分写+epollET模式+阻塞非阻塞这三者来实现,先来详细讲讲这三者。
部分写部分读
read系统调用的流程为:进入内核态根据fd找到对应的文件对象,Linux读写文件几乎都走页缓存,不会直接碰磁盘,如果内核缓冲区有东西就直接用,没有东西才去磁盘读入内核缓冲区,接下来需要把数据从内核缓冲区拷贝到用户缓冲区,两块内存完全独立,一次能拷多少拷多少,这也是最需要注意的地方!read的部分读正是发生在内核缓冲区到用户buf这一步,原因是内核缓冲区和用户缓冲区大小限制,可能内核缓冲区数据不够读也可能内核缓冲区数据太多用户buf装不下。write也是一样。
epollET模式
epoll分为水平模式和边缘模式,水平模式LT简单来说就是缓冲区只要有数据就一直通知缓冲区可读或可写,边缘模式简单来说是在缓冲区状态变化的那一刻只通知一次。LT的时候就算出现部分写/部分读也没关系,下次还会通知,但是只要缓冲区状态满足内核就一直发事件,用户态/内核态切换频繁,开销大;ET由于内核只通知一次在高并发连接时比LT高效的多但是如果出现部分读没读完会出大问题!所以就引出了epollET模式的核心:循环读取!循环读取当read返回-1并且errno=EAGAIN/EWOULDBLOCK时说明内核缓冲区已经空了读完了。
阻塞+非阻塞
我们在实现高并发服务器时通常用epollET模式+非阻塞,接下来说说什么是阻塞什么是非阻塞:
阻塞就是当内核缓冲区为空的时候read不返回一直等待有数据可读了,非阻塞就是当读到内核缓冲区没有数据可读时直接返回-1不等待,阻塞和非阻塞的区别在哪?当内核缓冲区有数据的时候(不管数据够不够),行为完全一样,当内核缓冲区完全没有数据的时候二者非为等待和立刻返回。为什么说epollET模式必须搭配非阻塞fd使用?原因就在于我们要检查是否内核缓冲区已经空了,假如我们用阻塞fd,当内核缓冲区此刻为空read就不会返回了,ET+非阻塞=“卡死”,所以得用非阻塞fd,当检测到内核缓冲区为空的时候read返回-1并设置errno,只需要在while循环里检查errno并break就可以了。
最后为大家赋上一小段核心代码:
void set_nonblock(int fd) {//设置fd为非阻塞 int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); } while(1){ n = read(fd, buf, sizeof(buf)); if(n>0){ clients[fd].readbuf.append(buf, n); }else if(n==0){ close(fd); clients.erase(fd); del_epoll(fd, 0); return; }else { if(errno==EAGAIN||errno==EWOULDBLOCK){ break;//此刻内核缓冲区已经为空,读完了 }else{ close(fd); return; } } }现在开始讲Reator,很多开发者会混淆epoll和Reactor,epoll是系统调用,Reactor是一套事件驱动的设计思想。
Reactor核心思想
不轮询、不阻塞等待连接、由IO多路复用监听所有文件描述符,只在事件就绪的时候分发任务去处理。
核心载体是:epoll+ET,这是实现Reactor的关键工具,前面说的实现高并发服务器就是用Reactor思想。
主从Reactor架构
主线程也就是主Reactor只负责监听是否有客户端连接,只处理连接,不做读写业务处理,读写业务处理扔给子线程(子Reactor),主线程每处理一个新客户端连接事件后把客户端fd挂载到子线程的从Reactor,子线程有自己独立的epoll加事件循环,负责监听与处理已连接客户端fd的 可读 / 可写 / 异常事件。
整套服务器监听处理逻辑:
主线程epoll只注册listen_fd的ET读事件
当监听到有新客户端连接时,触发listen_fd的读事件
主线程拿到listen_fd设置为非阻塞
将新客户端fd挂载到某一个子线程
子线程内部epoll注册这个客户端fd,监听读事件
后续这个客户端的读写事件都由这个子Reactor处理