在一個非阻塞的socket上調用read/write函數, 傳回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
從字面上看, 意思是:EAGAIN: 再試一次,EWOULDBLOCK: 如果這是一個阻塞socket, 操作将被block,perror輸出: Resource temporarily unavailable
總結:
這個錯誤表示資源暫時不夠,能read時,讀緩沖區沒有資料,或者write時,寫緩沖區滿了。遇到這種情況,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即傳回-1, 同時errno設定為EAGAIN。
是以,對于阻塞socket,read/write傳回-1代表網絡出錯了。但對于非阻塞socket,read/write傳回-1不一定網絡真的出錯了。可能是Resource temporarily unavailable。這時你應該再試,直到Resource available。
綜上,對于non-blocking的socket,正确的讀寫操作為:
讀:忽略掉errno = EAGAIN的錯誤,下次繼續讀
寫:忽略掉errno = EAGAIN的錯誤,下次繼續寫
對于select和epoll的LT模式,這種讀寫方式是沒有問題的。但對于epoll的ET模式,這種方式還有漏洞。
epoll的兩種模式LT和ET
二者的差異在于level-trigger模式下隻要某個socket處于readable/writable狀态,無論什麼時候進行epoll_wait都會傳回該socket;而edge-trigger模式下隻有某個socket從unreadable變為readable或從unwritable變為writable時,epoll_wait才會傳回該socket。如下兩個示意圖:
從socket讀資料:

