標籤:

使用epoll時需要將socket設為非阻塞嗎?

在APUE中介紹select和poll中說「一個描述阻塞與否並不影響select是否阻塞。也就是說,如果希望讀一個非阻塞描述符,並且以超時值5s調用select,則select最多阻塞5s。」


我看到有些程序使用epoll時,使用fcntl將描述符置為非阻塞。這個和上面描述的有衝突沒?


對於epoll有兩種觸發模式:水平觸發LT和邊緣觸發ET,其中邊緣觸發「必須」(經評論區提示,這個這個「必須「用的不嚴謹,說明一下:不是因為程序硬性要求這樣,而是從工程實現的角度來看,如果不這麼做會產生問題)需要設置所監聽的socket為non_blocking。

邊緣觸發,顧名思義,不到邊緣情況,是死都不會觸發的。

EPOLLOUT事件:

EPOLLOUT事件在連接時建立時首先觸發觸發一次,表示可寫,其他時候的觸發條件為:


1.某次write,寫滿了發送緩衝區,返回錯誤碼為EAGAIN。


2.對端讀取了一些數據,又重新可寫了,此時會觸發EPOLLOUT。

簡單地說:EPOLLOUT事件只有socket從unwritable變為writable時,才會觸發一次。對於EPOLLOUT事件,必須要將該文件描述符緩衝區一直寫滿,讓 errno 返回 EAGAIN 為止,或者發送完所有數據為止。

EPOLLIN事件:

EPOLLIN事件則只有當對端有數據寫入時才會觸發,所以觸發一次後需要不斷讀取所有數據直到讀完EAGAIN為止,否則剩下的數據只有在下次對端有寫入時才能一起取出來了。設想這樣一個場景:接收端接收完整的數據後會向對端發送應答報文,對端才會繼續向接收端發送數據,從而觸發下一次的EPOLLIN,而這時沒有讀完socket緩衝區中的所有數據,導致接收端無法向對端發送應答報文,而對端沒有收到應答報文,也就不會再發送數據觸發下一次的EPOLLIN,而沒有下一次的EPOLLIN事件,接收端也就永遠不知道此socket緩衝區中還有未讀出的數據。(一個完美的死循環)

簡單的說:EPOLLIN事件只有對端新數據寫入時,才會觸發一次。對於EPOLLIN事件,必須要將該文件描述符一直讀到空,讓 errno 返回 EAGAIN 為止。

