如何優(yōu)雅的處理 Accept 出現(xiàn) Emfile 的問題
本文轉(zhuǎn)載自微信公眾號「Linux開發(fā)那些事兒」,作者LinuxThings。轉(zhuǎn)載本文請聯(lián)系Linux開發(fā)那些事兒公眾號。
通常情況下,服務(wù)端調(diào)用 accept 函數(shù)會返回一個新的文件描述符,用于和客戶端之間的數(shù)據(jù)傳輸
在服務(wù)器的開發(fā)中,有時會遇到這種情況:當(dāng)調(diào)用 accept 函數(shù)接受客戶端連接,函數(shù)返回失敗,對應(yīng)的錯誤碼是 EMFILE, 它表示當(dāng)前進程打開的文件描述符已達上限,此時,服務(wù)器不能再接受客戶端連接
當(dāng)遇到上述問題,怎么合理的處理呢,下面就來分析一下
建立連接的流程
先簡單回顧下客戶端和服務(wù)器建立連接的流程,具體的如下圖所示:
1. 客戶端發(fā)起 SYN 請求
2. 服務(wù)器收到客戶端的 SYN 請求后,內(nèi)核把連接放入半連接隊列,同時給客戶端返回一個 SYN + ACK
3. 客戶端向服務(wù)器返回一個確認的 ACK, 服務(wù)器收到本次 ACK 之后,三次握手完成,同時,內(nèi)核把連接從半連接隊列中移除,創(chuàng)建新完全連接,加入到全連接隊列中
4. 應(yīng)用層調(diào)用 accept 函數(shù)從全連接隊列中取出連接
上面的第 1、第 2、第 3 步是 TCP 的三次握手,它是由內(nèi)核中TCP協(xié)議完成的, 第 4 步是應(yīng)用層調(diào)用 accept 接口
在 epoll 中的問題
epoll 是 Linux中IO多路復(fù)用模型,在服務(wù)器的開發(fā)中有廣泛的應(yīng)用,下面就以 epoll 為例來詳細說明
服務(wù)器端創(chuàng)建偵聽文件描述符 listenfd 之后, 向 epoll 注冊讀事件
當(dāng) epoll 檢測到 listenfd 上有讀事件發(fā)生,會立即通知應(yīng)用層,應(yīng)用層調(diào)用 accept 接受新連接,而此時進程打開的文件描述符數(shù)量已經(jīng)達到上限了,所以每次 accept 都是失敗的
這里會出現(xiàn)以下幾個問題
- 由于 每次 accept 都失敗了,相當(dāng)于 listenfd 上的可讀事件沒有處理,epoll 會不停的觸發(fā) listenfd 上的可讀事件,應(yīng)用層也就會不停的調(diào)用 accept,然后又出現(xiàn) accept 調(diào)用失敗,如此這般不停的執(zhí)行無效的循環(huán),白白浪費了CPU的資源
- 上面提到服務(wù)器在不停的執(zhí)行無效的循環(huán), 將會引發(fā)另一個問題,如果此時有新客戶端連接到來,建立連接的過程會很慢
前面說的 epoll 默認是使用了水平觸發(fā)模式,如果使用垂直觸發(fā)模式會出現(xiàn)什么問題呢?
垂直觸發(fā)模式下,listenfd 從無讀事件狀態(tài)到有讀事件狀態(tài)時,才會通知到應(yīng)用層,在應(yīng)用層處理完 listenfd 上所有的讀事件之前,epoll 不會再通知應(yīng)用層
也就是說,應(yīng)用層收到 listenfd 上讀事件通知之后,需要把 listenfd 上所有的讀事件全部處理完,下次listenfd 上再有讀事件時,才會通知應(yīng)用層
回到 accept 的問題上,在垂直觸發(fā)模式下,當(dāng) epoll 通知應(yīng)用層 listenfd 上有可讀事件時,應(yīng)用層調(diào)用 accept, 由于此時進程打開的文件描述符數(shù)量已經(jīng)達到上限了,所以 accept 調(diào)用失敗
也即 listenfd 上的可讀事件還沒有處理,在應(yīng)用層處理完 listenfd 上可讀事件之前,epoll 不會再通知應(yīng)用層 listenfd 上有可讀事件
如果在應(yīng)用層處理完 listenfd 上可讀事件之前,有新的客戶端連接到來,這個時候 epoll 是不會通知應(yīng)用層 listenfd 上有可讀事件,這會導(dǎo)致一個嚴重的問題:accept 只要出現(xiàn)了 EMFILE的錯誤碼,就再也無法接受客戶端的連接了
所以,當(dāng)出現(xiàn) EMFILE 時,不管使用 epoll 的水平觸發(fā)模式還是垂直觸發(fā)模式都會存在問題
如何解決
EMFILE 表示進程打開的文件描述符數(shù)量達到上限了,可以把這個值調(diào)大些,但這治標(biāo)不治本
本來系統(tǒng)設(shè)置文件描述符數(shù)量上限是為了限制進程對系統(tǒng)資源的過度占用,況且,這個值調(diào)整到多大合適呢,總不能無限大吧,所以調(diào)整上限值的方式不是最合適的方式
accept 成功時會返回一個新的文件描述符,如果此時進程打開的文件描述符數(shù)量已經(jīng)達到上限了,就會返回失敗
假如此時能關(guān)閉一個空閑的文件描述符,讓出一個名額,再調(diào)用 accept 就會創(chuàng)建成功,這種方式具體的處理步驟如下:
1、事先準(zhǔn)備一個空閑的文件描述符 idlefd,相當(dāng)于先占一個"坑"位
2、調(diào)用 close 關(guān)閉 idlefd,關(guān)閉之后,進程就會獲得一個文件描述符名額
3、再次調(diào)用 accept 函數(shù), 此時就會返回新的文件描述符 clientfd, 立刻調(diào)用 close 函數(shù),關(guān)閉 clientfd
4、重新創(chuàng)建空閑文件描述符 idlefd,重新占領(lǐng) "坑" 位,再出現(xiàn)這種情況的時候又可以使用
由于測試代碼比較長,這里就不貼了,感興趣可以通過文末的方式獲取,下面是處理 EMFILE 的偽代碼:
- int ret = accept( listenfd, (struct sockaddr*)&addr, sizeof(addr) );
- if (-1 == ret)
- {
- if ( errno == EMFILE )
- {
- //關(guān)閉空閑文件描述符,釋放 "坑"位
- close(idlefd);
- //接受 clientfd
- clientfd = accept( listenfd, nullptr, nullptr);
- //關(guān)閉 clientfd,防止一直觸發(fā) listenfd 上的可讀事件
- close(clientfd);
- //重新占領(lǐng) "坑"位
- idlefd = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
- }
- }