---
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 也將相應的被喚醒。