# 2025q1 Homework3 (kxo)
contributed by <`MikazukiHikari`>
## kxo
### 使用暫停/離開時的文字會有亂碼
:::danger
文字訊息不要用圖片展現
:::
> 已修正
```
|O| |O
-------
| |X|
-------
| | |
-------
| | |
-------
>~?Stopping to display the chess board
```
```
| | |
-------
| | |
-------
| |X|
-------
| | |
-------
~b?Stopping the kernel space tic-tac-toe game...
```
`read()` 從 `/dev/kxo` 讀回的資料是二進位 raw buffer,而 `printf("%s", display_buf)`需要的是一個以 null 結尾 (`\0`) 的字串。
沒有加 `\0` 時,`printf` 不知道在哪裡停止輸出導致造成印出亂碼!
因此使用 `display_buf[bytes_read] = '\0'`,手動補上結尾字元`\0` ,保證字串合法。`bytes_read` 是 `read()` 的回傳值,為實際讀到的位元組數。
```
O| |X|O
-------
O|X|X|
-------
| |X|
-------
|O| |
-------
Stopping the kernel space tic-tac-toe game...
```
> Commit [a2985ba](https://github.com/sysprog21/kxo/commit/a2985ba98d9440414107cba4bd5093b95e34f86c)
### 簡化重新執行 ./xo-user的流程
使用 `ctrl + Q`離開對局後,因為 `attr_obj.end` 變成 `1`,再次執行 `./xo-user` 將不會進入迴圈,必須重新安裝核心模組。因此在最後一次執行 `kxo_release` 時,將 `attr_obj.end` 改成 `0` ,從而使重新執行無需重新安裝核心模組。
### 畫面呈現在使用者層級
根據作業要求,將原本繪製在核心模式的 kxo 棋盤,改寫成畫面呈現的部分全在使用者層級,將`draw_board` 從 `main.c`移至`xo-user.c`,使生成棋盤在使用者層級執行,以此減少核心和使用者層級之間的通訊成本,同時也一樣能解決前面提到的[亂碼問題](https://hackmd.io/xSjXKJSaTMCG7jO26l7lCg?both#%E4%BD%BF%E7%94%A8%E6%9A%AB%E5%81%9C%E9%9B%A2%E9%96%8B%E6%99%82%E7%9A%84%E6%96%87%E5%AD%97%E6%9C%83%E6%9C%89%E4%BA%82%E7%A2%BC)。
我們可以在 `kxo_release` 最後加入下面這一行:
```c
pr_info("kxo: DRAWBUFFER_SIZE = %d, N_GRIDS = %d\n", DRAWBUFFER_SIZE, N_GRIDS);
```
可得知 `DRAWBUFFER_SIZE` 和 `N_GRIDS` 的大小:
```
[783187.823699] release, current cnt:0
[783187.823701] kxo: DRAWBUFFER_SIZE = 66, N_GRIDS = 16
```
`N_GRIDS`很好理解,就是 $4*4=16$ 大小的棋盤;`DRAWBUFFER_SIZE`則是一開始 $2$ 個換行符號 + 每行 $7$ 個字元(空白也算)以及 $1$ 個換行符號,共有 $8$ 行,等於$2+(7+1)*8=66$:
```
O| | |
-------
O| | |X
-------
O| | |X
-------
| | |
-------
```
因此,每次能減少 $66-16=50$ 個字元的通訊成本。
> Commit [6d65fb8](https://github.com/sysprog21/kxo/commit/6d65fb873c0ec4aa3083567ca4bcc487960df1ce)
:::danger
提供數學分析
:::
### 在螢幕顯示當下的時間 (含秒數),並持續更新
使用了 C 標準庫中的 `time()` 和 `localtime()` 函式來獲取當前時間,並將其格式化為「年-月-日 時:分:秒」的格式。將其加在`xo-user.c`原本的 `draw_board()` 函式中,使顯示時間和棋盤繪製一起完成。效果如下:
```
O| | |
-------
|X| |
-------
|X|O|
-------
O| | |
-------
Time: 2025-04-10 00:38:14
```
>Commit [4f28034](https://github.com/sysprog21/kxo/commit/4f28034838cd67f21cb76025593927488a9fc2c8)
### 顯示過去棋盤紀錄
>Commit [12ca43f](https://github.com/MikazukiHikari/kxo/commit/12ca43fb8de1937ea2713b40d93ed4cb7107dbee)
我將這份實作分成兩部分,分別是「從 kernel space 取得每一步的編號並記錄」以及「將紀錄傳到user space」
#### 取得每一步的編號並記錄
`main.c`中的`ai_one_work_func`和`ai_two_work_func`會將兩AI預測後下的步數賦值給`move`再放至陣列`table`,因此我們只要讀取`move`即可獲得每一步的編號。
接著我們新建`log.[ch]`來紀錄過去的棋盤,首先建立一個陣列當作紀錄步數的表格,假設是 $4*4$ 大小的棋盤,由於一次移動最多就 $4*4=16$ 種可能,因此 $4$ bits 便可記錄一步,一局遊戲最多動 $16$ 步,最終只要$4*16=64$ 個 bits 便可記錄一局。
```c
static uint64_t game_log[MAX_LOGS];
int moves[MAX_LOGS];
```
宣告陣列 `game_log` 來當作紀錄步數的表格,`MAX_LOGS` 代表最多能記錄多少局,預設為 `16`;`moves` 則用於紀錄每局移動的總步數。
在 `log.[ch]` 建立如下函式:
* `void log_init(void)`:初始化`game_log`、`moves`、當局紀錄、總局數和索引。
* `void log_board_update(int move)`:更新一個新的移動在當局紀錄,同時更新當局移動的總步數。
* `void log_append_board(void)`:將當局紀錄存至`game_log`,更新索引、總局數並初始化下個索引的`moves`和當局紀錄,由於我使用陣列,因此當總局數超過`MAX_LOGS`會用新的紀錄覆蓋舊的紀錄。並且,未完成的遊戲不會進入`game_log`中
* `uint64_t log_get_board(uint8_t index)`:取得第 `index` 個棋盤紀錄,`index = 0 ~ 15`。
* `uint8_t log_get_size(void)`:取得總局數。
* `uint8_t log_get_index(void)`:取得現在的索引。
* `uint64_t log_get_board_moves(uint8_t index)`:取得第 `index` 個棋盤紀錄的移動的總步數。
#### 將紀錄傳到使用者層級
這裡參考了 [rota1001](https://hackmd.io/@rota1001/linux2025-homework3#%E9%A1%AF%E7%A4%BA%E9%81%8E%E5%8E%BB%E6%A3%8B%E7%9B%A4%E7%B4%80%E9%8C%84) 大大的實作
使用 `ioctl` 作為核心模組與使用者層級交流的管道。設定了四種模式 `IOCTL_READ_SIZE` 、`IOCTL_READ_LIST`、`IOCTL_READ_INDEX` 和 `IOCTL_READ_LIST_MOVES`,但最後發現並不需要用到`IOCTL_READ_INDEX`模式。
透過對`cmd`做 bit operation 便可使用想要的模式並且攜帶`index`進去函式以取得對應索引的棋盤紀錄及該局移動的總步數。
* `IOCTL_READ_SIZE`:回傳總局數,並且可透過它取得「應該要顯示幾局(若總局數小於`MAX_LOGS-1`則顯示總局數;反之則顯示`MAX_LOGS-1`局)」以及「當前在`game_log`的索引」。
* `IOCTL_READ_LIST`:向使用者緩衝區寫入它要求的 `index` 對應的棋盤紀錄。
* `IOCTL_READ_INDEX`:當前在`game_log`的索引,但沒用到。
* `IOCTL_READ_LIST_MOVES`:向使用者緩衝區寫入它要求的 `index`對應當局的移動總步數。
```c
static long kxo_ioctl(struct file *flip, unsigned int cmd, unsigned long arg)
{
int ret;
switch (cmd & 3) {
case IOCTL_READ_SIZE:
ret = log_get_size();
pr_info("kxo_ioctl: the size is %d\n", ret);
break;
case IOCTL_READ_LIST:
uint64_t record = log_get_board(cmd >> 2);
ret = copy_to_user((void *) arg, &record, 8);
pr_info("kxo_ioctl: read list\n");
break;
case IOCTL_READ_INDEX:
ret = log_get_index();
pr_info("kxo_ioctl: the index is %d\n", ret);
break;
case IOCTL_READ_LIST_MOVES:
uint64_t record1 = log_get_board_moves(cmd >> 2);
ret = copy_to_user((void *) arg, &record1, 8);
pr_info("kxo_ioctl: read list_moves\n");
break;
default:
ret = -ENOTTY;
}
return ret;
}
```
:::danger
若不依賴 ioctl,能改用 sysfs 進行通訊嗎?
:::
之後,在 `xo-user.c` 使用前面註冊的 `ioctl()` 取得棋盤紀錄、總局數、索引和當局的移動總步數,由於我希望顯示局數是由新到舊,所以需要修改 `for` 迴圈。(假設當前索引是7,則顯示的棋盤紀錄和移動總步數的索引會是6,5,4...,0,15,14,...,8。第7局未完成,所以不會顯示)
最後按照下面的標準將編號 0~15 轉換成英文字母 (ABCD) + 數字 (1234) 的形式:
:::danger
注意書寫規範:中英文間用一個半形空白字元區隔
:::
```
1 | 0 4 8 12
2 | 1 5 9 13
3 | 2 6 10 14
4 | 3 7 11 15
--+--------------
A B C D
```
輸入 Ctrl+Q 之後出現的結果如下:
```
O| | |
-------
|X| |
-------
|X|O|
-------
O| | |
-------
Time: 2025-04-11 09:02:34
Stopping the kernel space tic-tac-toe game...
Moves: A4 -> B3 -> A2 -> A3 -> C2 -> B2 -> A1 -> C3
Moves: C3 -> C2 -> B2 -> B4 -> B3 -> C1 -> B1
Moves: A3 -> C2 -> A1 -> C3 -> A2
Moves: C3 -> D1 -> C2 -> D4 -> B2 -> B4 -> C4
Moves: A3 -> C2 -> A2 -> C3 -> A1
Moves: B3 -> C3 -> A4 -> C2 -> D4 -> B2 -> A2 -> C4
Moves: A4 -> B3 -> A1 -> C3 -> C4 -> B4 -> D1 -> B2
Moves: C3 -> A3 -> B2 -> B1 -> C2 -> A4 -> A2
Moves: C1 -> B4 -> B1 -> C4 -> A1
Moves: A1 -> B2 -> A3 -> A2 -> B1 -> C2
Moves: A2 -> C3 -> A1 -> A3 -> B1 -> C1 -> C2 -> B2
Moves: C3 -> D1 -> C2 -> B2 -> B3 -> C4 -> D3
Moves: B1 -> C4 -> A1 -> B4 -> C1
Moves: C3 -> B4 -> B3 -> C2 -> B2 -> C4 -> D4
Moves: C1 -> B4 -> B1 -> C4 -> A1
```