contributed by <PigeonSir>
注意書寫規範!
ai 演算法 : mcts.c, negamax.c
kxo module : main.c
user-mode 觀看棋局 : xo-user.c
遊戲規則 & 勝利判定 : game.c
一開始編譯 kxo 時發生錯誤,其原因為 zobrist.c 中的 u128 未定義,暫時透過新增定義解決
但詢問 chatgpt 發現該操作存在風險,因為並非所有架構都支援 __uint128_t,查看該函式的程式碼
說好的進度呢?
要可以同時觀看多場對局,代表要有很多個 AI1 和 AI2,並要透過鍵盤觸發 signal 來切換對局、暫停和停止,參考 coro 的程式碼發現 setjump/longjump 僅能在 user space 使用,所以我的計畫是在 kernel mode 中運用現有的架構延伸多棋局的功能,並且在 user space 運用 corutine 支援多使用者同時觀看不同的棋局。
timer_setup(&timer, timer_handler, 0) → 沒有啟動 timer(尚未進入排程)mod_timer(&timer, jiffies + delay) 啟動第一個 tick 並等待 delay 後觸發 timer_handler()tasklet_schedule(&game_tasklet) 轉交排程器來決定誰是下一個落子先以確保每個棋盤可以獨立運算為目標,不排程畫圖的相關任務,中間有經過一些修改,以下為修改過的流程
為了存放多場棋局的資料並且傳送特定棋局的參數至愈排程的函式,新增結構體 kxo_task
tasklet 和 workqueue 都無法直接傳遞額外參數。它們只能接受固定型別的 callback 函式:
實作時,需要在 kxo_task 中定義 tasklet_struct 和 work_struct 的結構體,並且在模組初始化階段定義執行的函式
在函式被執行後用 cotainer_of() 或是透過指標取得 kxo_task 棋盤資訊
按照上述策略修改程式碼後會觸發以下錯誤
觸發的時間點是在 ai_one_work_func 呼叫 get_cpu 並且使用 mutex 後,推測是因為調用了多場棋局但全部共用一個 mutex lock 導致的,所以試著先改成每個棋盤一個 mutex
改完之後就不再有該問題了,但是發現電腦會變很卡,而且每一場棋局都只有 ai O 在下棋
commmit 70162cf
結果沒什麼大不了,只是我在 game_tasklet_func 中決定排程時條件判斷的變數忘了改。
原本以為我已經排除了,結果只是因為我把每個棋盤的 mutex lock 分開後只在每個棋盤排程 ai O 所以沒有觸發等待,把 ai X 加入後該問題又跟著回來了,即便我只使用一個棋盤還是會發生,而且當我把兩個 ai 任務的 mutex_lock 都註解掉時,這個錯誤又不發生了,之後可以考慮不要使用 mutex,但目前先研究一下觸發錯誤的原因
觀察 get_cpu() 的程式碼
Keeping Kernel Code Preempt-Safe
commit f71ff80
目前的作法在任一個棋盤(cdev)被 open 就會全部的棋局都開始運算,接下來改成只有被開啟的棋局需要被排程。
先在 kxo_task 中新增 bool playing 來識別該棋局有無被開啟
在 kxo_open 中透過 inode 來得知現在被開啟的棋盤編號
最後在 timer_handler 中只排程有被開啟的棋局
前面只執行了多棋局的運算,接下來在核心層級把在畫圖加回來,並且要可以支援多棋局觀看
game_tasklet_func 每下一子就排程
draw_board_work_functiondraw_board_work_function 搬運資料
draw_board() 把棋盤資料從 table 搬到 draw_buffer,並加入棋盤格和換行produce_board() 把 draw_buffer 的資料寫進 kfifotimer_handler 中若是棋局結束也會執行一遍kfifo_to_user() 傳遞 rx_fifo 內的資料給使用者層級Commit 2eab22c
在結構體內新增傳送棋盤資訊用的參數
剩下就是根據執行時的 id 維護對應的棋盤資訊來達成顯示多個棋局的功能
實測透過 cat /dev/kxoN 可以正確的顯示棋盤資訊,但明明沒新的落子卻一直畫新的圖,推估是因為 ai 的計算時間較長,同一個 tasklet 會排程一個 ai 和一個畫圖的 work_struct 進 workqueue,但兩者執行時間落差過大,導致一次 ai 下一個子的同時畫圖的任務已經執行超過一次了,雖然使用 xo-user 觀看棋局時會避免重複繪製棋盤,但這樣的溝通方式還是有改善空間,接下來透過簡化傳送的資料以及把畫圖移至使用者層級來改善。
commit b75b3d6
原本的作法是紀錄並傳送整個棋盤,改為只傳送當前的落子位置,由使用者執行紀錄棋盤與畫圖,但是勝負判定還是要在核心模組內進行,所以核心模組還是需要透過 table 紀錄棋盤資訊
先把 produce_board 和 draw_board 移到 xo-user.c,並在 ai 運算完後直接把結果放到 kfifo 裡面,在一個 unsigned short 裡面紀錄換誰、棋盤編號和落子位置
| win | turn | game ID | move |
|---|---|---|---|
| 1 bit | 1 bit | 6 bit | 1 byte |
win: 為 1 時代表有人贏了
turn: 'X' -> 1 'O' -> 0
修改 main.c 和 xo-user.c 並觀察輸出,發現會重複傳送資料以及出現錯誤的 game id
實際印出 ai 計算完放到 kfifo 中的值發現跟預先規劃的配置不同
發現原因是我在把 ai 算出來的 move 轉成設定的資料格式並寫到 step 時沒有使用 WRITE_ONCE
修正之後就確定寫進 kfifo 的數值是正常的
比較在兩種溝通方式各下 100 局
先嘗試統計原始的程式碼 kxo_read 的資料
=== kxo_read timing report ===
Total Calls : 1822
Avg time (us) : 108118.164
Max time (us) : 247843.300
Top 10 Longest Calls:
247843.3
247446.3
247150.1
247116.1
247014.6
246969.2
246808.0
246707.7
246618.1
246578.5
測試新版的程式碼
=== kxo_read timing report ===
Total Calls : 82
Avg time (us) : 479325.243
Max time (us) : 996201.100
Top 10 Longest Calls:
996201.1
994974.3
989618.1
982364.0
898238.9
889721.9
889645.3
885614.0
878803.3
855962.8
commit 0e3b576
先嘗試在 xo.user.c 接收、解碼並儲存棋盤資訊,前面已經成功將正確的 step 放到 kfifo 中,但後續發現在使用者空間解碼並顯示出來時還是會重複讀取跟讀到亂數,經過觀察 dmesg 發現錯誤總是發生在讀取大於 0xFF 的數之後,推測原因是 kfifo_to_user 可以一次複製超過一個 byte 的資料到 user-space 但是他的 pointer 只會移動一個 byte ,所以改成把 unsigned short 拆成兩個 byte 分開傳送,就可以收到正確的資料
user space
kernel space
先實現在 user space 畫出一個棋盤,使用原本在核心模組的作法,透過 char table[][] 儲存每個棋盤的資料,在 main 中接到一個新的 step 後會先把兩個 byte 轉為一個 unsigned short 並呼叫
draw_board()
commit 6d22236
在 xo-user.c 中根據棋盤的數量開啟多個 fd 並存到陣列 fds 中,按照先前的設計核心此時就會開始排程棋局的運算
這個方式有改良的空間,之前在設計核心模組的時候特別多用一個參數來判定一個棋盤有沒有被開啟,但是 user space 這樣寫的話又變成一次性開起全部了,接下來應該要讓使用者自行決定要不要開啟新的棋局,例如按按鍵切換棋局時如果是之前沒開過的再把它 open 。
在主迴圈中用 select() 同時監看所有棋局的 /dev/kxoX,有資料才 read,然後更新對應的棋盤,為了觀察正確性先把全部的棋盤都畫出來
在 result 大於 0 時代表有鍵盤事件觸發或是有新的落子,此時就要檢查全部的棋盤
每一次 read 到值之後都會用 draw_board 畫出棋盤,實測可以順利畫出多個棋局
commit ()[]
原先的設計是 read 到新的資料後就會 解碼 -> 更新棋盤 -> 畫出來,現在透過鍵盤事件來決定要畫出來的棋盤 id,使用 > 和 < 來切換(對應 62 跟 60)