sysprog
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # Linux 核心專題: 重作 kxo > 執行人: mincch > [解說影片](https://youtu.be/JZnuofbJINE?si=SmNTC1LKuuLJw6qU) ### Reviewed by `Mike1117` - 很多程式碼區塊未註記程式語言,導致可讀性下降。 - >接這去記錄每一步移動 record_move 當有新的棋盤狀態傳輸過來時,我們需要檢查舊狀態和新狀態的差異,可以判斷出在哪個格子進行了移動,並將其記錄下來。 - 「接著」 - 選擇在 user space 比對新舊棋盤推斷下棋的位置,而不是由 kernel space 傳送的理由是什麼? ### Reviewed by `alexc-313` > 但是修改後的 code 雖然減少了每次傳輸的成本,但卻增加了大量的呼叫次數 這可能是因為修改後的程式碼多字呼叫 `smp_wmb()` ,是否能夠在保存並行性的前提下減少呼叫次數? 處理完一行再呼叫 `smp_wmb()` 是否可行? 另外,建議使用 c 註記程式語言,而非 clike 。 ## 任務描述 以[第三份作業](https://hackmd.io/@sysprog/linux2025-kxo)為基礎、搭配課堂討論和觀摩學員的成果,重新投入 kxo 開發。過程中可參照其他學員的成果 (但要實驗和指出其缺失),務必清楚標示。 ## TODO: 重作 kxo ## TODO: 觀摩其他學員的成果 > 紀錄你的啟發、你進行的驗證,以及後續改進 ## 開發環境 ```shell $ uname -rs Linux 6.11.0-28-generic $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz CPU family: 6 Model: 69 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 Stepping: 1 CPU(s) scaling MHz: 80% CPU max MHz: 2600.0000 CPU min MHz: 800.0000 BogoMIPS: 4589.34 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 64 KiB (2 instances) L1i: 64 KiB (2 instances) L2: 512 KiB (2 instances) L3: 3 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-3 ``` ## 處理 ctrl+Q 無法使用 自 GitHub kxo 進行 fork 專案後,閱讀 README 後發現 Ctrl + Q: Terminate all tic-tac-toe games running in kernel space,但是當我實際在操作時卻沒有反應,接著我去檢查了 `xo-user.c` 看到了以下這段 code ```clike static void raw_mode_enable(void) { tcgetattr(STDIN_FILENO, &orig_termios); atexit(raw_mode_disable); struct termios raw = orig_termios; raw.c_lflag &= ~(ECHO | ICANON); tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } ``` `c_lflag` : 主要是在影響字元的處理方式,處理使用者互動體驗,例如:打字的時候,螢幕上是否立刻顯示出來。 `ECHO` :控制輸入字符是否顯示出來。當設為 true 時,輸入的字元會立即顯示。當清除時 `raw.c_lflag &= ~ECHO` ,輸入的字元不會顯示,這常用於密碼輸入或遊戲等需要隱藏輸入的場景。 `ICANON`:當設為 true 時,不會直接送出,直到接收到換行符 `\n` 或 EOF ,再將整個傳遞給程式。所以這代表在按下 Enter 鍵之前,可以編輯或修改輸入的內容。 `c_iflag` : 處理鍵盤輸入的原始資料,例如:終端機是不是要處理 Ctrl+S 和 Ctrl+Q 這種特殊輸入 `IXON` :用來攔截 Ctrl+S / Ctrl+Q `ICANON`:將 CR(0x0D) 轉成 LF(0x0A),若設為 false 可保留 `\r` ```diff static void raw_mode_enable(void) { tcgetattr(STDIN_FILENO, &orig_termios); atexit(raw_mode_disable); struct termios raw = orig_termios; raw.c_lflag &= ~(ECHO | ICANON); + raw.c_iflag &= ~(IXON | ICRNL); tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); } ``` 這邊將 `IXON` 與 `ICRNL` 設為 false,可以使我們鍵盤的輸入 Ctrl+Q 不會再被攔截導致沒有反應,這樣只要按下 Ctrl+Q 就可以正常終止遊戲。 ## 使畫面呈現的部分在使用者層級 在將畫面呈現在使用者層級實作前我先研讀了 kxo 的原始設計中,遊戲盤面的繪製是透過模組中的 draw_board 完成的。這個函式使用draw_buffer,把 ASCII 畫面字元放到 kfifo;使用者程式只負責把字串整段讀出來再 printf()。這做法雖然直觀,但每次畫面更新都要在核心與 user space 之間處理會有額外更多的開銷。在最後印出 `DRAWBUFFER_SIZE`,可以發現為 `DRAWBUFFER_SIZE = 66` ,而修改之後只需要傳送 board 自己的大小 size 運用 bpftrace 測量: ``` write_bytes : 1058 B read_bytes : 5984 B write_calls : 15 read_calls : 18 ioctl_calls : 4 @ioctl_calls: 4 @rd_bytes: 5984 @rd_calls: 18 @wr_bytes: 1058 @wr_calls: 15 ``` 1058B / 15 = 70 byte per call ``` write_bytes : 3506722 B read_bytes : 714248 B write_calls : 350764 read_calls : 175286 ioctl_calls : 4 @ioctl_calls: 4 @rd_bytes: 714248 @rd_calls: 175286 @wr_bytes: 3506722 @wr_calls: 350764 ``` 3506722B / 350764 = 9.9 byte per call 用同樣30秒的時間讓他 timeout ,但是修改後的 code 雖然減少了每次傳輸的成本,但卻增加了大量的呼叫次數 ```c static int draw_board(char *table) { int i = 0, k = 0; draw_buffer[i++] = '\n'; smp_wmb(); draw_buffer[i++] = '\n'; smp_wmb(); while (i < DRAWBUFFER_SIZE) { for (int j = 0; j < (BOARD_SIZE << 1) - 1 && k < N_GRIDS; j++) { draw_buffer[i++] = j & 1 ? '|' : table[k++]; smp_wmb(); } draw_buffer[i++] = '\n'; smp_wmb(); for (int j = 0; j < (BOARD_SIZE << 1) - 1; j++) { draw_buffer[i++] = '-'; smp_wmb(); } draw_buffer[i++] = '\n'; smp_wmb(); } return 0; } ``` 這段函式負責根據遊戲介面 (char *table) 的狀態,把它變成一個 draw_buffer。將井字遊戲盤面的文字表示形式 ( 'X', 'O', ' ', '|', '-') 放到一個記憶體緩衝區中。 `draw_buffer` : 用於暫存要輸出的盤面字串 ( static char draw_buffer[DRAWBUFFER_SIZE] )。 `table` : 傳入的參數,代表了當前遊戲的介面狀態,通常是一個包含 'X', 'O', ' ' 等字元的陣列。 根據 table 的內容,將井字遊戲盤面的視覺化表示(包含格子內容、分隔符號 | 和 -、以及換行符號 \n)寫入 draw_buffer 中。 `smp_wmb()` : 在多核心處理器系統中,處理器可能會為了效能而重新排序記憶體寫入操作。`smp_wmb()` 確保在此之前的所有寫入操作(即對 draw_buffer 的寫入)都已完成並對其他處理器可見,這對於並行操作和資料一致性非常重要。 ```c static void produce_board(void) { unsigned int len = kfifo_in(&rx_fifo, draw_buffer, sizeof(draw_buffer)); if (unlikely(len < sizeof(draw_buffer))) pr_warn_ratelimited("%s: %zu bytes dropped\n", __func__, sizeof(draw_buffer) - len); pr_debug("kxo: %s: in %u/%u bytes\n", __func__, len, kfifo_len(&rx_fifo)); } ``` 這段函式負責將 draw_board 準備好的 draw_buffer 中的資料,透過 KFIFO (Kernel FIFO) 機制寫入 rx_fifo。 `kfifo_in(&rx_fifo, draw_buffer, sizeof(draw_buffer))` : 將資料寫入 KFIFO 。 `draw_buffer` : 包含要寫入資料的源緩衝區 (也就是 draw_board 準備好的盤面字串)。 `if (unlikely(len < sizeof(draw_buffer)))` 這是一個錯誤檢查。如果實際寫入的資料量少於預期,表示 KFIFO 可能滿了,會導致部分資料被丟棄。 將 produce_board 以及 draw_board 移除,並在 `xo-user.c` 加入 ```c static void draw_board(uint32_t bits) { printf("\x1b[H"); for (int r = 0; r < 4; ++r) { printf("|"); for (int c = 0; c < 4; ++c) { int idx = r * 4 + c; uint32_t v = (bits >> (idx * 2)) & 3; char ch = (v == 1) ? 'X' : (v == 2) ? 'O' : ' '; printf(" %c |", ch); } printf("\n-----------------\n"); } fflush(stdout); } ``` 這段函式負責從 KFIFO 讀取到的壓縮盤面資料 (bits) ,然後將他輸出在終端機上繪製出遊戲盤面。 用 00 , 01 , 10 分別來代表 ' ','O', 'X' `uint32_t v = (bits >> (idx * 2)) & 3` : 將 bits 右移 idx * 2 位,這樣就把當前格子 idx 的 2 位元狀態移到了 bits 的最低兩位。接著再 & 3:可以找出最低兩位元的值。這就是當前格子的狀態 (0, 1, 或 2)。 ## 對弈的過程中,要在螢幕顯示當下的時間 (含秒數),並持續更新 為了在使用者層獲取和顯示時間,我使用了時間處理函式庫,並調整 select 系統呼叫,使其能夠在沒有新的遊戲盤面資料或使用者輸入時,也能周期性地執行,以便更新時間顯示。 引入 <time.h> 標頭檔在 xo-user.c 中,新增對 <time.h> 的引用。這個函式庫提供了獲取和格式化時間的相關函式,例如 time() 和 localtime()。 遇到的困難 1. 時間與棋盤共享 stdout 造成的衝突:終端機的所有輸出都導向同一個標準輸出 (stdout)。當時間列和遊戲棋盤都需要向終端機寫入資料時,同時多個 printf() 操作都可能改變終端機滑鼠的當前位置。會造成棋盤被時間覆蓋掉,並且畫面會有閃爍的感覺 2. 游標位置干擾問題:在前面的處理中,我的 draw_board() 函式內部可能直接使用了 `\x1b[H` (將游標移動到左上角)來清除畫面或是重新顯示畫面。這導致了一些問題。時鐘已經寫好的字會立即被棋盤重畫的內容覆蓋掉,導致時間無法穩定顯示。 為了解決這些問題我將時間顯示與棋盤繪製分離,我創建了一個專門顯示時間的函式 `draw_time()` 來去單獨處理顯示時間的功能 ```c static void draw_time(void) { time_t now = time(NULL); const struct tm *tm = localtime(&now); printf("\x1b[H"); printf("Time: %02d:%02d:%02d\x1b[K", tm->tm_hour, tm->tm_min, tm->tm_sec); fflush(stdout); } ``` `time_t now = time(NULL);` 和 `const struct tm *tm = localtime(&now);`:利用 time() 獲取時間戳,再用 localtime() 轉換為 tm 結構,來提取時、分、秒。 `fflush(stdout);` 獨立的 fflush 調用確保了 `draw_time()` 輸出的時間能夠立即顯示在螢幕上,不會因為 stdout 的緩衝機制而延遲,這對於時間的即時性較重要。 為了解決游標會在畫面左上角一直閃爍的問題,我直接在 `main` 的開頭和結尾加上 `printf("\x1b[?25l");` 和 `printf("\x1b[?25h");` 分別代表了隱藏游標和顯示游標,在開始進入棋局前將游標隱藏起來,結束時再把他顯示,這樣就可以解決一直在畫面閃爍的問題。 ## 當離開對弈時,顯示多場對弈的過程 在 `xo-user.c` 中,新增 moves 陣列用於儲存每次移動的文字表示,n_moves 記錄移動次數,game_lines 儲存多個遊戲的歷史記錄,n_games 記錄遊戲局數。接著去記錄每一步移動 `record_move` 當有新的棋盤狀態傳輸過來時,我們需要檢查舊狀態和新狀態的差異,可以判斷出在哪個格子進行了移動,並將其記錄下來。比較 oldb 和 newb。找出哪個格子從 0 (空) 變成了 1 (X) 或 2 (O)。 ```c static void record_move(uint32_t oldb, uint32_t newb) { if (n_moves >= 128) return; for (int i = 0; i < 16; ++i) { uint32_t o = (oldb >> (i * 2)) & 3; uint32_t n = (newb >> (i * 2)) & 3; if (o == 0 && n != 0) { moves[n_moves][0] = 'A' + (i % 4); moves[n_moves][1] = '1' + (i / 4); moves[n_moves][2] = '\0'; ++n_moves; break; } } } ``` 接下來是當遊戲結束(分出勝負或平局)時,需要將當前遊戲的移動過程儲存到歷史記錄中,並為下一局遊戲重置相關計數器。實作 `game_over_flush` :這個函式在遊戲結束時被呼叫,負責將當前局的移動歷史 (moves) 拼接成一個字串,儲存到 game_lines 中,並重置 n_moves。 ```c static void game_over_flush(void)Add commentMore actions { if (!n_moves) return; int len = 0; len += snprintf(game_lines[n_games] + len, sizeof(game_lines[0]) - len, "Moves:"); for (int i = 0; i < n_moves; ++i) len += snprintf(game_lines[n_games] + len, sizeof(game_lines[0]) - len, " %s%s", moves[i], (i + 1 == n_moves) ? "" : " -> "); n_games++; n_moves = 0; } ``` 最後將 game_history 在 main 要結束前輸出,即可得到各對局的對弈過程。 在實作過程中遇到了問題: 開始棋局後沒有經過暫停直接 Ctrl+Q :就會缺少 Moves 輸出 但如果有 Ctrl+P → 恢復 → Ctrl+Q : Moves 就可以正常列印 在原本設計中,Ctrl+Q 在 `listen_keyboard_handler` 中會直接設定 `end_attr = true;` 並呼叫 `game_over_flush()`。當 Ctrl+Q 被按下時, device_fd 還沒有讀取到最後的棋盤更新(或者 main 迴圈中的 read_attr 處理還沒觸發),導致 record_move 沒有記錄最後一步, game_over_flush 在 moves 陣列被正確輸入前就被呼叫。導致來不及讀取最後被遺漏。 listen_keyboard_handler() 內對 Ctrl‑Q 的處理: ```c read_attr = false; end_attr = true; game_over_flush(); ``` 就會導致:提早關閉讀取 (read_attr = false) 主迴圈的 select() 之後就 不再監聽 /dev/kxo,這一步永遠不會進到 record_move() → moves[]。 ```diff static void listen_keyboard_handler(void) case 17: read(attr_fd, buf, 6); buf[4] = '1'; - read_attr = false; end_attr = true; - game_over_flush(); write(attr_fd, buf, 6); break; ``` 將 `game_over_flush()` 移動到 `main` 函式結束前再呼叫,並且保持 `read_attr = true`: 確保在 Ctrl+Q 時,主迴圈仍然能讀取到最後的棋盤狀態更新。即可修正這些問題。

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully