徹底學會使用epoll(五)—— ET模式下的注意事項

gettogetto發表於2017-03-26

徹底學會epoll(五)—— ET模式下的注意事項

——lvyilong316

5.1 ET模式下的讀寫

    經過前面幾節分析,我們可以知道,當epoll工作在ET模式下時,對於讀操作,如果read一次沒有讀盡buffer中的資料,那麼下次將得不到讀就緒的通知,造成buffer中已有的資料無機會讀出,除非有新的資料再次到達。對於寫操作,主要是因為ET模式下fd通常為非阻塞造成的一個問題——如何保證將使用者要求寫的資料寫完。

要解決上述兩個ET模式下的讀寫問題,我們必須實現:

a. 對於讀,只要buffer中還有資料就一直讀;

b. 對於寫,只要buffer還有空間且使用者請求寫的資料還未寫完,就一直寫。

要實現上述ab兩個效果,我們有兩種方法解決。

方法一

(1) 每次讀入操作後(readrecv),使用者主動epoll_mod IN事件,此時只要該fd的緩衝還有資料可以讀,則epoll_wait返回讀就緒

(2) 每次輸出操作後(writesend),使用者主動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,勢必造成效率降低,這不是適得其反嗎?

綜上,此方式不應該使用。

方法二

只要可讀就一直讀直到返回 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不能工作在阻塞模式,而是工作在阻塞模式可能在執行中會出現一些問題。

方法三

仔細分析方法二的寫操作,我們發現這種方式並不很完美,因為寫操作返回EAGAIN就終止寫,但是返回EAGAIN只能說名當前buffer已滿不可寫,並不能保證使用者(或服務端)要求寫的資料已經寫完。那麼如何保證對非阻塞的套接字寫夠請求的位元組數才返回呢(阻塞的套接字直到將請求寫的位元組數寫完才返回)?

我們需要封裝socket_write()的函式用來處理這種情況,該函式會盡量將資料寫完再返回,返回-1表示出錯。socket_write()內部,當寫緩衝已滿(send()返回-1,errnoEAGAIN),那麼會等待後再重試.

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");   

擴充套件服務端使用多路轉接技術(selectpollepoll等)時,accept應工作在非阻塞模式。 

原因:如果accept工作在阻塞模式,考慮這種情況: TCP 連線被客戶端夭折,即在伺服器呼叫 accept 之前(此時select等已經返回連線到達讀就緒),客戶端主動傳送 RST 終止連線,導致剛剛建立的連線從就緒佇列中移出,如果套介面被設定成阻塞模式,伺服器就會一直阻塞在 accept 呼叫上,直到其他某個客戶建立一個新的連線為止。但是在此期間,伺服器單純地阻塞在accept 呼叫上(實際應該阻塞在select上),就緒佇列中的其他描述符都得不到處理。

    解決辦法是把監聽套介面設定為非阻塞, 當客戶在伺服器呼叫 accept 之前中止

某個連線時,accept 呼叫可以立即返回 -1, 這時源自 Berkeley 的實現會在核心中處理該事件,並不會將該事件通知給 epoll,而其他實現把 errno 設定為 ECONNABORTED 或者 EPROTO 錯誤,我們應該忽略這兩個錯誤。(具體可參看UNP v1 p363

相關文章