總結:現在明白為什麼說epoll要求非同步socket了吧?如果你的文件描述符如果不是非阻塞的.1.對於讀:由於需要一直讀直到把數據讀完,所以大家在編寫程序的時候一般會用一個循環一直讀取socket,那這個循環勢必會在最後一次阻塞,即沒有數據可讀的情況下,阻塞式socket會在數據讀完之後一直阻塞下去,而非阻塞式的socket則返回&<0,並讓errno 返回 EAGAIN 。2.對於寫,當使用阻塞式socket時,socket的unwritable/writable狀態變化沒有任何意義!!因為此時無論發送多大的數據write總是會阻塞直到所有數據都發送出去。(也就是說,邊緣觸發的epoll如果不和非阻塞的socket搭配,使用起來會產生問題


設想這樣的情況。

你使用了epoll,且你的fd是阻塞的。

如果epoll通知你fd可以寫數據了。然後你啦啦啦的打算寫100B數據,但是內核buf只有50B的可用

空間,這時候,你的進程就被阻塞了。。。

所以說,用了epoll不能保證你的進程在讀寫的時候不會阻塞


我覺得只有邊沿觸發才必須設置為非阻塞。

邊沿觸發的問題:

1. sockfd 的邊緣觸發,高並發時,如果沒有一次處理全部請求,則會出現客戶端連接不上的問題。不需要討論 sockfd 是否阻塞,因為 epoll_wait() 返回的必定是已經就緒的連接,所以不管是阻塞還是非阻塞,accept() 都會立即返回。

2. 阻塞 connfd 的邊緣觸發,如果不一次性讀取一個事件上的數據,會干擾下一個事件,所以必須在讀取數據的外部套一層循環,這樣才能完整的處理數據。但是外層套循環之後會導致另外一個問題:處理完數據之後,程序會一直卡在 recv() 函數上,因為是阻塞 IO,如果沒數據可讀,它會一直等在那裡,直到有數據可讀。但是這個時候,如果用另一個客戶端去連接伺服器,伺服器就不能受理這個新的客戶端了。

3. 非阻塞 connfd 的邊緣觸發,和阻塞版本一樣,必須在讀取數據的外部套一層循環,這樣才能完整的處理數據。因為非阻塞 IO 如果沒有數據可讀時,會立即返回,並設置 errno。這裡我們根據 EAGAIN 和 EWOULDBLOCK 來判斷數據是否全部讀取完畢了,如果讀取完畢,就會正常退出循環了。

總結一下:

1. 對於監聽的 sockfd,最好使用水平觸發模式,邊緣觸發模式會導致高並發情況下,有的客戶端會連接不上。如果非要使用邊緣觸發,可以用 while 來循環 accept()。

2. 對於讀寫的 connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,建議設置非阻塞。

3. 對於讀寫的 connfd,邊緣觸發模式下,必須使用非阻塞 IO,並要求一次性地完整讀寫全部數據。


哈哈哈哈, 終於可以在我乎上回答一個專業問題啦~~~我也是剛看明白, 所以特地來分享一下

結論先行:

使用epoll是否需要將socket設置為nonblocking?

取決於你使用的觸發方式, 如果你使用水平觸發(Level-triggered) 那麼此時的epoll相當於高級的select, 你的論述是對的, 是不需要一定將socket設置為非阻塞的; 然而, 當你使用邊緣觸發(Edge-triggered) 那麼此時從業務的完整性考慮, 是建議將socket設置為nonbocking模式, 並且在讀寫觸發EAGAIN之後再進行epoll_wait.

來解釋一下水平觸發和邊緣觸發, 類似於數字電路當中的電位水平, 從低電平到高電平的瞬間觸發動作叫邊緣觸發, 而處於高電平觸發動作叫做水平觸發。

想像這樣一個場景:

有一個pipe描述符 fd 按順序發生了如下的動作:

1. 讀端的 fd 被註冊到一個epoll的描述符當中, 監聽讀信號, 此時pipe中沒有消息, 無論是邊緣觸發還是水平觸發此刻都不會被觸發

2. fd 的寫端被寫入2kb數據

3. 讀端調用epoll_wait, 返回 fd, 此刻pipe中有2kb數據, 並且從不可讀變為可讀, 所以邊緣觸發和水平觸發都會返回

4. 讀端讀取1kb的數據

5. 讀端繼續調用epoll_wait

在第五步的時候, 邊緣觸發和水平觸發的差異就顯現出來了, 此時pipe中仍然有數據,所以水平觸發的epoll會立刻返回, 但是邊緣觸發的epoll_wait 並不會返回, 因為此時pipe一直可讀, 並沒有從不可讀變為可讀狀態

所以這裡就會出現一個問題, 如果寫端在等讀端處理完數據返回, 而讀端卻在等寫端的2kb數據中的另外1kb, 雙方就會產生死鎖。 因此, 在使用邊緣觸發的時候, 建議將描述符設置為nonblocking, 並且在read/write產生EAGAIN的錯誤之後再使用epoll_wait

這也是為什麼文檔中:

An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or write starve

使用了should 而沒有使用 must, 從業務的完整性來說, 為了讓每次邊緣觸發的消息都被完整的解讀, 需要使用nonblocking的描述符 並read/write 直到 EAGAIN

幫忙看一下我的解釋是對的嗎? @陳碩


用個故事來開始我們的答案,故事就是man文檔里的,我只是搬運一下。

Level-triggered and edge-triggered:

1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance.

2. A pipe writer writes 2 kB of data on the write side of the pipe.

3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor.

4. The pipe reader reads 1 kB of data from rfd.

5. A call to epoll_wait(2) is done.

這五步過程我們可以簡化為這樣一個場景:

  1. A與B通過套接字在文件描述符rfd上建立了一個連接。
  2. A在epoll上註冊一個文件描述符rfd。
  3. B向這個文件描述符寫2kB的數據。
  4. epoll_wait告訴A:B發了數據過來,你可以讀了。
  5. A粗心大意只讀取了1kB數據,還有1kB的數據忘記讀了,就以為自己讀完了。
  6. 這時候epoll_wait調用完成。

If the rfd file descriptor has been added to the epoll interface using the EPOLLET (edge-triggered) flag, the call to epoll_wait(2) done in step 5 will probably hang despite the available data still present in the file input buffer; meanwhile the remote peer might be expecting a response based on the data it already sent. The reason for this is that edge-triggered mode delivers events only when changes occur on the monitored file descriptor. So, in step 5 the caller might end up waiting for some data that is already present inside the input buffer. In the above example, an event on rfd will be generated because of the write done in 2 and the event is consumed in 3. Since the read operation done in 4 does not consume the whole buffer data, the call to epoll_wait(2) done in step 5 might block indefinitely.

故事繼續:

第五步做完後,如果epoll_wait設置為了邊緣觸發,儘管文件描述符中還有數據,但是epoll_wait不會被該文件描述中剩餘的數據觸發,除非有新的數據到達。而這剩餘的1kB的數據是B告訴A:你讀完整條消息後給我回個消息,我再給你繼續發後面的數據。但是因為A的粗心,沒有讀到後面的信息,所以A也就沒有給B回復,於是B就不停的等啊,等啊,等到海枯石爛。A也在等B的消息,心想:mmp,只發1kB就不發了,話也不說完。於是等啊,等啊,等到滄海桑田。

之所以出現這樣的情況呢?是因為A不知道B到底給他發了多少信息,導致A不知道有沒有讀完。為了避免這樣的事情再次發生,他們就決定採用非阻塞IO了,A收到信息後反覆讀,直到出現EAGAIN錯誤。

回到題主的問題,使用epoll時是否要設置非阻塞IO。

在邊緣觸發模式下,為了避免上面提及的這種情況的發生,應該設置成非阻塞的IO。

在水平觸發模式下,使用阻塞式IO結構簡單,但是阻塞式IO可能會導致在某個IO上阻塞一段時間(區別是:上面提到邊緣觸發是可能會產生永久阻塞),而其實這時候是可以去處理其他已經準備好的IO,這樣就導致CPU利用率不是特別高。因此,水平觸發模式下,如果需要提高並發 ,避免惡意攻擊,也應該採用非阻塞IO。

學生黨的一點理解,如有疑異,歡迎和大佬們討論。


1.寫過代碼就知道,EPOLLIN事件觸發read了socket的數據後,會將socket事件修改為EPOLLOUT用於伺服器發送消息給客戶端,如果採用阻塞的方式,是永遠不可能將事件修改為EPOLLOUT,所以要採用非阻塞方式。

2.epoll_wait返回觸發了事件的列表時,如果socket採用阻塞的方式,那麼循環會一直阻塞在讀取第一個socket的流程的,而後面socket的數據是無法讀取的


首先,io復用函數如epoll/select等在等待事件發生的時候都是阻塞的。

其次,這裡指的非阻塞說的是io復用函數等待的事件,通常是網路連接上的io。

最後,為什麼使用io復用時,網路io要用非阻塞的?

舉一個最簡單的例子,寫的時候,如果協議棧寫緩衝區只有1個位元組,那麼io復用函數會告訴你可寫,但如果你此時要寫2個位元組,如果是阻塞io那麼調用write就會阻塞。由於io復用都是單線程調用的,一個連接阻塞就會影響該線程上其他的連接,因此必須使用非阻塞io。

ps:

上面的答案只有一個是對的。其餘的都是胡說八道。

建議先搞懂write/read的真實含義以及什麼時候會阻塞再來看看你們的答案吧。


沒有。select和描述符的阻塞方式和時間是分別設置互不影響的。


推薦閱讀:

epoll實現中共享內存問題?
大神們來看看這樣架構 使用epoll有沒有問題?
Linux 3.x 中epoll的驚群問題?
node.js 底層使用的是epoll這種單線程io多路復用,還是多線程阻塞模型?

TAG:epoll |