# 網絡,第 7 部分:非阻塞 I O,select()和 epoll
> 原文:<https://github.com/angrave/SystemProgramming/wiki/File-System%2C-Part-7%3A-Scalable-and-Reliable-Filesystems>
### 不要浪費時間等待
通常,當您調用`read()`時,如果數據不可用,它將等到數據準備就緒,然后函數返回。當您從磁盤讀取數據時,該延遲可能不會很長,但是當您從慢速網絡連接讀取時,如果數據到達,則可能需要很長時間才能到達該數據。
POSIX 允許您在文件描述符上設置一個標志,以便對該文件描述符的`read()`的任何調用都將立即返回,無論它是否已完成。使用此模式下的文件描述符,您對`read()`的調用將啟動讀取操作,當它正在工作時,您可以執行其他有用的工作。這稱為“非阻塞”模式,因為對`read()`的調用不會阻止。
要將文件描述符設置為非阻塞:
```c
// fd is my file descriptor
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
```
對于套接字,可以通過將`SOCK_NONBLOCK`添加到`socket()`的第二個參數,在非阻塞模式下創建它:
```c
fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
```
當文件處于非阻塞模式并且您調用`read()`時,它將立即返回任何可用的字節。假設已從套接字另一端的服務器到達 100 個字節,并調用`read(fd, buf, 150)`。 Read 將立即返回值 100,這意味著它會讀取您要求的 150 個字節中的 100 個。假設您嘗試通過調用`read(fd, buf+100, 50)`來讀取剩余數據,但最后 50 個字節仍未到達。 `read()`將返回-1 并將全局錯誤變量 **errno** 設置為 EAGAIN 或 EWOULDBLOCK。這是系統告訴你數據尚未準備好的方式。
`write()`也適用于非阻塞模式。假設您要使用套接字將 40,000 個字節發送到遠程服務器。系統一次只能發送這么多字節。通用系統一次可以發送大約 23,000 個字節。在非阻塞模式下,`write(fd, buf, 40000)`將返回它能夠立即發送的字節數,或大約 23,000。如果你再次調用`write()`,它將返回-1 并將 errno 設置為 EAGAIN 或 EWOULDBLOCK。這是系統告訴你它仍然忙于發送最后一塊數據的方式,并且尚未準備好發送更多數據。
### 如何檢查 I / O 何時完成?
有幾種方法。讓我們看看如何使用 _ 選擇 _ 和 _epoll_ 來做到這一點。
#### 選擇
```c
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
```
給定三組文件描述符,`select()`將等待任何這些文件描述符變為“就緒”。
* `readfds` - 當有數據可以讀取或達到 EOF 時,`readfds`中的文件描述符就緒。
* `writefds` - 當對 write()的調用成功時,`writefds`中的文件描述符就緒。
* `exceptfds` - 系統特定的,沒有明確定義。只需為此傳遞 NULL。
`select()`返回準備好的文件描述符總數。如果它們在 _ 超時 _ 定義的時間內沒有準備就緒,它將返回 0.在`select()`返回后,調用者需要遍歷 readfds 和/或 writefds 中的文件描述符以查看哪些文件描述符準備好了。由于 readfds 和 writefds 同時充當輸入和輸出參數,當`select()`指示存在準備好的文件描述符時,它將覆蓋它們以僅反映??準備好的文件描述符。除非調用者只打算調用`select()`一次,否則在調用它之前保存 readfds 和 writefds 的副本是個好主意。
```c
fd_set readfds, writefds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
for (int i=0; i < read_fd_count; i++)
FD_SET(my_read_fds[i], &readfds);
for (int i=0; i < write_fd_count; i++)
FD_SET(my_write_fds[i], &writefds);
struct timeval timeout;
timeout.tv_sec = 3;
timeout.tv_usec = 0;
int num_ready = select(FD_SETSIZE, &readfds, &writefds, NULL, &timeout);
if (num_ready < 0) {
perror("error in select()");
} else if (num_ready == 0) {
printf("timeout\n");
} else {
for (int i=0; i < read_fd_count; i++)
if (FD_ISSET(my_read_fds[i], &readfds))
printf("fd %d is ready for reading\n", my_read_fds[i]);
for (int i=0; i < write_fd_count; i++)
if (FD_ISSET(my_write_fds[i], &writefds))
printf("fd %d is ready for writing\n", my_write_fds[i]);
}
```
[有關 select()](http://pubs.opengroup.org/onlinepubs/9699919799/functions/select.html)的更多信息
## epoll 的
_epoll_ 不是 POSIX 的一部分,但它受 Linux 支持。這是一種等待許多文件描述符的更有效方法。它會告訴你準確的描述符。它甚至為您提供了一種方法,可以使用每個描述符存儲少量數據,如數組索引或指針,從而可以更輕松地訪問與該描述符關聯的數據。
要使用 epoll,首先必須使用 [epoll_create()](http://linux.die.net/man/2/epoll_create)創建一個特殊的文件描述符。您不會讀取或寫入此文件描述符;你只需將它傳遞給其他 epoll_xxx 函數并在結尾處調用 close()。
```c
epfd = epoll_create(1);
```
對于要使用 epoll 監視的每個文件描述符,您需要使用 [epoll_ctl()](http://linux.die.net/man/2/epoll_ctl)和`EPOLL_CTL_ADD`選項將其添加到 epoll 數據結構中。您可以向其添加任意數量的文件描述符。
```c
struct epoll_event event;
event.events = EPOLLOUT; // EPOLLIN==read, EPOLLOUT==write
event.data.ptr = mypointer;
epoll_ctl(epfd, EPOLL_CTL_ADD, mypointer->fd, &event)
```
要等待某些文件描述符準備就緒,請使用 [epoll_wait()](http://linux.die.net/man/2/epoll_wait)。它填充的 epoll_event 結構將包含您在添加此文件描述符時在 event.data 中提供的數據。這使您可以輕松查找與此文件描述符關聯的自己的數據。
```c
int num_ready = epoll_wait(epfd, &event, 1, timeout_milliseconds);
if (num_ready > 0) {
MyData *mypointer = (MyData*) event.data.ptr;
printf("ready to write on %d\n", mypointer->fd);
}
```
假設您正在等待將數據寫入文件描述符,但現在您要等待從中讀取數據。只需將`epoll_ctl()`與`EPOLL_CTL_MOD`選項一起使用即可更改您正在監控的操作類型。
```c
event.events = EPOLLOUT;
event.data.ptr = mypointer;
epoll_ctl(epfd, EPOLL_CTL_MOD, mypointer->fd, &event);
```
要從 epoll 中取消訂閱一個文件描述符,同時保留其他文件描述符,請將`epoll_ctl()`與`EPOLL_CTL_DEL`選項一起使用。
```c
epoll_ctl(epfd, EPOLL_CTL_DEL, mypointer->fd, NULL);
```
要關閉 epoll 實例,請關閉其文件描述符。
```c
close(epfd);
```
除了非阻塞`read()`和`write()`之外,對非阻塞套接字的`connect()`的任何調用也將是非阻塞的。要等待連接完成,請使用`select()`或 epoll 等待套接字可寫。
## 有趣的 Blogpost 關于邊緣情況與選擇
[https://idea.popcount.org/2017-01-06-select-is-fundamentally-broken/](https://idea.popcount.org/2017-01-06-select-is-fundamentally-broken/)
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話