owned this note
owned this note
Published
Linked with GitHub
# 2025q1 Homework3 (kxo)
contributed by < `vicLin8712` >
## 問題紀錄
### Kxo `xo-user.c` 檔案分析
此檔案示範如何在 userspace 端與 kernel space 進行通訊,此檔案為 user space 端的程式碼。
```C
#define XO_STATUS_FILE "/sys/module/kxo/initstate"
static bool status_check(void)
{
FILE *fp = fopen(XO_STATUS_FILE, "r");
if (!fp) {
printf("kxo status : not loaded\n");
return false;
}
char read_buf[20];
fgets(read_buf, 20, fp);
read_buf[strcspn(read_buf, "\n")] = 0;
if (strcmp("live", read_buf)) {
printf("kxo status : %s\n", read_buf);
fclose(fp);
return false;
}
fclose(fp);
return true;
}
```
`status_check` 用於檢測對應模組是否掛載。
`FILE` 結構體屬於 `<stdio.h>` ,此宣告意即將 `fp` 指向 `XO_STATUS_FILE`,並且僅可讀。
注意到其指向 `/sys/module/kxo/initstate`,為何不是 `dev` 等檔案系統?
`/sys` 檔案系統主要用於暴露檔案資訊給存取者,而 `/dev` 用於存取硬體資訊,故而放置在 `/sys` 內部。另一個問題是裝置狀態儲存位置需要額外在核心模組寫入嗎?
查看`main.c:492 ret = device_create_file(kxo_dev, &dev_attr_kxo_state);` 發現其利用`linux devices` 所提供的 `device_create_file`,建立一個於 `sys` 的檔案用於儲存 `kxo_dev` 的狀態,當然前面需要先建立 `kxo_dev` 的物件(後續會討論到)。
`fget` 用於讀取 `fp` 的20個字元,注意到 C11 標準內提到 `fget` 僅會讀取到換行符號(或檔案結束)為止,並於最後保留 `\0`,也就是僅會讀取最多19個 bytes。問題是究竟 `fget` 所獲得的 bytes 會有何種形式?回顧到 `fp` 的創建,需回去查看核心模組的原始碼,後續再追蹤,在此先認為他會回傳某些用於判斷模組正常與否的資料。
`strcspn` 用於取出兩者字串一樣的位置,在此例也就是 `read_buf` 出現 `\n` 的位置,將其設置為 0 避免掉換行。
```c
static struct termios orig_termios;
static void raw_mode_disable(void)
{
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
static void raw_mode_enable(void)
{
tcgetattr(STDIN_FILENO, &orig_termios);
atexit(raw_mode_disable);
struct termios raw = orig_termios;
raw.c_iflag &= ~IXON;
raw.c_lflag &= ~(ECHO | ICANON);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
```
`termios` 查詢此結構體的使用,可參考 [termios(3) — Linux manual page](https://www.man7.org/linux/man-pages/man3/termios.3.html) ,此標頭檔提供用於控制終端機,terminal,所需的基本操作。
終端機在此專案中用以呈現下棋步驟,其中步驟由核心模組的演算法所決定。先查看 `raw_mode_enable` ,`tcgetattr` 取得 `STDIN_FILENO` 所對應的檔案描述符 (STDIN_FILENO 實際上 = 0),並將其賦予後者 `orig_termios`。
#### 主要目的
使用者端建立輸出列印機制,在終端機顯現出來自 kernel module 所提供的資訊,以及接收來自鍵盤輸入的相關命令。第一個問題是該如何建立一個機制,使得持續列印出 kernel 所提供的資訊,同時亦監聽鍵盤輸入,若有輸入特定命令則及時響應。
```c
FD_ZERO(&readset);
FD_SET(STDIN_FILENO, &readset);
FD_SET(device_fd, &readset);
```
上方函式利用 `fcntl` 函式庫功能將 kernel 輸入與鍵盤輸入添加至讀取集合 `readset` 中。
```c
int result = select(max_fd + 1, &readset, NULL, NULL, NULL);
if (result < 0) {
printf("Error with select system call\n");
exit(1);
}
```
`result` 設為執行 `select` 的結果,可查詢 s[elect(2) — Linux manual page](https://man7.org/linux/man-pages/man2/select.2.html),此函式目的在於等待 `readset` 內 `fd` 有 I/O 事件發生,則回傳給 `result`,下方為避免錯誤發生所添加的判斷式。
```C
if (FD_ISSET(STDIN_FILENO, &readset)) {
FD_CLR(STDIN_FILENO, &readset);
listen_keyboard_handler();
} else if (read_attr && FD_ISSET(device_fd, &readset)) {
FD_CLR(device_fd, &readset);
printf("\033[H\033[J"); /* ASCII escape code to clear the screen */
read(device_fd, display_buf, DRAWBUFFER_SIZE);
display_buf[DRAWBUFFER_SIZE - 1] = '\0';
printf("%s", display_buf);
}
```
上方函式判斷究竟是哪個 `fd` 發生 I/O 事件,使用 `FD_ISSET` 巨集,以第一個條件判斷式來說,即為 `STDIN_FILENO` 發生 I/O,因此進行 `listen_keyboard_handler()` 函式用以處理輸入的字串判斷後續相對應動作。 `else if` 則用以判斷 kernel I/O 所做的相對應處理。
結束後需要回復原先終端機設定,並關閉 kernel 的 `fd`,如下。
```c
raw_mode_disable();
fcntl(STDIN_FILENO, F_SETFL, flags);
close(device_fd);
```
### Kxo `main.c` 檔案分析
此為 kernel module 檔案,因此先由 `init` 部分觀察,再延伸至後續所引用到的程式碼。
`dev_t` 為 Linux Kernel 用於管理裝置識別碼的一個結構。
```c
dev_t dev_id;
```
接下來要在 kernel space 嘗試分配記憶體,如下方操作
```c
if (kfifo_alloc(&rx_fifo, PAGE_SIZE, GFP_KERNEL) < 0)
return -ENOMEM;
/* Register major/minor numbers */
ret = alloc_chrdev_region(&dev_id, 0, NR_KMLDRV, DEV_NAME);
if (ret)
goto error_alloc;
major = MAJOR(dev_id);
```
`kfifo_alloc()` 將記憶體分配至 `rx_fifo` 指標所指向的地址,同時賦予 `PAGE_SIZE` 大小的記憶體,可用 `getconf PAGE_SIZE` 查詢當前 kernel 的 page size。`GFP_KERNEL` ,get free page 為 kfifo 的 flag,可於 [kfifo](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html) 查詢。同時文中也提及宣告 `kfifo` 指標的方式,即為利用 API `DECLARE_KFIFO_PTR`。
`alloc_chrdev_region` 為 [Linux Kernel API](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html#c.alloc_chrdev_region),其功能原文為 register a range of char device numbers ,即可向核心註冊所需要的裝置編號範圍。
註冊編號後,接下來要將裝置綁定相對應的操作,如下程式碼
```c
cdev_init(&kxo_cdev, &kxo_fops);
ret = cdev_add(&kxo_cdev, dev_id, NR_KMLDRV);
```
`cdev_init` 可在 [Linux Kernel API - List Management Functions](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html#c.alloc_chrdev_region) 查詢,其將 `cdev` 結構初始化,並與 `kxo_fops` 操作進行綁定。
接下來將先前向核心註冊的裝置編號與 `kxo_cdev` 進行綁定。
註冊完裝置編號與綁定相關驅動操作後,接下來就可以向檔案系統創建檔案供 user space 操作,參考如下程式碼
```c
struct device *kxo_dev =
device_create(kxo_class, NULL, MKDEV(major, 0), NULL, DEV_NAME);
ret = device_create_file(kxo_dev, &dev_attr_kxo_state);
```
裝置的結構與相關操作可以在 [Device drivers infrastructure](https://www.kernel.org/doc/html/latest/driver-api/infrastructure.html) 查閱,第一個結構 `kxo_dev` 利用 `device_create` 建立此類別的裝置,後續使用 `device_create_file` 建立個別裝置檔。
下一步建立 buffer 以供裝置檔案使用,利用 `fast_buf.buf = vmalloc(PAGE_SIZE)`
:::info
`vmalloc` 所建立的記憶體空間有何特性?
:::
下行程式碼將棋盤填入空白,也就是初始化之意思。
```c
memset(table, ' ', N_GRIDS);
```
下一步對此棋盤屬性進行宣告,如下
```c
attr_obj.display = '1';
attr_obj.resume = '1';
attr_obj.end = '0';
rwlock_init(&attr_obj.lock);
```
`attr_obj` 定義有四個屬性,接下來著重探討 `rwlock` 。
#### timer_handler
此程式碼模擬固定時間產生 hard_irq 訊號給 CPU,並進行 soft_irq 做後續處理。而 `timer_handler` 則用於控制時間計時用以當作發出 hard_irq 訊號的依據。
整體的運作流程:
1. 關閉 cpu irq
2. 開始計時
3. 檢查勝利條件
4. 結束計時
5. 開啟 cpu irq
延伸 3. 檢查勝利條件,也就是可以分為有結果與尚未有結果,這邊觀察尚未有結果的程式碼。
在並行處理時,需先鎖住當前棋盤狀態,僅供狀態讀取
```c
read_lock(&attr_obj.lock);
```
接下來進行 kernel 端的棋盤規劃,當棋盤對奕尚未結束,則進行 `ai_game()`
##### ai_game
`ai_game()` 為人工智慧演算法執行的函式,其需在 `irq_disable` 的條件之下利用 `tasklet_schedule` 將 `game_tasklet` 進行排程。接下來探討 `game_tasklet` 函式。
由 `static DECLARE_TASKLET_OLD(game_tasklet, game_tasklet_func);` 巨集宣告 `game_tasklet`,探討 `game_tasklet_func`,分析程式碼如下
```c
READ_ONCE(finish);
READ_ONCE(turn);
smp_rmb();
if (finish && turn == 'O') {
WRITE_ONCE(finish, 0);
smp_wmb();
queue_work(kxo_workqueue, &ai_one_work);
} else if (finish && turn == 'X') {
WRITE_ONCE(finish, 0);
smp_wmb();
queue_work(kxo_workqueue, &ai_two_work);
}
queue_work(kxo_workqueue, &drawboard_work);
```
`READ_ONCE` 與 `smp_rmb` 規定了操作順序,`READ_ONCE` 避免了編譯器優化作出的打亂排序,`smp_rmb` 確保了所有 read 操作須在此函式前完成。
後續針對不同的 `turn` ,呼叫對應的 `ai_worker` 排入 `work_queue` 中等待演算,並於最後將 `drawboard_work` 排入排程。
接下來查看 `ai_worker` 程式碼
```c
mutex_lock(&producer_lock);
int move;
WRITE_ONCE(move, mcts(table, 'O'));
smp_mb();
if (move != -1)
WRITE_ONCE(table[move], 'O');
WRITE_ONCE(turn, 'X');
WRITE_ONCE(finish, 1);
smp_wmb();
mutex_unlock(&producer_lock);
```
上方程式碼先將 `producer` 鎖住,避免多下棋者要存取修改當前的棋盤資訊,再將 `mcts` 演算法計算出下一個 `move`,最後再修改棋盤 flag 並解鎖。
## 減少通訊成本
### 棋盤移植至 user space
原先機制會在 kernel space,透過函式 `draw_board` 繪製,並儲存在 `draw_buffer` 字元陣列中。也因此在繪製新棋盤時需要利用 mutex lock 鎖住,而後再藉由 `kfifo_in` 函式將 `draw_buff` 資料存入 circular buffer 之中,後續再由 user space 處理 circular buffer 資訊。
首先先將繪製棋盤的函式 `draw_board` 移至到 user space 中,在 `xo-user.c` 檔中,原先需讀取全部資訊,包含棋盤,落子,而更改後,僅需讀取落子順序,再搭配原先在 kernel space 移至 user space 的棋盤繪製函數,即可繪製出下棋過程
>[commit 12b41f0](https://github.com/vicLin8712/kxo/commit/12b41f0792d095cb68216ca37eba13d65b0f4a40)
## 並行處理