网站制作排名优化,广州最近传染病情况,做暖暖欧美网站,南安市住房和城乡建设部网站在linux的网络编程中#xff0c;很长的时间都在使用select来做事件触发。在linux新的内核中#xff0c;有了一种替换它的机制#xff0c;就是epoll。相比于select#xff0c;epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中#xf…在linux的网络编程中很长的时间都在使用select来做事件触发。在linux新的内核中有了一种替换它的机制就是epoll。相比于selectepoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中它是采用轮询来处理的轮询的fd数目越多自然耗时越多。并且linux/posix_types.h头文件有这样的声明 #define __FD_SETSIZE 1024 表示select最多同时监听1024个fd当然可以通过修改头文件再重编译内核来扩大这个数目但这似乎并不治本。 /*epoll机制的简介*/ epoll的接口非常简单一共就三个函数 1.创建epoll句柄 int epfd epoll_create(int size); 创建一个epoll的句柄size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数给出最大监听的fd1的值。需要注意的是当创建好epoll句柄后它就是会占用一个fd值在linux下如果查看/proc/进程id/fd/是能够看到这个fd的所以在使用完epoll后必须调用close()关闭否则可能导致fd被耗尽。 2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epoll的事件注册函数它不同与select()是在监听事件时告诉内核要监听什么类型的事件而是在这里先注册要监听的事件类型。 第一个参数是epoll_create()的返回值第二个参数表示动作用三个宏来表示 EPOLL_CTL_ADD 注册新的fd到epfd中 EPOLL_CTL_MOD 修改已经注册的fd的监听事件 EPOLL_CTL_DEL 从epfd中删除一个fd 第三个参数是需要监听的fd第四个参数是告诉内核需要监听什么事件struct epoll_event结构如下 struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; events可以是以下几个宏的集合 EPOLLIN 触发该事件表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭) EPOLLOUT 触发该事件表示对应的文件描述符上可以写数据 EPOLLPRI 表示对应的文件描述符有紧急的数据可读这里应该表示有带外数据到来 EPOLLERR 表示对应的文件描述符发生错误 EPOLLHUP 表示对应的文件描述符被挂断 EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT 只监听一次事件当监听完这次事件之后如果还需要继续监听这个socket的话需要再次把这个socket加入到EPOLL队列里。 3. 等待事件触发当超过timeout还没有事件触发时就超时。int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待事件的产生类似于select()调用。参数events用来从内核得到事件的集合maxevents告之内核这个events有多大(数组成员的个数)这个maxevents的值不能大于创建epoll_create()时的size参数timeout是超时时间毫秒0会立即返回-1将不确定也有说法说是永久阻塞。该函数返回需要处理的事件数目如返回0表示已超时。返回的事件集合在events数组中数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 从man手册中得到ET和LT的具体描述如下 EPOLL事件有两种模型 Edge Triggered (ET) //高速工作方式错误率比较大只支持no_block socket (非阻塞socket) Level Triggered (LT) //缺省工作方式即默认的工作方式,支持block socket和no_block socket错误率比较小。 假如有这样一个例子(LT方式即默认方式下内核会继续通知可以读数据ET方式内核不会再通知可以读数据) 1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符 2. 这个时候从管道的另一端被写入了2KB的数据 3. 调用epoll_wait(2)并且它会返回RFD说明它已经准备好读取操作 4. 然后我们读取了1KB的数据 5. 调用epoll_wait(2)...... Edge Triggered 工作模式 如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志那么在第5步调用epoll_wait(2)之后将有可能会挂起因为剩余的数据还存在于文件的输入缓冲区内而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中会有一个事件产生在RFD句柄上因为在第2步执行了一个写操作然后事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据因此我们在第5步调用 epoll_wait(2)完成后是否挂起是不确定的。epoll工作在ET模式的时候必须使用非阻塞套接口以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口在后面会介绍避免可能的缺陷。(LT方式可以解决这种缺陷) i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时(认为读完)才需要挂起等待。但这并不是说每次read()时都需要循环读直到读到产生一个EAGAIN才认为此次事件处理完成当read()返回的读到的数据长度小于请求的数据长度时(即小于sizeof(buf))就可以确定此时缓冲中已没有数据了也就可以认为此事读事件已处理完成。 Level Triggered 工作模式 (默认的工作方式) 相反的以LT方式调用epoll接口的时候它就相当于一个速度比较快的poll(2)并且无论后面的数据是否被使用因此他们具有同样的职能。因为即使使用ET模式的epoll在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。 然后详细解释ET, LT: //没有对就绪的fd进行IO操作内核会不断的通知。 LT(level triggered)是缺省的工作方式并且同时支持block和no-block socket。在这种做法中内核告诉你一个文件描述符是否就绪了然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作内核还是会继续通知你的所以这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。 //没有对就绪的fd进行IO操作内核不会再进行通知。 ET(edge-triggered)是高速工作方式只支持no-block socket。在这种模式下当描述符从未就绪变为就绪时内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪并且不会再为那个文件描述符发送更多的就绪通知直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如你在发送接收或者接收请求或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误。但是请注意如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)内核不会发送更多的通知(only once),不过在TCP协议中ET模式的加速效用仍需要更多的benchmark确认这句话不理解。 另外当使用epoll的ET模型(epoll的非默认工作方式)来工作时当产生了一个EPOLLIN事件后 读数据的时候需要考虑的是当recv()返回的大小如果等于要求的大小即sizeof(buf)那么很有可能是缓冲区还有数据未读完也意味着该次事件还没有处理完所以还需要再次读取 while(rs) //ET模型 { buflen recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen 0) { // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读 // 在这里就当作是该次事件已处理处. if(errno EAGAIN || errno EINT) //即当buflen0且errnoEAGAIN时表示没有数据了。(读/写都是这样) break; else return; //真的失败了。 } else if(buflen 0) { // 这里表示对端的socket已正常关闭. } if(buflen sizeof(buf) rs 1; // 需要再次读取(有可能是因为数据缓冲区buf太小所以数据没有读完) else rs 0; //不需要再次读取(当buflensizeof(buf)时这是ET模式下非阻塞文件描述符的特性) } 非常重要:: 还有假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.这种方法类似于readn和writen的封装(自己写过在《UNIX环境高级编程》中也有介绍) ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total buflen; const char *p buffer; while(1) { tmp send(sockfd, p, total, 0); if(tmp 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp total) return buflen; total - tmp; p tmp; } return tmp; }