往socket寫資料
是以,在epoll的ET模式下,正确的讀寫方式為:
讀:隻要可讀,就一直讀,直到傳回0,或者 errno = EAGAIN
寫:隻要可寫,就一直寫,直到資料發送完,或者 errno = EAGAIN
正确的讀:
點選(此處)折疊或打開
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
正确的寫
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
nwrite = write(fd, buf + data_size - n, n);
if (nwrite n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror("write error");
}
break;
}
n -= nwrite;
正确的accept,accept 要考慮 2 個問題
(1) 阻塞模式 accept 存在的問題
考慮這種情況:TCP連接配接被用戶端夭折,即在伺服器調用accept之前,用戶端主動發送RST終止連接配接,導緻剛剛建立的連接配接從就緒隊列中移出,如果套接口被設定成阻塞模式,伺服器就會一直阻塞在accept調用上,直到其他某個客戶建立一個新的連接配接為止。但是在此期間,伺服器單純地阻塞在accept調用上,就緒隊列中的其他描述符都得不到處理。 解決辦法是把監聽套接口設定為非阻塞,當客戶在伺服器調用accept之前中止某個連接配接時,accept調用可以立即傳回-1,這時源自Berkeley的實作會在核心中處理該事件,并不會将該事件通知給epool,而其他實作把errno設定為ECONNABORTED或者EPROTO錯誤,我們應該忽略這兩個錯誤。
(2)ET模式下accept存在的問題
考慮這種情況:多個連接配接同時到達,伺服器的TCP就緒隊列瞬間積累多個就緒連接配接,由于是邊緣觸發模式,epoll隻會通知一次,accept隻處理一個連接配接,導緻TCP就緒隊列中剩下的連接配接都得不到處理。 解決辦法是用while循環抱住accept調用,處理完TCP就緒隊列中的所有連接配接後再退出循環。如何知道是否處理完就緒隊列中的所有連接配接呢?accept傳回-1并且errno設定為EAGAIN就表示所有連接配接都處理完。
綜合以上兩種情況,伺服器應該使用非阻塞地accept,accept在ET模式下的正确使用方式為:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0)
handle_client(conn_sock);
if (conn_sock == -1)
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
perror("accept");
一道騰訊背景開發的面試題
使用Linuxepoll模型,水準觸發模式;當socket可寫時,會不停的觸發socket可寫的事件,如何處理?
第一種最普遍的方式:
需要向socket寫資料的時候才把socket加入epoll,等待可寫事件。
接受到可寫事件後,調用write或者send發送資料。
當所有資料都寫完後,把socket移出epoll。
這種方式的缺點是,即使發送很少的資料,也要把socket加入epoll,寫完後在移出epoll,有一定操作代價。
一種改進的方式:
開始不把socket加入epoll,需要向socket寫資料的時候,直接調用write或者send發送資料。如果傳回EAGAIN,把socket加入epoll,在epoll的驅動下寫資料,全部資料發送完畢後,再移出epoll。
這種方式的優點是:資料不多的時候可以避免epoll的事件處理,提高效率。
最後貼一個使用epoll,ET模式的簡單HTTP伺服器代碼:
#include
#define MAX_EVENTS 10
#define PORT 8080
//設定socket連接配接為非阻塞模式
void setnonblocking(int sockfd) {
int opts;
opts = fcntl(sockfd, F_GETFL);
if(opts
perror("fcntl(F_GETFL)\n");
exit(1);
opts = (opts | O_NONBLOCK);
if(fcntl(sockfd, F_SETFL, opts)
perror("fcntl(F_SETFL)\n");
int main(){
struct epoll_event ev, events[MAX_EVENTS];
int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
struct sockaddr_in local, remote;
char buf[BUFSIZ];
//建立listen socket
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0))
perror("sockfd\n");
setnonblocking(listenfd);
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);;
local.sin_port = htons(PORT);
if( bind(listenfd, (struct sockaddr *) &local, sizeof(local))
perror("bind\n");
listen(listenfd, 20);
epfd = epoll_create(MAX_EVENTS);
if (epfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
perror("epoll_ctl: listen_sock");
for (;;) {
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_pwait");
for (i = 0; i
fd = events[i].data.fd;
if (fd == listenfd) {
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0) {
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: add");
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
continue;
if (events[i].events & EPOLLIN) {
n += nread;
if (nread == -1 && errno != EAGAIN) {
perror("read error");
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {
perror("epoll_ctl: mod");
if (events[i].events & EPOLLOUT) {
sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
while (n > 0) {
nwrite = write(fd, buf + data_size - n, n);
if (nwrite
if (nwrite == -1 && errno != EAGAIN) {
perror("write error");
break;
n -= nwrite;
close(fd);
return 0;
epoll的優點
1. 支援一個程序打開大數目的socket描述符(FD)
select 最不能忍受的是一個程序所打開的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對于那些需要支援的上萬連接配接數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多程序的解決方案(傳統的Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。
2. IO效率不随FD數目增加而線性下降
傳統的select/poll另一個緻命弱點就是當你擁有一個很大的socket集合,不過由于網絡延時,任一時間隻有部分的socket是"活躍"的,但是select/poll每次調用都會線性掃描全部的集合,導緻效率呈現線性下降。但是epoll不存在這個問題,它隻會對"活躍"的socket進行操作---這是因為在核心實作中epoll是根據每個fd上面的callback函數實作的。那麼,隻有"活躍"的socket才會主動的去調用 callback函數,其他idle狀态socket則不會,在這點上,epoll實作了一個"僞"AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll并不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模拟WAN環境,epoll的效率就遠在select/poll之上了。
3. 使用mmap加速核心與使用者空間的消息傳遞。
這點實際上涉及到epoll的具體實作了。無論是select,poll還是epoll都需要核心把FD消息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心于使用者空間mmap同一塊記憶體實作的。而如果你想我一樣從2.5核心就關注epoll的話,一定不會忘記手工 mmap這一步的。
4. 核心微調
這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法回避linux平台賦予你微調核心的能力。比如,核心TCP/IP協定棧使用記憶體池管理sk_buff結構,那麼可以在運作時期動态調整這個記憶體pool(skb_head_pool)的大小--- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的資料包隊列長度),也可以根據你平台記憶體大小動态調整。更甚至在一個資料包面數目巨大但同時每個資料包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
<a href="http://blog.linezing.com/2011/01/%E5%89%96%E6%9E%90-epoll-etlt-%E8%A7%A6%E5%8F%91%E6%96%B9%E5%BC%8F%E7%9A%84%E6%80%A7%E8%83%BD%E5%B7%AE%E5%BC%82%E8%AF%AF%E8%A7%A3%EF%BC%88%E5%AE%9A%E6%80%A7%E5%88%86%E6%9E%90%EF%BC%89">剖析 epoll ET/LT 觸發方式的性能差異誤解(定性分析)</a>
平時大家使用 epoll 時都知道其事件觸發模式有預設的 level-trigger 模式和通過 EPOLLET 啟用的 edge-trigger 模式兩種。從 epoll 發展曆史來看,它剛誕生時隻有 edge-trigger 模式,後來因容易産生 race-cond 且不易被開發者了解,又增加了 level-trigger 模式并作為預設處理方式。
二者的差異在于 level-trigger 模式下隻要某個 fd 處于 readable/writable 狀态,無論什麼時候進行 epoll_wait 都會傳回該 fd;而 edge-trigger 模式下隻有某個 fd 從 unreadable 變為 readable 或從 unwritable 變為 writable 時,epoll_wait 才會傳回該 fd。
通常的誤區是:level-trigger 模式在 epoll 池中存在大量 fd 時效率要顯著低于 edge-trigger 模式。
但從 kernel 代碼來看,edge-trigger/level-trigger 模式的處理邏輯幾乎完全相同,差别僅在于 level-trigger 模式在 event 發生時不會将其從 ready list 中移除,略為增大了 event 處理過程中 kernel space 中記錄資料的大小。
然而,edge-trigger 模式一定要配合 user app 中的 ready list 結構,以便收集已出現 event 的 fd,再通過 round-robin 方式挨個處理,以此避免通信資料量很大時出現忙于處理熱點 fd 而導緻非熱點 fd 餓死的現象。統觀 kernel 和 user space,由于 user app 中 ready list 的實作千奇百怪,不一定都經過仔細的推敲優化,是以 edge-trigger 的總記憶體開銷往往還大于 level-trigger 的開銷。
一般号稱 edge-trigger 模式的優勢在于能夠減少 epoll 相關系統調用,這話不假,但 user app 裡可不是隻有 epoll 相關系統調用吧?為了繞過餓死問題,edge-trigger 模式的 user app 要自行進行 read/write 循環處理,這其中增加的系統調用和減少的 epoll 系統調用加起來,有誰能說一定就能明顯地快起來呢?
實際上,epoll_wait 的效率是 O(ready fd num) 級别的,是以 edge-trigger 模式的真正優勢在于減少了每次 epoll_wait 可能需要傳回的 fd 數量,在并發 event 數量極多的情況下能加快 epoll_wait 的處理速度,但别忘了這隻是針對 epoll 體系自己而言的提升,與此同時 user app 需要增加複雜的邏輯、花費更多的 cpu/mem 與其配合工作,總體性能收益究竟如何?隻有實際測量才知道,無法一概而論。不過,為了降低處理邏輯複雜度,常用的事件處理庫大部分都選擇了 level-trigger 模式(如 libevent、boost::asio等)
結論:
? epoll 的 edge-trigger 和 level-trigger 模式處理邏輯差異極小,性能測試結果表明正常應用場景 中二者性能差異可以忽略。
? 使用 edge-trigger 的 user app 比使用 level-trigger 的邏輯複雜,出錯機率更高。
? edge-trigger 和 level-trigger 的性能差異主要在于 epoll_wait 系統調用的處理速度,是否是 user app 的性能瓶頸需要視應用場景而定,不可一概而論。