--- tags: linux-summer-2021 --- # 2021q3 Homework3 (vpoll) contributed by < `RinHizakura` > > [第 4 週測驗題](https://hackmd.io/@sysprog/linux2021-summer-quiz4) ## vpoll ## 實作 ### 資料結構 ```cpp struct vpoll_data { wait_queue_head_t wqh; __poll_t events; }; ``` 此結構會被放在檔案開啟(`open`)時建立且初始化,並被置於 `struct file` 中的 `private_data` 之下(`private_data` 方便 driver 在操作檔案時可以攜帶指標作為參數),且在關閉檔案時釋放。 * `wait_queue_head_t`: wait queue 結構,與 poll 的相關操作相關 * `__poll_t`: poll 需要回傳的 bitmask,表示可以進行的 file operation ### `vpoll_init` 基本的初始化與 [quiz1](https://hackmd.io/@RinHizakura/SJruJZF0d) 類似就不再次討論,僅紀錄一下幾個不同的地方: * [`IS_ERR`](https://elixir.bootlin.com/linux/v5.14-rc5/source/include/linux/err.h#L34) 用來確保 kernel 中的回傳指標是合法的物件而非錯誤,這是因為函式的返回值可能是配置的地址或者 error code,如下程式碼,地址的 0xFFFFF000 到 0xFFFFFFFF 範圍間被保留以編碼錯誤 ```cpp #define MAX_ERRNO 4095 #define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO) static inline bool __must_check IS_ERR(__force const void *ptr) { return IS_ERR_VALUE((unsigned long)ptr); } ``` * 配合 [`PTR_ERR`](https://elixir.bootlin.com/linux/v5.14-rc5/source/include/linux/err.h#L29) 則可以將指標轉型成 error code * `class_create` 建立出的結構下之 [`devnode`](https://elixir.bootlin.com/linux/v5.14-rc5/source/include/linux/device/class.h#L63) 被替換掉,後者是 *Callback to provide the devtmpfs*,用來設定該 device file 的存取權限 ### `vpoll_poll` ```cpp static __poll_t vpoll_poll(struct file *file, struct poll_table_struct *wait) { struct vpoll_data *vpoll_data = file->private_data; poll_wait(file, &vpoll_data->wqh, wait); return READ_ONCE(vpoll_data->events); } ``` 此函式對應對 user-space program 對 /dev/vpoll 進行的 poll/select/epoll 操作(在 `epoll_wait()` / `epoll_ctl()` 有機會呼叫),主要的處理步驟通常包含: 1. 對 wait queues 呼叫 `poll_wait` 以將 wait queue 註冊到 poll table 2. 回傳一個 bitmask 表示目前所可以進行的 non-blocking 檔案操作 通過呼叫 `poll_wait` 將 wait queue 加入到 poll table `wait` 之中,以使 `vpoll` 中的 wait queue 可以並註冊至系統中。則當 `vpoll` 中有事件發生時,且 wait queue 有任務在等待時 (如呼叫 `epoll_wait` ),便能主動喚醒 `vpoll` 的 wait queue 之任務來執行。最後回傳 `events` bitmask 以告知 user process 已經準備好可以立即進行的操作。 > [Linux Device Drivers: 6.3. poll and select](http://www.makelinux.net/ldd3/?u=chp-6-sect-3.shtml) ### `vpoll_ioctl` ```cpp static long vpoll_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct vpoll_data *vpoll_data = file->private_data; __poll_t events = arg & EPOLLALLMASK; long res = 0; spin_lock_irq(&vpoll_data->wqh.lock); switch (cmd) { case VPOLL_IO_ADDEVENTS: vpoll_data->events |= events; break; case VPOLL_IO_DELEVENTS: vpoll_data->events &= ~events; break; default: res = -EINVAL; } if (res >= 0) { res = vpoll_data->events; if (waitqueue_active(&vpoll_data->wqh)) wake_up_locked_poll(&vpoll_data->wqh, vpoll_data->events); } spin_unlock_irq(&vpoll_data->wqh.lock); return res; } ``` 對應向 /dev/vpoll 進行的 ioctl 操作。其行為是透過 [`ioctl`](https://man7.org/linux/man-pages/man2/ioctl.2.html) 的 request number (`cmd`),從 userspace 向 module 設定 poll 的 mask 以模擬 poll 操作。 如果設定成功 (`res >= 0`),需額外判斷 wait queue 是否為空 (`waitqueue_active`),如果非空,則需要主動使用 `wake_up_locked_poll` 將 wait queue 上其他 poll 事件相關的 thread 喚醒。 :::danger 換成其他的 `wake_up_*` 會導致系統 stucking,為甚麼必須是 `wake_up_locked_poll` 呢? ::: ### userspace program ```cpp int main(int argc, char *argv[]) { struct epoll_event ev = { .events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLOUT | EPOLLHUP | EPOLLPRI, .data.u64 = 0, }; int efd = open("/dev/vpoll", O_RDWR | O_CLOEXEC); if (efd == -1) handle_error("/dev/vpoll"); int epollfd = epoll_create1(EPOLL_CLOEXEC); if (efd == -1) handle_error("epoll_create1"); if (epoll_ctl(epollfd, EPOLL_CTL_ADD, efd, &ev) == -1) handle_error("epoll_ctl"); ``` 在 [`struct epoll_event`](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) 中設定要監聽的事件,包含: * `EPOLLIN` : The associated file is available for read(2) operations * `EPOLLRDHUP`: * `EPOLLERR`: Error condition happened on the associated file descriptor. This event is also reported for the write end of a pipe when the read end has been closed. * epoll_wait(2) will always report for this event; * `EPOLLOUT`: The associated file is available for write(2) operations. * `EPOLLHUP`: Hang up happened on the associated file descriptor. * `EPOLLPRI`: There is an exceptional condition on the file descriptor. 接著透過 `open` 開啟 vpoll 之 device file: * `O_CLOEXEC` 使得 file descriptor 會在 exec 之後被自動關閉 [`epoll_create1`](https://man7.org/linux/man-pages/man2/epoll_create.2.html) 建立一個 epoll instance,由 [`epoll_ctl`](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) 將要監聽的 file descriptor 與目標事件進行設定 (`EPOLL_CTL_ADD`)。最終,使用者就可以藉由 [`epoll_wait`](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) 等待 file descriptor 上事件的發生。 最後,藉由 fork 建立的 child process 會使用 `ioctl` 去添加 `vpoll` 中用來模擬 poll 的 event mask,而 parent process 則 `epoll_wait` 等待 event mask 被設定後模擬某一事件的發生,且再藉由 `ioctl` 去將 `vpoll` 中的該 event mask 刪除。 ## Memory Order > [memory-barriers.txt: SLEEP AND WAKE-UP FUNCTIONS](https://github.com/torvalds/linux/blob/master/Documentation/memory-barriers.txt) 對於全域的事件 flag 與其任務的 wake 或 sleep 狀態需要考量 memory order 議題,因為其牽涉到兩個資料的交互:正在等待 event 的任務之狀態,以及用於表示事件發生的 global flag。需要保證對這些資料的存取是按照預期的順序發生,在 Linux 中可以藉由特定的 API 來隱式的插入 memory barrier,得到此保證 (例如本例中用到的 `wake_up_*`)。 如下展示了一個 sleeper task(task A): 其等待一個變數 `event_indicated` 為 true,然後退出。否則繼續 `schedule()` 使其他任務先行被排程。 ```cpp for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); } ``` 而另一個 waker task (task B),則設定變數 `event_indicated` 為 true,然後喚醒該 sleeper task ```cpp event_indicated = true; wake_up(&event_wait_queue); ``` task A 的邏輯是 1. 設定自己的狀態為 `TASK_UNINTERRUPTIBLE` 2. 判斷 `event_indicated` 是否滿足條件 task B 的邏輯則是: 1. 無條件設定 `event_indicated` 2. 嘗試喚醒 task A,如果 task A 不是 `TASK_UNINTERRUPTIBLE` 則表示其已經醒來因此不必再喚醒之 我們想達到的目的是: 當 `event_indicated` 為 true 時,task A 必然是被喚醒的。但如果執行的順序被重排,可能發生被喚醒的 task A 看見 `event_indicated` 被設定為 false,同時 task B 看見 task A 的狀態不是 `TASK_UNINTERRUPTIBLE`,導致 task B 將 `event_indicated` 設定為 true 卻沒有去喚醒 task A 的問題。為此,`set_current_state` 和 `wake_up` 中會插入 memory barrier,以保證執行的順序。 ``` CPU 1 (Sleeper) CPU 2 (Waker) =============================== =============================== set_current_state(); STORE event_indicated smp_store_mb(); wake_up(); STORE current->state ... <general barrier> <general barrier> LOAD event_indicated if ((LOAD task->state) & TASK_NORMAL) STORE task->state ``` 藉由兩個 memory barrier, task A 將狀態設為 `TASK_UNINTERRUPTIBLE` 或 task B 將 `event_indicated` 設為 true 至少有一是可以被另一方看見的。可以保證一旦 `event_indicated` 設為 true,task A 也將相應的被喚醒。