从代码开始吧:
epoll_ctl(clifd, EPOLL_CTL_ADD, EPOLLIN | EPOLLOUT);
epoll主循环将使用水平模式(默认,EPOLLLT)监听clifd的读写状态,在水平模式下,只要clifd的内核读缓冲区存在未读的数据,每一次的epoll_wait()返回针对clifd的epoll_event都会设置EPOLLIN;只要clifd的内核写缓冲区存在可写空间,每一次的epoll_wait()返回针对clifd的epoll_event都会设置EPOLLOUT。通常来说,读光内核缓冲区不难,写满内核缓冲区就有点扯了。通常的解决方案是:
例如在一个epoll循环内,监听readfd的读事件,将从readfd中获得的数据完整写到writefd中,当write()并未完整执行时,监听writefd的写事件:
1 while (1) 2 { 3 int n, i; 4 n = epoll_wait(epoll_fd, events, 8192, -1); 5 6 for (i = 0; i < n; i++) 7 { 8 int fd = events[i].data.fd; 9 10 if (events[i].events & EPOLLOUT) 11 { 12 // ... 13 // 找到在NonBlockSend中的缓存buf 14 int nwrite = write(fd, buf, len); 15 if (nwrite == len) 16 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); 17 else 18 // 更新buf 19 } 20 21 if (events[i].events & EPOLLIN) 22 { 23 char buf[1024]; 24 int len = read(fd, buf, sizeof(buf)); 25 26 int nwrite = write(writefd, buf, len);
if (len == nwrite)
return; 27 if (-1 == nwrite && EAGAIN != errno) 28 { 29 close(writefd); 30 } 31 else 32 { 33 nwrite = nwrite == -1 ? 0 : nwrite; 34 NonBlockSend(writefd, buf + nwrite, len - nwrite); 35 } 36 } 37 } 38 }
其中NonBlockSend()就是将数据存入writefd对应的缓冲区中,并为writefd建立EPOLLOUT事件监听。恩,看起来没什么问题了。但是,我也是刚刚才发现,问题还是有的。想象一下,某一次epoll_wait()返回writefd写事件,针对writefd的write()调用将会成功,然而,假如同时触发的还有readfd的读事件,并且该事件先于writefd写事件处理,那么,从readfd中读到的新数据将先于已缓存的旧数据发送。。。。所以,处理readfd的读事件,应该先判断writefd是否已有缓存数据,是则直接调用NonBlockSend()。
不断修改epoll的监听事件集合好像不太好,于是我就想能否用一下边缘模式(EPOLLET),因为在该模式下,只有当writefd从不可写变为可写时,epoll_wait()才会通知writefd的写事件,也就是说,直到你再次把缓冲区写满后,epoll_wait()的返回才有可能包含writefd。对上面简单的应用场合,使用EPOLLET是合情合理的,只需要把15至17行删掉,然后从一开始就为writefd建立EPOLLOUT事件监听,而不是放到NonBlockSend()里。
附几篇关于epoll两种模式的介绍:
http://stackoverflow.com/questions/12892286/epoll-with-edge-triggered-event/12897334#12897334
http://stackoverflow.com/questions/9162712/what-is-the-purpose-of-epolls-edge-triggered-option