徹底學會使用epoll(五)—— ET模式下的注意事項
徹底學會epoll(五)—— ET模式下的注意事項
5.1 ET模式下的讀寫
經過前面幾節分析,我們可以知道,當epoll工作在ET模式下時,對於讀操作,如果read一次沒有讀盡buffer中的資料,那麼下次將得不到讀就緒的通知,造成buffer中已有的資料無機會讀出,除非有新的資料再次到達。對於寫操作,主要是因為ET模式下fd通常為非阻塞造成的一個問題——如何保證將使用者要求寫的資料寫完。
要解決上述兩個ET模式下的讀寫問題,我們必須實現:
a. 對於讀,只要buffer中還有資料就一直讀;
b. 對於寫,只要buffer還有空間且使用者請求寫的資料還未寫完,就一直寫。
要實現上述a、b兩個效果,我們有兩種方法解決。
l 方法一
(1) 每次讀入操作後(read,recv),使用者主動epoll_mod IN事件,此時只要該fd的緩衝還有資料可以讀,則epoll_wait會返回讀就緒。
(2) 每次輸出操作後(write,send),使用者主動epoll_mod OUT事件,此時只要該該fd的緩衝可以傳送資料(傳送buffer不滿),則epoll_wait就會返回寫就緒(有時候採用該機制通知epoll_wai醒過來)。
這個方法的原理我們在之前討論過:當buffer中有資料可讀(即buffer不空)且使用者對相應fd進行epoll_mod IN事件時ET模式返回讀就緒,當buffer中有可寫空間(即buffer不滿)且使用者對相應fd進行epoll_mod OUT事件時返回寫就緒。
所以得到如下解決方式:
if(events[i].events&EPOLLIN)//如果收到資料,那麼進行讀入
{
cout << "EPOLLIN" << endl;
sockfd = events[i].data.fd;
if ( (n = read(sockfd, line, MAXLINE))>0)
{
line[n] = '/0';
cout << "read " << line << endl;
if(n==MAXLINE)
{
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //資料還沒讀完,重新MOD IN事件
}
else
{
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //buffer中的資料已經讀取完畢MOD OUT事件
}
}
else if (n == 0)
{
close(sockfd);
}
}
else if(events[i].events&EPOLLOUT) // 如果有資料傳送
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
ev.data.fd=sockfd; //設定用於讀操作的檔案描述符
ev.events=EPOLLIN|EPOLLET; //設定用於注測的讀操作事件
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要處理的事件為EPOLIN
}
注:對於write操作,由於sockfd是工作在阻塞模式下的,所以沒有必要進行特殊處理,和LT使用一樣。
分析:這種方法存在幾個問題:
(1) 對於read操作後的判斷——if(n==MAXLINE),不能說明這種情況buffer就一定還有沒有讀完的資料,試想萬一buffer中一共就有MAXLINE位元組資料呢?這樣繼續 MOD IN就不再得到通知,而也就沒有機會對相應sockfd MOD OUT。
(2) 那麼如果服務端用其他方式能夠在適當時機對相應的sockfd MOD OUT,是否這種方法就可取呢?我們首先思考一下為什麼要用ET模式,因為ET模式能夠減少epoll_wait等系統呼叫,而我們在這裡每次read後都要MOD IN,之後又要epoll_wait,勢必造成效率降低,這不是適得其反嗎?
綜上,此方式不應該使用。
l 方法二
讀: 只要可讀, 就一直讀, 直到返回 0, 或者 errno = EAGAIN
寫: 只要可寫, 就一直寫, 直到資料傳送完, 或者 errno = EAGAIN
if (events[i].events & EPOLLIN)
{
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
if (events[i].events & EPOLLOUT)
{
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;
}
ev.data.fd=fd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev); //修改sockfd上要處理的事件為EPOLIN
}
注:使用這種方式一定要使每個連線的套接字工作於非阻塞模式,因為讀寫需要一直讀或寫直到出錯(對於讀,當讀到的實際位元組數小於請求位元組數時就可以停止),而如果你的檔案描述符如果不是非阻塞的,那這個一直讀或一直寫勢必會在最後一次阻塞。這樣就不能在阻塞在epoll_wait上了,造成其他檔案描述符的任務餓死。
綜上:方法一不適合使用,我們只能使用方法二,所以也就常說“ET需要工作在非阻塞模式”,當然這並不能說明ET不能工作在阻塞模式,而是工作在阻塞模式可能在執行中會出現一些問題。
l 方法三
仔細分析方法二的寫操作,我們發現這種方式並不很完美,因為寫操作返回EAGAIN就終止寫,但是返回EAGAIN只能說名當前buffer已滿不可寫,並不能保證使用者(或服務端)要求寫的資料已經寫完。那麼如何保證對非阻塞的套接字寫夠請求的位元組數才返回呢(阻塞的套接字直到將請求寫的位元組數寫完才返回)?
我們需要封裝socket_write()的函式用來處理這種情況,該函式會盡量將資料寫完再返回,返回-1表示出錯。在socket_write()內部,當寫緩衝已滿(send()返回-1,且errno為EAGAIN),那麼會等待後再重試.
ssize_t socket_write(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char* p = buffer;
while(1)
{
tmp = write(sockfd, p, total);
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;//返回已寫位元組數
}
分析:這種方式也存在問題,因為在理論上可能會長時間的阻塞在socket_write()內部(buffer中的資料得不到傳送,一直返回EAGAIN),但暫沒有更好的辦法。
不過看到這種方式時,我在想在socket_write中將sockfd改為阻塞模式應該一樣可行,等再次epoll_wait之前再將其改為非阻塞。
5.2 ET模式下的accept
考慮這種情況:多個連線同時到達,伺服器的 TCP 就緒佇列瞬間積累多個就緒
連線,由於是邊緣觸發模式,epoll 只會通知一次,accept 只處理一個連線,導致 TCP 就緒佇列中剩下的連線都得不到處理。
解決辦法是用 while 迴圈抱住 accept 呼叫,處理完 TCP 就緒佇列中的所有連線後再退出迴圈。如何知道是否處理完就緒佇列中的所有連線呢? accept 返回 -1 並且 errno 設定為 EAGAIN 就表示所有連線都處理完。
的正確使用方式為:
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");
}
擴充套件:服務端使用多路轉接技術(select,poll,epoll等)時,accept應工作在非阻塞模式。
原因:如果accept工作在阻塞模式,考慮這種情況: TCP 連線被客戶端夭折,即在伺服器呼叫 accept 之前(此時select等已經返回連線到達讀就緒),客戶端主動傳送 RST 終止連線,導致剛剛建立的連線從就緒佇列中移出,如果套介面被設定成阻塞模式,伺服器就會一直阻塞在 accept 呼叫上,直到其他某個客戶建立一個新的連線為止。但是在此期間,伺服器單純地阻塞在accept 呼叫上(實際應該阻塞在select上),就緒佇列中的其他描述符都得不到處理。
解決辦法是把監聽套介面設定為非阻塞, 當客戶在伺服器呼叫 accept 之前中止
某個連線時,accept 呼叫可以立即返回 -1, 這時源自 Berkeley 的實現會在核心中處理該事件,並不會將該事件通知給 epoll,而其他實現把 errno 設定為 ECONNABORTED 或者 EPROTO 錯誤,我們應該忽略這兩個錯誤。(具體可參看UNP v1 p363)
相關文章
- 徹底學會使用epoll(一)——ET模式實現分析模式
- 徹底學會使用epoll(三)——ET的讀操作例項分析
- 徹底學會使用epoll(四)——ET的寫操作例項分析
- 徹底學會使用epoll(六)——關於ET的若干問題總結
- 「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IOLinux模式
- Epoll在LT和ET模式下的讀寫方式模式
- Oracle使用*的注意事項Oracle
- 使用parallel注意事項Parallel
- 使用Google Fonts注意事項Go
- Go 切片使用注意事項Go
- 使用CocosBuilder注意事項UI
- removeChild使用時注意事項REM
- 使用Vue.js的注意事項Vue.js
- 使用HTTP的三個注意事項HTTP
- 快取使用中的注意事項快取
- 【前端】一文徹底學會Promise前端Promise
- TCP使用注意事項總結TCP
- C中memcpy使用注意事項memcpy
- 萬兆網路卡使用注意事項
- MySQL半同步使用注意事項MySql
- Guava HashMultimap使用及注意事項Guava
- setbuf函式使用注意事項函式
- php getallheaders使用注意事項PHPHeader
- 使用直方圖注意事項直方圖
- Azure ARM模式下VNet配置中需要注意的幾點事項模式
- ip代理軟體的使用注意事項
- 說點JSON使用的注意事項JSON
- cookie的使用方法以及注意事項Cookie
- ThinkPHP中CURD where的使用注意事項PHP
- Linux中fork的使用注意事項Linux
- 在 HttpHandler 中使用 Session 的注意事項HTTPSession
- Xlistview的注意事項View
- 大資料學習注意事項大資料
- Oracle臨時表使用注意事項Oracle
- 伺服器使用安全注意事項伺服器
- 不同版本exp/imp使用注意事項
- mysql索引使用技巧及注意事項MySql索引
- uni-app 使用Weex/nvue的注意事項APPVue