# 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 ```