從代碼開始吧:
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