--- ###### tags: `linux2022` --- # epoll / file descripter contributed by < `eric88525` > 引述 epoll(2) > The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them. The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors. # Overview + 概念像是請人代為監控 fd 狀態,有需要時就直接拿取 ready 的 fd + epoll 代表 event poll,是 linux 的特殊結構 + 允許 process 監視多個 file descriptors ,並在 I/O 可以執行時得到提醒 (edge-triggered 和 level-triggered) + epoll 不是 system call,而是一種 kernel **資料結構** ![](https://i.imgur.com/HZLqNqA.png) # epoll 語法 ## 1. epoll create 建立 epoll instance,此 system call 回傳 epoll instance 的 file descriptior。 + 參數 + **size** : 希望 process 監視多少個 file descriptor。在 linux 2.6.8 之後取消,改為動態決定 size ```c #include <sys/epoll.h> int epoll_create(int size); ``` ![](https://i.imgur.com/OOQ9YPQ.jpg) 有令一個用法 `epoll_create1(int flags)`, flags 可為 `0` 或是 `EPOLL_CLOEXEC`,當為 `0` 就跟 epoll_create 一樣 ```c int epoll_create1(int flags); ``` 當 flags = `EPOLL_CLOEXEC`,被 fork 出去的 child process 會在 exec() 以前先關掉 epoll descriptor,讓 child process 不能用 epoll instance。 ## 2. epoll_ctl process 可以透過此函式來增加想觀察的 file descriptor 被註冊的 fd 稱做 `epoll set` 或是 `interest list` ![](https://i.imgur.com/BznWlzp.jpg) ready list 是 interest list 的子集合 ![](https://i.imgur.com/MDOciut.jpg) 實際用法 ```c #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` + 參數: + epfd: 透過 `epoll_create()` 建立的 file descriptor,用以識別 epoll instance + op: operation 有三種,分別是 + 註冊: `EPOLL_CTL_ADD` + 刪除: `EPOLL_CTL_DEL` + 修改: `EPOLL_CTL_MOD` + fd: 想加入到 intererst / ready list 的 file descriptor + event: 指向 `epoll_event` structure 的指標。 + `epoll_event` 包含 **events** 和 **data** + events 為一 bitmask,指出要觀察哪種 events,像是 (EPOLLIN, EPOLLOUT) [事件列表](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html)。 + data: epoll_data 型態,kernel 需要傳回的資料 ```c typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; ``` ![](https://i.imgur.com/R0s5K4h.jpg) ## 3. epoll_wait 透過 epoll_wait system call,thread 可以得知在 epoll set/interest set 內哪些 event 被觸發。 ```c #include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout); ``` | name | description | | -------- | -------- | | epfd | 透過 `epoll_create()` 建立的 file descriptor,用以識別 epoll instance | | evlist | epoll_event 的 array,執行完 epoll_wait 後會被填充,用來得知哪些 fd 在 ready list | | maxevents | length of evlist | | timeout | block 時間(ms) | **timeout** + 0: 檢查完成後就離開,不會block + -1: process 永遠的等待 (sleep),直到 epoll_wait 回傳 + $>0$: 有回傳或是時間數完才離開 **return value** + -1: 出事了 [error codes](https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html) + 0: fd 都不在 ready list + $>0$: 如果有只少一個 fd 在 ready list,returns the number of file descriptors ready for the requested I/O,接著就能檢查 evlist 來看哪些 fd 有事件發生。 # epoll 的陷阱 ## file descriptors (描述符) epoll 跟 fd 息息相關,因此需要先了解 fd process 透過 file descriptor 來與 i/o streams 有關聯。每個 process 都有自己的 fd table,有兩個欄位 **flag** 和 **pointer**,flag 只有一種選項 `close on exec`,這個後續提到。 ![](https://i.imgur.com/hC3IbxU.png) descriptor 可以透過 sysyem call 像是 open, pipe, socker 創建,或是透過 **fork**。 當 process exits 或是 close 都會**關閉 file descriptor**,還有一種情況: 標記為 `close on exec` 的 descriptor 在 fork 後,只讓 parent 使用 descriptor,child process 則關閉 descriptor。 Process b 由 a fork 而來,在 b exec() 以前 descriptor 就會被**標記 inactive 而無法使用。** ![](https://i.imgur.com/guFFB27.png) KERNEL 另有維護一個 open file table, 裡面記載所有被打開的 file (如果某檔案被兩個 process開,那就會有兩欄) ![](https://i.imgur.com/t3wPqKh.png) 當 process 被 fork 出來,descriptor 也會被複製並指向相同地方,如果更改其中一者的 offset 其他被複製出來的也會受影響 (他們是連動的)。 ![](https://i.imgur.com/TrYL4lU.png) ## innode table 介紹 innode 是 file system 的 data structure,裡面記載 file system object 的**物件資訊**。 資訊包含: + location: 資料儲存在哪個 block 或是 disk + file 和 directory 的屬性 + 額外的 metadata,像是 access time, owner, permissions... 每個存在於檔案系統的檔案都包含 **inode entry**,又稱作 **inode number**,用以指向檔案。 而 innode table 用來紀錄 inode number 和 inode structure 的對應。 下圖表示 Process A 在打開 `abc.txt` 後產生 **fd5**,Proces B 打開同一份檔案後產生 **fd10**,雖然他們在 open file table 指向不同地方,但最後指向同一份檔案。 ![](https://i.imgur.com/mRQ8c37.png) # epoll 核心 以下是 process A 打開兩個不同的檔案 ![](https://i.imgur.com/7dFU0kv.png) process A 呼叫 `epoll_create` 建立 epoll instance,**fd9** 作為 file descriptor。 ![](https://i.imgur.com/X6esq6N.png) 透過 `epoll_ctl` 新增要監視的 fd, **fd0** 新增到 interest list。 ![](https://i.imgur.com/bGftvmO.png) 如果此時 fork 出 Process B,B 繼承 A 的 fd table,就連 **fd9**都共享。 不論 Process A 新增啥到 interest list,Process B 都會收到提醒。 ![](https://i.imgur.com/tfOqZaF.png) 就算有 process 關閉了被 epoll 關注的檔案,他還是會收到提醒。 # 跟 select / poll 比較 select / poll 複雜度 $O(N)$,每當檢查時都會全掃一遍。假設是網站的話就要把所有 client 都檢查一次。 epoll 則只需要呼叫 `epoll_wait` ,拿到的就都是有 event 發生的 fd。 As a result, the cost of epoll is O(number of events that have occurred) and not O(number of descriptors being monitored) as was the case with select/poll. # level trigger 條件觸發 / edge trigger 邊緣觸發 + 條件觸發(滿足條件就產生 io事件) + 邊緣觸發(狀態變化時發生一個 io 事件) 預設 epoll 提供 **level-triggered notifications**,每當呼叫 `epoll_wait`,只回傳 ready list。就像下圖只回傳 [fd2, fd3]。 ![](https://i.imgur.com/SFzDBJJ.jpg) 但有時我們只想觀察某個 fb 的狀態,不管他是不是 ready,也就是想得到 **edge-triggered notifications**,此時我們能透過對 bitmask 做 or 運算來關注。 ```c function Poller:register(fd, r, w) local ev = self.ev[0] ev.events = bit.bor(C.EPOLLET, C.EPOLLERR, C.EPOLLHUP) if r then ev.events = bit.bor(ev.events, C.EPOLLIN) end if w then ev.events = bit.bor(ev.events, C.EPOLLOUT) end ev.data.u64 = fd local rc = C.epoll_ctl(self.fd, C.EPOLL_CTL_ADD, fd, ev) if rc < 0 then errors.get(rc):abort() end end ``` + `edge trigger` vs `level trigger`: + level trigger: 專注在**條件(ready)**,只要 fd ready ,就會**一直**提醒你。 + edge trigger: 只要有**狀態變化**才會通知你**一次**。 > 資料來源 [The method to epoll’s madness](https://copyconstruct.medium.com/the-method-to-epolls-madness-d9d2d6378642) [边缘触发(Edge Trigger)和条件触发(Level Trigger)](https://blog.csdn.net/josunna/article/details/6269235) [深入理解 Linux 的 epoll 機制](https://www.readfog.com/a/1641834490361909248)