2026/5/21 17:34:02
网站建设
项目流程
做图片推广的网站吗,泰安网络公司平台,医院建网站,成都网站开发收费1. poll
poll也是一种多路转接的方案#xff0c;它专门用来解决select的两个问题#xff1a;
等待fd有上限的问题。每次调用都需要重新设置fd_set的问题。
1.1 poll函数
man poll#xff1a; ①struct pollfd* fds#xff1a;用来设置需要等待的fd以及事件 如上图所示它专门用来解决select的两个问题等待fd有上限的问题。每次调用都需要重新设置fd_set的问题。1.1 poll函数man poll①struct pollfd* fds用来设置需要等待的fd以及事件如上图所示struct pollfd结构体中存在三个成员变量第一个是fd表示需要操作系统等待的文件描述符。第二个是short events表示需要操作系统等待该fd的事件类型。第三个是short revents操作系统告诉用户层该fd的哪个事件就绪了。此时的文件描述符fd直接设置到struct pollfd结构中即可需要设置哪个就设置哪个不用再去寻找对应的位图。告诉操作系统需要等待的事件时只需要直接设置short events即可不用将不同的事件类型放在不同的位图中。当指定文件描述符fd的就绪时操作系统会设置对应short revents用户层直接读取fds中的这个字段便可知道是哪个事件就绪了。struct pollfd结构体将用户和操作系统设置的字段分开了所以就不存在相互干扰的问题。events和revents的取值如上图所示便是用户层以及操作系统可以设置的事件类型这些同样是一些宏定义常用的就是POLLIN数据可读以及POLLOUT数据可写。假设fds结构体中events的值是POLLIN此时操作系统就关注指定文件描述符的读事件是否就绪如果就绪就将revents的值也设置成POLLIN用户层读取到该值后就知道文件可读了。②nfds_t nfds需要poll等待的文件描述符fd的个数。在内核中nfds_t类型本质上是一个unsigned long int类型也是一个整形。第二个参数nfds就是用来设定需要poll等待文件描述符的个数的。用户层和操作系统同时维护一个元素为struct pollfd类型的数组这个数组中有多少个元素用户层需要让操作系统等待的文件描述符就有多少个变量nfds就表示数组的大小。这个数组就类似用户层和操作系统之间的“临界资源”双方都能看到而且都可以访问由于访问的位置不同所以不会出现干扰。由于nfds的值是由用户层设定的所以poll可同时等待的文件描述符数量并没有上限unsigned long int的最大值非常大远大于一个系统能打开的文件个数所以可以理解为没有上限。③int timeout阻塞等待的时间和select中的struct timeval变量的作用类似但是这里的timeout是一个int类型的变量它的单位是1ms。并且它不是一个输入输出型参数只需要定义一次即可。timeout0表示在timout时间以内阻塞等待超出这个时间就超时返回如该值是1000就表示阻塞等待1s。timeout 0表示非阻塞等待。timeout 0表示阻塞等待。返回值就绪事件的个数。和select的返回值意义一样。1.2 简易poll服务器这里在上一篇select代码下直接改直接放代码MakefilepollServer:main.cc g -o $ $^ -stdc11 .PHONY:clean clean: rm -f pollServerpollServer.hpp#ifndef __POLL_SVR_H__ #define __POLL_SVR_H__ #include iostream #include sys/select.h #include poll.h #include Log.hpp #include Sock.hpp #define FD_NONE -1 using namespace std; // select 我们只完成读取写入和异常不做处理 -- epoll(写完整) class PollServer { public: static const int nfds 100; public: PollServer(const uint16_t port 8080) : _port(port), _nfds(nfds) { _listensock Sock::Socket(); Sock::Bind(_listensock, _port); Sock::Listen(_listensock); logMessage(DEBUG, %s, create base socket success); _fds new struct pollfd[_nfds]; for (int i 0; i _nfds; i) { _fds[i].fd FD_NONE; _fds[i].events _fds[i].revents 0; } _fds[0].fd _listensock; _fds[0].events POLLIN; _timeout 1000; } void Start() { while (true) { int n poll(_fds, _nfds, _timeout); switch (n) { case 0: logMessage(DEBUG, %s, time out...); break; case -1: logMessage(WARNING, select error: %d : %s, errno, strerror(errno)); break; default: // 成功的 HandlerEvent(); break; } } } ~PollServer() { if (_listensock 0) close(_listensock); if (_fds) delete[] _fds; } private: void HandlerEvent() // fd_set 是一个集合里面可能会存在多个sock { for (int i 0; i _nfds; i) { if (_fds[i].fd FD_NONE) // 1. 去掉不合法的fd continue; // 2. 合法的fd不一定就绪了 if (_fds[i].revents POLLIN) // 如果fd读事件就绪 { if(_fds[i].fd _listensock) // 读事件就绪连接事件到来accept { Accepter(); } else // 读事件就绪INPUT事件到来read / recv { Recver(i); } } } } void Accepter() { string clientip; uint16_t clientport 0; // listensock上面的读事件就绪了表示可以读取了获取新连接了 int sock Sock::Accept(_listensock, clientip, clientport); // 这里进行accept不会阻塞 if (sock 0) { logMessage(WARNING, accept error); return; } logMessage(DEBUG, get a new line success : [%s:%d] : %d, clientip.c_str(), clientport, sock); int pos 1; // 规定了 _fd_array[0] _listensock; 不用管 for (; pos _nfds; pos) // 将fd放入到数组中 - 找一个合法的位置 { if (_fds[pos].fd FD_NONE) break; } if (pos _nfds) // 可以对struct pollfd进行自动扩容,或者直接改数组大小,这里不处理 { logMessage(WARNING, %s:%d, poll server already full, close: %d, sock); close(sock); } else // 找到了合法的位置 { _fds[pos].fd sock; _fds[pos].events POLLIN; } } void Recver(int pos) { // 读事件就绪INPUT事件到来、recvread logMessage(DEBUG, message in, get IO event: %d, _fds[pos]); // 此时poll已经帮我们进行了事件检测fd上的数据一定是就绪的即 本次 不会被阻塞 // 怎么保证以读到了一个完整的报文呢 - 模拟实现epoll的时候再考虑 char buffer[1024]; int n recv(_fds[pos].fd, buffer, sizeof(buffer) - 1, 0); if (n 0) // 正常读取 { buffer[n] 0; logMessage(DEBUG, client[%d]# %s, _fds[pos].fd, buffer); } else if (n 0) // 对端把链接关了 { logMessage(DEBUG, client[%d] quit, me too..., _fds[pos]); close(_fds[pos].fd); // 关闭不需要的fd _fds[pos].fd FD_NONE; // 不要让poll帮我关心当前的fd了 _fds[pos].events 0; } else // 读取错误 { logMessage(WARNING, %d sock recv error, %d : %s, _fds[pos].fd, errno, strerror(errno)); close(_fds[pos].fd); // 关闭不需要的fd _fds[pos].fd FD_NONE; // 不要让poll帮我关心当前的fd了 _fds[pos].events 0; } } void DebugPrint() // 打印一下数组里合法的fd { cout _fd_array[]: ; for (int i 0; i _nfds; i) { if (_fds[i].fd FD_NONE) continue; cout _fds[i].fd ; } cout endl; } private: uint16_t _port; int _listensock; struct pollfd *_fds; int _nfds; int _timeout; }; #endif如上图所示使用telnet连接服务端后现象和selsect一样也是一个服务端进程可以同时和多个客户端进行通信。1.3 poll的优缺点优点和select一样效率高还有应用场景是有大量的链接只有少量是活跃的省资源。这是所有多路转接都具备的优点。struct pollfd结构包含了要监视的event和发生的revent不再使用select“参数-值”传递的方式接口使用比select更方便。poll并没有最大等待文件描述符数量限制 但是数量过大后性能也是会下降。缺点和select一样poll返回后需要轮询struct pollfd数组来获取就绪的描述符。每次调用poll都需要把大量的struct pollfd结构从用户层拷贝到内核中。同时连接的大量客户端在一时刻可能只有很少的处于就绪状态因此随着监视的描述符数量的增长, 其效率也会线性下降。代码的编写也比较复杂比select简单2. epollepoll是基于poll的基础上改进的e是expand增强/拓展的意思增强版的poll但实际比poll厉害得多它不仅克服了select的缺点而且解决了poll遍历成本是效率最高的多路转接模式但是它也是最复杂的一种模式。2.1 epoll的相关函数man epoll_create如上图所示的epoll_create系统调用是用来创建epoll句柄的。epoll是一个模型这个模型包含多个数据结构句柄可以理解为是这个模型标志通过句柄可以找到这个模型并且使用它。int size自Linux2.6.8以后该参数是被忽略的不起实际作用但是必须是大于0的一个值。返回值返回的也是一个文件描述符fd。epoll句柄在内核中也是一个结构体类似于struct file而Linux下一切皆文件所以返回的也是一个文件描述符拿着这个文件描述符可以访问到这个epoll句柄用完之后, 必须调用close()关闭。man epoll_ctl如上图所示的epoll_ctl系统调用是用来修改创建的epoll句柄属性的。四个参数int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epfd该值就是epoll_create的返回值用来指示哪个epoll句柄。int op是修改句柄属性的选项有增删改三个选项EPOLL_CTL_ADD向句柄中增加要等待的文件描述符。EPOLL_CTL_MOD修改句柄中指定的文件描述符。EPOLL_CTL_DEL从句柄中删除指定的文件描述符。int fd要进行操作的文件描述符。struct epoll_event* event用来指定要等待的事件。如上图所示便是内核中struct epoll_event结构体的定义它有两个成员变量。第二个成员变量是一个联合体epoll_data_t data可以看到有四个成员共用这个联合体后面会讲解它每个变量的作用。第一个成员变量是uint32_t events用来设置需要等待的事件其值也是有几个宏组成的集合值意义EPOLLIN表示对应的文件描述符可以读 包括对端SOCKET正常关闭EPOLLOUT表示对应的文件描述符可以写EPOLLPRI表示对应的文件描述符有紧急的数据可读 这里应该表示有带外数据到来EPOLLERR表示对应的文件描述符发生错误EPOLLHUP表示对应的文件描述符被挂断EPOLLET将EPOLL设为边缘触发Edge Triggered模式, 这是相对于水平触发Level Triggered来说的EPOLLONESHOT只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里返回值调用成功返回0调用失败返回-1并且设置相应的错误码。man epoll_wait如上图所示的epoll_wait系统调用是用来从操作系统中获取被等待文件描述符的状态的。int epfd该值就是epoll_create的返回值用来指示哪个epoll句柄。struct epoll_event* events是一个结构体数组操作系统将就绪的文件描述符放入这个数组中供用户层读取。int maxevents该值就是events数组的大小是用户层用来告诉内核这个数组有多大的这个值不能大于epoll_create时的size。int timeout超时时间 毫秒0会立即返回-1是永久阻塞和poll中是一样的。返回值 也是和poll的返回值以及select代表的意义一样大于0表示就绪的文件描述符个数等于0表示超时返回小于0表示调用失败。以上三个系统调用是epoll模型的核心调用epoll_ctl是用户层用来告诉内核自己的需求的epoll_wait是内核用来告诉用户层哪些文件描述符的什么事件就绪的。现在知道了接口的使用但是仍然并不清除为什么epoll模型能够解决poll和select存在的问题所以下面了解下epoll模型的底层原理。2.2 epoll的原理网络通信过程中接收端将数据从网卡硬件层开始逐层向上交付最后给到应用层那么接收端是如何知道网卡上有数据到来的也就是操作系统是怎么感知到数据来了呢将计算机体系结构冯诺依曼体系结构以及中断向量表放在了一起来看当网卡接收到数据后输入外设网卡会自己产生一个控制信号直接给CPU中的控制器表示此时网卡中有数据到来可以读了。冯诺依曼体系中外设的数据信号不能直接和CPU传递如上图中红色线必须经过存储器。外设的控制信号可以直接传递给CPU的控制器如上图黑色线。外设给CPU发送一个信号表示数据到来这叫做中断事件发生。CPU根据中断信号的编号去操作系统维护的中断向量表中找到对应的中断服务函数并且执行。中断服务函数中会调用网卡接收数据的驱动程序将数据读取并且向上层交付如上图绿色线。在这里要重点关注中断服务函数从网卡中接收数据是从它开始的。epoll模型理论图包含计算机体系结构中的驱动层操作系统系统调用三层在调用epoll_create创建模型后会返回一个文件描述符fd这个fd同样放在服务器进程PCB所维护的进程描述符表中通过fd这个句柄就可以找到对应的epoll模型。epoll模型同样是一个大的结构体只是这个结构体更加复杂Linux下一切皆文件在Linux眼中都是struct file所以创建模型后返回的也是一个文件描述符。上图中操作系统中黑色框内的部分就是epoll模型包含一个红黑树和一个就绪队列。每一个epoll模型都有一个独立的eventpoll结构体用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中如此重复添加的事件就可以通过红黑树而高效的识别出来红黑树的插入时间效率是logN)其中N为树的高度。而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系也就是说当响应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback它会将发生的事件添加到rdlist双链表中。在epoll中对于每一个事件都会建立一个epitem结构体struct epitem { struct rb_node rbn; // 红黑树节点 struct list_head rdllink; // 双向链表节点 struct epoll_filefd ffd; // 事件句柄信息 struct eventpoll *ep; // 指向其所属的eventpoll对象 struct epoll_event event; // 期待发生的事件类型 }当调用epoll_wait检查是否有事件发生时只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空则把发生的事件复制到用户态同时将事件数量返回给用户这个操作的时间复杂度是O(1)。epoll的使用过程就是三部曲:调用epoll_create创建一个epoll句柄调用epoll_ctl将要监控的文件描述符进行注册调用epoll_wait等待文件描述符就绪以增加需要操作系统等待的文件描述符为例调用epoll_ctl将fd以及需要等待的事件构建成struct epoll_event变量插入到红黑树中操作系统会遍历红黑树中所有节点。红黑树节点中包含很多成员变量如上图左下角所示这其中必然有文件描述符fd需要等待的事件event左右字节的指针还包括next和prev指针。如果是删除或者修改等操作同样是在修改这颗红黑树而红黑树查找效率非常高所以对应的操作也会很高效。当操作系统发现红黑树中有节点的事件就绪后就会将该节点放入到就绪队列中就绪队列是一个双向循环链表。将节点从红黑树放入到就绪队列中并没有发生拷贝关键就在next和prev指针上。当网卡中有数据到来时通过中断函数最终调用了网卡驱动程序在驱动程序中有一个回调函数void* private_data这是由操作系统提供的。private_data回调函数会将红黑树节点中的next和prev指针的指向关系做对应的修改让该节点链入到就绪队列中去。红黑树的一个节点它不只属于红黑树还可能属于就绪队列。红黑树中的节点和就绪队列中的节点地址可能是一样的。画的是逻辑图所以将就绪队列和红黑树分开了。就绪队列中必然也包括就绪文件的文件描述符以及就绪的事件如上图所示的struct epoll_event结构。所以凡是处于就绪队列中的节点必然已经就绪。用户层在调用epoll_wait后获取的就是内核中就绪队列中的内容所以获取到的全部都是就绪的事件所以用户层的struct epoll_event类型数组中全部都是就绪的事件。epoll_wait将所有就绪的事件按照顺序放入到用户层传入的数组中。此时从内核到用户层虽然也需要遍历但是此时是遍历拷贝而不需要遍历检测所以时间复杂度相当于从之前的O(N)变成了O(1)效率大提高。2.3 简易epoll服务器先写个大概Log.hpp和Sock.hpp和前一篇一样就不放了封装一下epoll的三个接口Epoll.hpp#pragma once #include iostream #include sys/epoll.h #include unistd.h class Epoll { public: static const int gsize 256; public: static int CreateEpoll() { int epfd epoll_create(gsize); if (epfd 0) return epfd; exit(5); } static bool CtlEpoll(int epfd, int op, int fd, uint32_t events) { struct epoll_event ev; ev.events events; ev.data.fd fd; // 先用第三个参数联合体的fd int n epoll_ctl(epfd, op, fd, ev); return n 0; } static int WaitEpoll(int epfd, struct epoll_event revs[], int revs_num, int timeout) { return epoll_wait(epfd, revs, revs_num, timeout); } };main.cc#include EpollServer.hpp #include memory using namespace std; using namespace ns_epoll; void change(std::string request) { // 完成业务逻辑 std::cout change : request std::endl; } int main() { unique_ptrEpollServer epoll_server(new EpollServer(change)); epoll_server-Start(); return 0; }EpollServer.hpp现在可以验证一下timeout后面逐步把三个函数加上去就是完整的EpollServer.hpp了_HandlerRequest和function还有main.cc的change函数在写Recver函数才用到就是客户端告诉服务端接收到IO事件要怎么处理这个事件#ifndef __EPOLL_SERVER_HPP__ #define __EPOLL_SERVER_HPP__ #include iostream #include string #include functional #include cassert #include Log.hpp #include Sock.hpp #include Epoll.hpp namespace ns_epoll { const static int default_port 8080; const static int gnum 64; class EpollServer // 这里先只处理读取 { using func_t std::functionvoid(std::string); public: EpollServer(func_t HandlerRequest, const int port default_port) : _port(port), _revs_num(gnum) { _revs new struct epoll_event[_revs_num]; // 1. 申请对应的空间 _listensock Sock::Socket(); // 2. 创建listensock Sock::Bind(_listensock, _port); Sock::Listen(_listensock); _epfd Epoll::CreateEpoll(); // 3. 创建epoll模型 logMessage(DEBUG, init success, listensock: %d, epfd: %d, _listensock, _epfd); // 4. 将listensock先添加到epoll中让epoll管理起来 if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN)) exit(6); logMessage(DEBUG, add listensock to epoll success.); } void Accepter(int listensock) {} void Recver(int sock) {] void HandlerEvents(int n) {} void LoopOnce(int timeout) { int n Epoll::WaitEpoll(_epfd, _revs, _revs_num, timeout); // 封装意义不大还是和上面一样封装了 // 细节1如果底层就绪的sock非常多revs承装不下的话 - 一次拿不完就下一次再拿 // 担心拿不完的话 - if(n _revs_num) // 可以扩容这里不处理 // 细节2关于epoll_wait的返回值问题有几个fd上的事件就绪就返回几, // epoll返回的时候会将所有就绪的event按照顺序放入到revs数组中, 一共有返回值个-用来遍历就绪队列 switch (n) { case 0: logMessage(DEBUG, timeout...); // 3, 4 break; case -1: logMessage(WARNING, epoll wait error: %s, strerror(errno)); break; default: // 等待成功 logMessage(DEBUG, get a event); sleep(1); HandlerEvents(n); break; } } void Start() { int timeout -1; // -1是阻塞,0是非阻塞,1000是每隔1秒... while (true) { LoopOnce(timeout); } } ~EpollServer() { if (_listensock 0) close(_listensock); if (_epfd 0) close(_epfd); if (_revs) delete[] _revs; } private: int _listensock; int _epfd; uint16_t _port; struct epoll_event *_revs; // 就绪的事件 int _revs_num; func_t _HandlerRequest; }; } #endif编译运行和预期一样得到3和4文件描述符监听套接字的文件描述符是3句柄的值是4成功获取事件没处理就一直打印了下面写一下HandlerEventsvoid Accepter(int listensock) { std::string clientip; uint16_t clientport; int sock Sock::Accept(listensock, clientip, clientport); if (sock 0) { logMessage(WARNING, accept error); return; } // 不能直接读取因为并不清楚底层是否有数据 if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN)) // 将新的sock添加给epoll return; logMessage(DEBUG, add new sock : %d to epoll success, sock); } void Recver(int sock) { } void HandlerEvents(int n) { assert(n 0); for (int i 0; i n; i) { uint32_t revents _revs[i].events; int sock _revs[i].data.fd; if (revents EPOLLIN) // 读事件就绪 { if (sock _listensock) { Accepter(_listensock); // 1. listensock 就绪 } else { Recver(sock); // 2. 一般sock 就绪 - read } } if (revents EPOLLOUT) { // 这里不处理 } } }测验一下和预期一样得到5和6文件描述符下面写Recvervoid Recver(int sock) { char buffer[10240]; // 1. 读取数据 ssize_t n recv(sock, buffer, sizeof(buffer) - 1, 0); if (n 0) // 假设这里就是读到了一个完整的报文 { buffer[n] 0; _HandlerRequest(buffer); // 2. 用服务端传进来的方法处理数据 } else if (n 0) { bool res Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0); // 1. 先在epoll中去掉对sock的关心 assert(res); (void)res; close(sock); // 2. 再close文件 logMessage(NORMAL, client %d quit, me too..., sock); } else { bool res Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0); // 1. 先在epoll中去掉对sock的关心 assert(res); (void)res; close(sock); // 2. 再close文件 logMessage(NORMAL, client recv %d error, close error sock, sock); } }成功调用客户端的函数后面还会改进这个epoll。2.4 epoll的优点epoll的优点和select的缺点对应接口使用方便虽然拆分成了三个函数但是反而使用起来更方便高效不需要每次循环都设置关注的文件描述符也做到了输入输出参数分离开。数据拷贝轻量只在合适的时候调用epoll_ctl将文件描述符结构拷贝到内核中这个操作并不频繁(而select/poll是每次循环都要进行拷贝)。事件回调机制避免使用遍历检测而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中。epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪这个操作时间复杂度是O(1)即使文件描述符数目很多, 效率也不会受到影响。没有数量限制文件描述符数目无上限。虽然epoll的机制更复杂但是它用起来更方便也更高效。2.5 水平触发和边缘触发epoll主要解决的是多路转接中进行IO的时候等的这一环节当操作系统所监管的事件就绪了就会通知用户层来处理事件这个通知有两种方式水平触发(Level Triggered)工作模式简称LT。边缘触发(Edge Triggered)工作模式简称ET。来举一个生活中的例子假设你正在打英雄联盟正要打团的时候你妈喊你吃饭此时就存在两种方式如果喊你一次你没动那么就会继续喊第二次第三次…直到你去吃饭这种方式就是水平触发。如果喊你一次你没动之后就不再喊你了这种方式就是边沿触发。放在多路转接中就是事件就绪时操作系统通知用户层后用户层没有读取数据或者没有读取完毕如果操作系统继续通知就是LT模式如果没有继续通知就是ET模式。epoll默认状态下就是LT工作模式。LT模式下事件未被用户层处理完毕每调用一次epoll_wait就会返回一个大于0的值。ET模式下事件未被用户处理完毕只有第一次调用epoll_wait才会返回大于0的值之后不再返回并且将事件设置为未就绪状态除非该套接字中数据增加才会再返回一次大于0的值。在调用epoll_ctl的时候将struct epoll_event中的uint32_t events字段设置成EPOLLET此时该文件描述符就变成了ET模式并没有设置LT模式的方法因为默认就是LT模式。使用ET模式能够减少epoll触发的次数但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完如果不处理完剩下的数据就有可能被覆盖后果由程序员自己承担。相当于一个文件描述符就绪之后不会反复被提示就绪所以就比 LT 更高效一些。假如有这样一个例子:我们已经把一个tcp socket添加到epoll描述符这个时候socket的另一端被写入了2KB的数据调用epoll_wait并且它会返回说明它已经准备好读取操作然后调用read只读取了1KB的数据继续调用epoll_wait......水平触发Level Triggered 工作模式当epoll检测到socket上事件就绪的时候可以不立刻进行处理或者只处理一部分。如上面的例子由于只读了1K数据缓冲区中还剩1K数据在第二次调用 epoll_wait 时epoll_wait仍然会立刻返回并通知socket读事件就绪直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回LT模式支持阻塞读写和非阻塞读写。边缘触发Edge Triggered工作模式如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志epoll进入ET工作模式。当epoll检测到socket上事件就绪时必须立刻处理。如上面的例子虽然只读了1K的数据缓冲区还剩1K的数据在第二次调用 epoll_wait 的时候epoll_wait 不会再返回了。也就是说ET模式下文件描述符上的事件就绪后只有一次处理机会。ET模式的性能比LT模式性能更高epoll_wait 返回的次数少了很多Nginx默认采用ET模式使用epoll。ET模式只支持非阻塞的读写。ET模式的高效是建立在程序员的痛苦之上的由于它只通知用户层一次如果不一次处理完数据就没机会再处理了但是用户层是怎么知道数据有没有读取完毕呢答案是循环读取直到读不到数据了就证明读完了。如上图所示此时就存在一个问题客户端发送了10K的数据给服务端服务端收到了epoll的通知后用户层调用recv进行读取但是一次没有读取完毕只读取了1K的数据。由于此时epoll是ET模式所以操作系统认为事件已经被处理了就又将读事件设置成了未就绪的状态再次读取时recv就会阻塞不动整个进程就阻塞了如下面伪代码while(1) { int ret recv(sock,buffer,sizeof(buffer)-1,0); // 第二次读取就会阻塞 }由于epoll_wait不会再次返回剩下的9K数据会一直在缓冲区中直到下一次客户端再给服务器写数据操作系统再次将读事件设置成就绪状态才能再次recv。服务端只有将10k数据完全读取完才会给客户端一个确认应答。客户端收到服务端的确认应答后才会发送下一个请求。客户端发送下一个请求epoll_wait才会返回才会将读事件设置未继续服务端才能再次去缓冲区中读取。服务端无法读取剩余的数据也就不会发出响应客户端无法收到响应也就不会再次发送请求服务端无法收到再次的请求就无法再次读取缓冲区中剩余的数据。时间一长就会触发TCP的超时重传机制导致数据被覆盖甚至丢失等问题。为了解决ET模式这个问题文件描述符对应的缓冲区必须设置成非阻塞 IO方式使用fcntl设置。 只有非阻塞方式才能用轮询的方式不断读取缓冲区中的数据直到读取完毕。如果是LT模式就不用设置成非阻塞模式因为数据没有读取完毕epoll_wait会持续返回而事件也被保持就绪状态recv就可以持续读取数据直到将数据读取完毕。select和poll是采用LT模式的和epoll的默认方式一样那么如果将文件描述符设置成非阻塞方式仍然使用LT模式不是更方便吗既能循环读取又能让epoll持续返回也能提高效率啊为什么仍然要多此一举设计一个ET模式呢ET模式的高效不仅仅体现在通知机制上减少通知次数降低系统调用的开销。ET模式的高效还体现在增加底层网络的吞吐量上。ET模式表面上看是在强迫程序员将本轮就绪的数据全部读走深入网络底层TCP协议去看服务端由于一次将数据全部读走了从而能给客户端应答一个更大的窗口值。客户端就能更新出一个更大的滑动窗口增加一次发送的数据量从而提高底层数据发送的效率更好的利用诸如TCP延迟应答等策略提高整个网络通信的吞吐量。所以说ET模式在压榨程序员的基础上提高了整个网络通信的效率。epoll不仅解决了poll方式的问题而且还带来了其他优势比如使用简单遍历成本低等优势以及ET模式对于通信效率的提升虽然epoll的机制更复杂但是它带来了更好的效果利远大于弊。epoll的高性能是有一定的特定场景的如果场景选择的不适宜epoll的性能可能适得其反。比如对于多连接且多连接中只有一部分连接比较活跃时比较适合使用epoll。还有一个需要处理上万个客户端的服务器例如各种互联网APP的入口服务器这样的服务器就很适合epoll。如果只是系统内部服务器和服务器之间进行通信只有少数的几个连接这种情况下用epoll就并不合适具体要根据需求和场景特点来决定使用哪种模型。本篇完。下一篇网络和Linux网络_15IO多路转接reactor编程_服务器