:::info 解析 [kxo 專案](https://github.com/sysprog21/kxo)的程式 此資料不適合從頭到尾的閱讀,適合用查詢的方式查看。 ::: # main.c ## timer_handler() `WARN_ON_ONCE(!in_softirq());` -> 判斷是否在 softirq context -> 為了模擬 hardirq ,使用 timer interrupt `local_irq_disable();` -> 將 CPU 關閉接收其他 interrupt `char win = check_win(table);` -> 判斷是否已有輸贏 -> 回傳 `' '` ,表示未有輸贏,繼續比賽;其餘則表示已有輸贏,下一場比賽。 ``` c if (win == ' ') { ai_game(); mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); } ``` -> `win == ' '` : 格子中沒有 o/x 的符號,表示未有輸贏 -> `ai_game()` : 進入 tasklet ,登記 workqueue 的排程 -> `mod_timer` : 再次呼叫 `timer_setup()` ``` c else { read_lock(&attr_obj.lock); if (attr_obj.display == '1') { int cpu = get_cpu(); pr_info("kxo: [CPU#%d] Drawing final board\n", cpu); put_cpu(); mutex_lock(&producer_lock); draw_board(table); mutex_unlock(&producer_lock); /* Store data to the kfifo buffer */ mutex_lock(&consumer_lock); produce_board(); mutex_unlock(&consumer_lock); wake_up_interruptible(&rx_wait); } if (attr_obj.end == '0') { memset(table, ' ', N_GRIDS); /* Reset the table so the game restart */ mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); } read_unlock(&attr_obj.lock); pr_info("kxo: %c win!!!\n", win); } ``` `attr_obj` : device attribute,用來與userspace溝通 `read_lock` : `mutex_lock` / `mutex_unlock` : `draw_board` : 將 `table` 的 ooxx 加入分隔線後填入 `draw_buffer` ,方便印出來時閱讀 `produce_board` : 將 `draw_buffer` 的資料放入 fifo 之中 ==為什麼要放入fifo?== (有API可以呼叫並且保護資料嘛?) `wake_up_interruptible` : 用來喚醒 wait queue 中一個 process ,且該函式有 memory barrier 的功能。 - 根據 [Linux kernel memory barriers](https://www.kernel.org/doc/html/next/core-api/wrappers/memory-barriers.html) > wake_up_process() always executes a general memory barrier. > The available waker functions include: `wake_up_interruptible()` > - 根據 [Kernel Korner - Sleeping in the Kernel](https://www.linuxjournal.com/article/8144) > wake_up_interruptible wakes up one process that was sleeping on the smbiod_wait waitqueue. `memset` : 用給定值填滿記憶體空間 ``` c * memset - Fill a region of memory with the given value * @s: Pointer to the start of the area. * @c: The byte to fill the area with * @count: The size of the area. ``` `mod_timer` : 初始化 timer ,呼叫`timer_setup(&timer, timer_handler, 0);` ## draw_board(table) > <font color = "#F0F">將 `table` 的 ooxx 填入 `draw_buffer` ,方便印出來閱讀 </font> `draw_buffer[0]` 跟 `draw_buffer[1]` 是為了換兩行(空一行) ``` c int i = 0, k = 0; draw_buffer[i++] = '\n'; smp_wmb(); draw_buffer[i++] = '\n'; smp_wmb(); ``` 撰寫有 **ooxx** 的文字進入 `draw_buffer` ``` c for (int j = 0; j < (BOARD_SIZE << 1) - 1 && k < N_GRIDS; j++) { draw_buffer[i++] = j & 1 ? '|' : table[k++]; smp_wmb(); } ``` -> `(BOARD_SIZE << 1) - 1` :xxoo + 間隔的數量 -> `k < N_GRIDS`:確保無超出棋盤大小 撰寫**間隔**的文字進入 `draw_buffer` ``` c for (int j = 0; j < (BOARD_SIZE << 1) - 1; j++) { draw_buffer[i++] = '-'; smp_wmb(); } ``` ## produce_board > <font color = "#F0F">將 `draw_buffer` 的資料放入 fifo 之中 </font> ``` c len = kfifo_in(&rx_fifo, draw_buffer, sizeof(draw_buffer)); * kfifo_in - put data into the fifo * @fifo: address of the fifo to be used * @buf: the data to be added * @n: number of elements to be added * return the number of copied elements to fifo. ``` - 根據 [lib/kfifo.c](https://elixir.bootlin.com/linux/v6.14.3/source/lib/kfifo.c#L113) - 在 `rx_fifo` 空間充足時,`len = sizeof(draw_buffer)` - 在 `rx_fifo` 空間不足時,`len < sizeof(draw_buffer)` - `kfifo_in`的輸出是實際加入 fifo 的 buffer size 如果發現 `len < sizeof(draw_buffer)` ,印出警告 -> `rx_fifo` 空間不足 ``` c if (unlikely(len < sizeof(draw_buffer)) && printk_ratelimit()) pr_warn("%s: %zu bytes dropped\n", __func__, sizeof(draw_buffer) - len); ``` - `unlikely`: 表示幾乎不可能發生的情況(提醒其他人),並[優化 Compiler](https://stackoverflow.com/questions/109710/how-do-the-likely-unlikely-macros-in-the-linux-kernel-work-and-what-is-their-ben)。 ``` c # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) ``` - 需要嚴謹的分析後才可以使用 `likely` / `unlikely` - `printk_ratelimit` : 會限制警告訊息的速度,避免印出過多的警告訊息。 根據 [Documentation for /proc/sys/kernel/](https://www.kernel.org/doc/html/next/admin-guide/sysctl/kernel.html#printk-ratelimit) 的定義 > Some warning messages are rate limited. printk_ratelimit specifies the minimum length of time between these messages (in seconds). The default value is 5 seconds. > > A value of 0 will disable rate limiting. 印出當前的 `rx_fifo` 的使用量 ``` c pr_debug("kxo: %s: in %u/%u bytes\n", __func__, len, kfifo_len(&rx_fifo)); // kxo: my_func: in 64/128 bytes ``` ## attr_obj 如何利用 sysfs 建立 device attribute 讓核心模組與使用者相互溝通? -> 參見 [Linux Kernel Module Programming Guide: Chap8](https://sysprog21.github.io/lkmpg/) 建立 device attribute ``` c static DEVICE_ATTR_RW(kxo_state); ``` - 參見 [include/linux/device.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/device.h#L173) ``` c #define DEVICE_ATTR_RW(_name) \ struct device_attribute dev_attr_##_name = __ATTR_RW(_name) ``` - `dev_attr_kxo_state = ATTR_RW(kxo_state)` ``` c #define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store) ``` ``` c #define __ATTR(_name, _mode, _show, _store) { \ .attr = {.name = __stringify(_name), \ .mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \ .show = _show, \ .store = _store, \ } ``` - `kxo_state_show` : read,當使用者呼叫 cat 時 - `kxo_state_store` : write,當使用者呼叫 echo 時 建立 `kxo_state_show` for read ``` c static ssize_t kxo_state_show(struct device *dev, struct device_attribute *attr, char *buf) { read_lock(&attr_obj.lock); int ret = snprintf(buf, 6, "%c %c %c\n", attr_obj.display, attr_obj.resume, attr_obj.end); read_unlock(&attr_obj.lock); return ret; } ``` - ==為什麼要用read_lock== - [`snprintf`](https://linux.die.net/man/3/snprintf) : 為比較安全的 printf,因為不會寫超過指定大小 - `s` : string - `n` : 要輸入的 size 建立 `kxo_state_store` for write ``` c static ssize_t kxo_state_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { write_lock(&attr_obj.lock); sscanf(buf, "%c %c %c", &(attr_obj.display), &(attr_obj.resume), &(attr_obj.end)); write_unlock(&attr_obj.lock); return count; } ``` - ==為什麼要用write_lock== - [`sscanf`](https://linux.die.net/man/3/sscanf) : 將 `buf` 的內容存進 `attr_obj` 中 - example ``` c char input[] = "123 4.56 hello"; int a; float b; char word[20]; sscanf(input, "%d %f %s", &a, &b, word); ``` 這段 code 會從 input 中讀出: `%d` → `a = 123` `%f` → `b = 4.56` `%s` → `word = "hello"` `sscanf()` 會回傳成功讀取的項目數,在這個例子裡會是 3。 ``` c static struct kxo_attr attr_obj; ``` ``` c struct kxo_attr { char display; char resume; char end; rwlock_t lock; }; ``` - `display` : - `resume` : - `end` : - `lock` : ## ai_game() ``` c static void ai_game(void) { WARN_ON_ONCE(!irqs_disabled()); pr_info("kxo: [CPU#%d] doing AI game\n", smp_processor_id()); pr_info("kxo: [CPU#%d] scheduling tasklet\n", smp_processor_id()); tasklet_schedule(&game_tasklet); } ``` `WARN_ON_ONCE(!irqs_disabled())` : 當下若還能接收其他來自外界的中斷,就需要警告開發者;換句話說,**此程式不希望被來自外界的中斷給中斷**,因而提出警告,但是並沒辦法禁止中斷發生。 `tasklet_schedule` : 將新的 `&game_tasklet` 加入 `tasklet_vec` - 參見 [Interrupts and Interrupt Handling. Part 9.](https://0xax.gitbooks.io/linux-insides/content/Interrupts/linux-interrupts-9.html) ``` c static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); } void __tasklet_schedule(struct tasklet_struct *t) { unsigned long flags; local_irq_save(flags); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags); } ``` - 因為 tasklet 為一種 softirq,所以需要在 interrupt context中執行 - `local_irq_save(flag)` : save the state of the IF (interrupt flag) and **disable interrupt** - `local_irq_restore(flag)` : restore IF and **enable interrupt** - 更新 `tasklet_vec` - `t->next = NULL;` : 將 t->next 指向一個空白的空間 - `*__this_cpu_read()` : 將 t 放置目前的 tasklet_vec 的尾端 - `__this_cpu_write()` : 將新 tasklet t 的 next 成員的地址寫入到當前 CPU 的 tasklet_vec 的 tail 成員中 - `game_tasklet` : 為一個 struct tasklet 的結構,並有一個函數指標指向 `game_tasklet_func` ### DECLARE_TASKLET_OLD ``` c static DECLARE_TASKLET_OLD(game_tasklet, game_tasklet_func); ``` - 定義 name ,並且以 `_func` 作為 func 函數指標指向的函數 ``` c #define DECLARE_TASKLET_OLD(name, _func) \ struct tasklet_struct name = { \ .count = ATOMIC_INIT(0), \ .func = _func, \ } ``` -> 因為 `.func = _func`,這一行程式讓一個名為func的函式指標指向 `_func` ## game_tasklet_func() 如果不在 interrupt context 和 softirq context 的話,提出警告。 ``` c WARN_ON_ONCE(!in_interrupt()); WARN_ON_ONCE(!in_softirq()); ``` 保證「只讀一次、完整地」取值 ``` c READ_ONCE(finish); READ_ONCE(turn); ``` - turn 表示參賽者 `'X'` 或 `'O'` - finish 表示參賽者是否下完棋。 - 開始下棋~下完棋 `0 (False)` - 下完棋~下次開始下棋 `1 (True)` 保證「只寫一次」值 ``` c WRITE_ONCE(finish, 0); ``` - 將 0 寫入 finish 中 輪到 `O` 時,將 ``` c if (finish && turn == 'O') { WRITE_ONCE(finish, 0); smp_wmb(); queue_work(kxo_workqueue, &ai_one_work); ``` `queue_work` : 將 `ai_one_work` 中的函式放入 `kxo_workqueue` 的排程中 `kxo_workqueue` : 根據排程順序放置函式的 queue `ai_one_work` : 透過 `DECLARE_WORK` 宣告一個 `work_struct` 的結構,在這邊會呼叫此結構中的函式。 - 根據 [/include/linux/workqueue.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/workqueue.h#L636) ``` c * queue_work - queue work on a workqueue * @wq: workqueue to use * @work: work to queue ``` - 根據 [Interrupts and Interrupt Handling. Part 9.](https://0xax.gitbooks.io/linux-insides/content/Interrupts/linux-interrupts-9.html) 說明 `queue_work` 的用途 ``` c static inline bool queue_work(struct workqueue_struct *wq, struct work_struct *work) {...} ``` > macro that takes work_struct structure that has to be created and the function to be scheduled in this workqueue. After a work was created with the one of these macros, we need to put it to the workqueue. -> work 結構中函數會被排入 workqueue 的排程中 - 根據 [/include/linux/workqueue_types.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/workqueue_types.h#L16) 查看 work_struct 的結構 ``` c struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; ... } ``` 將印出 table 的函式旁入行程中,以便即時查看進度。 ``` c queue_work(kxo_workqueue, &drawboard_work); ``` `drawboard_work` 透過 `DECLARE_WORK` 宣告一個 `work_struct` 的結構 ### DECLARE_WORK > <font color = "#F0F">宣告了一個名為 `ai_one_work` 的工作項(struct work_struct),並將其處理函數設定為 `ai_one_work_func`。當 `ai_one_work` 被排入 workqueue 並執行時,`ai_one_work_func` 這個函數將會被呼叫來執行實際的工作。</font> ``` c static DECLARE_WORK(ai_one_work, ai_one_work_func); ``` - 根據 [/include/linux/workqueue.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/workqueue.h#L250) ``` c #define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f) ``` ``` c #define __WORK_INITIALIZER(n, f) { \ .data = WORK_DATA_STATIC_INIT(), \ .entry = { &(n).entry, &(n).entry }, \ .func = (f), \ __WORK_INIT_LOCKDEP_MAP(#n, &(n)) \ } ``` ## ai_one_work_func() 後續程式會用到 mutex ,須確保程式在 process context 執行,否則會 deadlock ``` c WARN_ON_ONCE(in_softirq()); WARN_ON_ONCE(in_interrupt()); ``` -> 如果程式在 softirq context 或 interrupt context ,提出警告。 在這個區塊中的程式碼保證會在同一個 CPU 上執行,且不會被核心搶佔 ``` c cpu = get_cpu(); ... put_cpu(); ``` - 參見 [Unreliable Guide To Hacking The Linux Kernel](https://www.kernel.org/doc/html/next/kernel-hacking/hacking.html) 和 [/include/linux/smp.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/smp.h#L244) > get_cpu() disables preemption (so you won’t suddenly get moved to another CPU) and returns the current processor number, between 0 and NR_CPUS. Note that the CPU numbers are not necessarily continuous. You return it again with put_cpu() when you are done. - ==`get_cpu()` vs `preempt_disable()` vs `local_irq_disable`== - 參見 [/include/linux/smp.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/smp.h#L275) ``` c #define get_cpu() ({ preempt_disable(); __smp_processor_id(); }) ``` - 參見 [/include/linux/irqflags.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/linux/irqflags.h#L206) ``` c #define local_irq_disable() \ do { \ bool was_disabled = raw_irqs_disabled();\ raw_local_irq_disable(); \ if (!was_disabled) \ trace_hardirqs_off(); \ } while (0) ``` - 未閱讀 [Linux 核心設計: `PREEMPT_RT` 作為邁向硬即時作業系統的機制](https://hackmd.io/@sysprog/preempt-rt) 及 [Linux 核心搶佔](https://hackmd.io/@sysprog/linux-preempt) ==why?== ``` c mutex_lock(&producer_lock); ... // critical section mutex_unlock(&producer_lock); ``` 將 `mcts(table, 'O')` 的回傳值輸入 `move` -> 推測是 ``` c int move; WRITE_ONCE(move, mcts(table, 'O')); ``` -> 推測 move 是指要填入 table 的位置 ==須再細讀 `mcts()`== 只要 `move` 不是 `-1` 就可以將 `O` 填入 ``` c if (move != -1) WRITE_ONCE(table[move], 'O'); ``` -> `move = -1` 為 move 的初始值 ==?== ## ai_two_work_func() 基本上都與 `ai_one_work_func()` 相同,只有計算移動的演算法不同,不同的段落如下 - 在 `ai_one_work_func()` 的程式: ``` c int move; WRITE_ONCE(move, mcts(table, 'O')); ``` - 在 `ai_two_work_func()` 的程式: ``` c int move; WRITE_ONCE(move, negamax_predict(table, 'X').move); ``` ## drawboard_work_func() 幾乎與 `timer_handler()` 某一段一模一樣 ==了解差異== ``` c read_lock(&attr_obj.lock); if (attr_obj.display == '0') { read_unlock(&attr_obj.lock); return; } read_unlock(&attr_obj.lock); ``` ``` c mutex_lock(&producer_lock); draw_board(table); mutex_unlock(&producer_lock); ``` ``` c /* Store data to the kfifo buffer */ mutex_lock(&consumer_lock); produce_board(); mutex_unlock(&consumer_lock); ``` ``` c wake_up_interruptible(&rx_wait); ``` ## open_cnt {} 存放一個計數器 ``` c static atomic_t open_cnt; ``` - 參見 [/include/linux/types.h](https://elixir.bootlin.com/linux/v6.14.3/source/tools/include/linux/types.h#L79) ``` c typedef struct { int counter; } atomic_t; ``` ## kxo_open() ``` c if (atomic_inc_return(&open_cnt) == 1) mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); pr_info("openm current cnt: %d\n", atomic_read(&open_cnt)); ``` - `atomic_inc_return(&open_cnt)`:原子地把 `open_cnt` 數值加 1,並回傳加完後的值。 - 如果回傳值是 1,代表這是第一次有人開啟裝置(也就是之前是 0),就執行下面的 `mod_timer()`。 - ==為什麼初始值一定是 0? 哪裡有定義?== - 印出目前 `open_cnt` 的數值,也就是目前有多少人開啟這個裝置。 - ==`open_cnt` 怎麼修改的== ## kxo_read() 如果使用者傳來的記憶體區塊不能安全地存取,就立刻回傳 -EFAULT(記憶體錯誤)。 ``` c if (unlikely(!access_ok(buf, count))) return -EFAULT; ``` - 參見 [/include/asm-generic/access_ok.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/asm-generic/access_ok.h#L45) ==不確定是不是這份文件的解釋== ``` c #define access_ok(addr, size) likely(__access_ok(addr, size)) ``` - `buf` 是使用者提供的記憶體位址,`count` 是要讀寫的位元組數。 - 如果記憶體範圍 不合法(例如無效位址或越界),就會回傳 false。 - 參見 [/include/uapi/asm-generic/errno-base.h](https://elixir.bootlin.com/linux/v6.14.3/source/include/uapi/asm-generic/errno-base.h#L18) ``` c #define EFAULT 14 /* Bad address */ ``` 如果無法順利取得 mutex(例如被中斷),就回傳 -ERESTARTSYS,表示系統呼叫被打斷,可以被重新啟動。 ``` c if (mutex_lock_interruptible(&read_lock)) return -ERESTARTSYS; ``` - 參見 [/kernel/locking/mutex.c](https://elixir.bootlin.com/linux/v6.14.3/source/kernel/locking/mutex.c#L947) ``` c * mutex_lock_interruptible() - Acquire the mutex, interruptible by signals. * @lock: The mutex to be acquired. * Return: 0 if the lock was successfully acquired or %-EINTR if a * signal arrived. ``` - 嘗試取得 `read_lock` 這個 mutex - 如果在等待鎖的過程中被訊號打斷(例如使用者用 Ctrl+C),它會回傳 `-EINTR` 將 kfifo 的資料傳入 user space 1. 試著從 KFIFO 讀資料 2. 有資料 ➔ 直接結束 3. 沒資料 a. 非阻塞模式 ➔ 回 -EAGAIN b. 阻塞模式 ➔ 進入睡眠,等有資料再醒來 4. 醒來後重試,直到讀到資料或出錯 ``` c do { ret = kfifo_to_user(&rx_fifo, buf, count, &read); if (unlikely(ret < 0)) break; if (read) break; if (file->f_flags & O_NONBLOCK) { ret = -EAGAIN; break; } ret = wait_event_interruptible(rx_wait, kfifo_len(&rx_fifo)); } while (ret == 0); ``` - 參見 [/include/linux/kfifo.h](https://elixir.bootlin.com/linux/v6.14.4/source/include/linux/kfifo.h#L688) 及 [The Linux Kernel API](https://www.kernel.org/doc/html/v4.15/core-api/kernel-api.html?highlight=kfifo_to_user#c.kfifo_to_user) ``` c * kfifo_to_user - copies data from the fifo into user space * @fifo: address of the fifo to be used * @to: where the data must be copied * @len: the size of the destination buffer * @copied: pointer to output variable to store the number of copied bytes ``` - 從 rx_fifo 把資料複製到 buf,最多 count bytes,實際讀到 read bytes - `if (file->f_flags & O_NONBLOCK)` 當 `file->f_flags & O_NONBLOCK` 不為零時就成立。 ==須查證== - 參見 [The File Object](https://litux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec8.html) - file->flags 表示多個指定的 flag 被打開時 - `O_NONBLOCK` 非阻塞狀態 -> 為固定值 - 參見 [/include/linux/wait.h](https://elixir.bootlin.com/linux/v6.14.4/source/include/linux/wait.h#L481) ``` c * wait_event_interruptible - sleep until a condition gets true * @wq_head: the waitqueue to wait on * @condition: a C expression for the event to wait for * The function will return -ERESTARTSYS if it was interrupted by a * signal and 0 if @condition evaluated to true. ``` - 如果是阻塞模式 ➔ 進入睡眠,等待 rx_fifo 裡有資料 # game.c ## line_t > <font color = "#F0F">lines 用來定義四個方向可能連線的方式</font> ``` c typedef struct { int i_shift, j_shift; int i_lower_bound, j_lower_bound, i_upper_bound, j_upper_bound; } line_t; const line_t lines[4] = { {0, 1, 0, 0, BOARD_SIZE, BOARD_SIZE - GOAL + 1}, // ROW {1, 0, 0, 0, BOARD_SIZE - GOAL + 1, BOARD_SIZE}, // COL {1, 1, 0, 0, BOARD_SIZE - GOAL + 1, BOARD_SIZE - GOAL + 1}, // PRIMARY {1, -1, 0, GOAL - 1, BOARD_SIZE - GOAL + 1, BOARD_SIZE}, // SECONDARY }; ``` - lines[0] 同一行,往右掃描 - lines[1] 同一列,往下掃描 - lines[2] 主角線,往右下掃描 - lines[3] 副對角線,往左下掃描 `BOARD_SIZE` : 棋盤大小 (5x5 的棋盤, `BOARD_SIZE=5` ) `GOAL` : 需要幾個連成一線才勝利 (xxoo 的 `GOAL=3` ) ## check_win(t) ``` c for (int i_line = 0; i_line < 4; ++i_line) { line_t line = lines[i_line]; for (int i = line.i_lower_bound; i < line.i_upper_bound; ++i) { for (int j = line.j_lower_bound; j < line.j_upper_bound; ++j) { char win = check_line_segment_win(t, i, j, line); if (win != ' ') return win; } } } ``` - 1st for loop: 測試四種可能連線的方向 - 2nd for loop: 橫軸範圍 - 3rd for loop: 縱軸範圍 -> 2nd & 3rd for loop 僅用來選擇連線的**起始點** -> `check_line_segment_win()` 會判斷是否真的有連線 確認棋盤是否還有空位能填寫 ``` c for (int i = 0; i < N_GRIDS; i++) if (t[i] == ' ') return ' '; return 'D'; ``` `N_GRIDS` : 棋盤尺寸 ( 5x5 的棋盤, `N_GRIDS=25` ) - `N_GRIDS = BOARD_SIZE * BOARD_SIZE` -> 確認棋盤內還有空白處 -> 若棋盤都沒有空白處則回傳 `'D'` ## check_line_segment_win() 確認是否有連線 ``` c check_line_segment_win(const char *t, int i, int j, line_t line) ``` - `t` : table ,可知整個棋盤的長相 - `i, j` : 查看是否連線的起始點 - `lines` : 說明連線方向 不允許超出邊界(!ALLOW_EXCEED) ``` c #if !ALLOW_EXCEED if (last == LOOKUP(t, i - line.i_shift, j - line.j_shift, ' ') || last == LOOKUP(t, i + GOAL * line.i_shift, j + GOAL * line.j_shift, ' ')) return ' '; #endif ``` ## calculate_win_value() ## available_moves() # game.h ## DRAWBUFFER_SIZE ``` c #define DRAWBUFFER_SIZE \ ((BOARD_SIZE * (BOARD_SIZE + 1) << 1) + (BOARD_SIZE * BOARD_SIZE) + \ ((BOARD_SIZE << 1) + 1) + 1) ``` `(BOARD_SIZE * (BOARD_SIZE + 1) << 1)` :填入間隔線( `|` 或 `-` )的數量 `(BOARD_SIZE * BOARD_SIZE)`:填入 ooxx 的空格數量 `((BOARD_SIZE << 1) + 1) + 1`:間隔數量 # mcts.c ## mcts() # negamax.c ## negamax_predict # xo-user.c ::: info 使用 `sudo cat /dev/kxo` 執行程式,只會執行到 kernel module 中的 `open()`->`read()`->`release()`,並不會執行到 userspace 的程式。若要執行 xo-user.c 需要 : - 使用 `gcc xo-user.c -o xo-user` 產生執行檔 - 且用 `./xo-user` 執行。 ::: ## status_check() > 如果 XO_STATUS_FILE 不為 `"live"` 的狀態,則回傳 false ``` c FILE *fp = fopen(XO_STATUS_FILE, "r"); ``` - `#define XO_STATUS_FILE "/sys/module/kxo/initstate"` - 把檔案 XO_STATUS_FILE 打開成唯讀模式 ("r") - 然後把這個打開的檔案,對應到一個 FILE 指標(也就是 fp) - 以後你可以透過 fp 來「讀取」這個檔案的內容!` ``` c char read_buf[20]; fgets(read_buf, 20, fp); ``` - 將 fp 中前 19 個資料存入 read_buf 中,第 20 格放 `\0` - 若提前遇到 `\n` ,就會提前結束讀取 ``` c read_buf[strcspn(read_buf, "\n")] = 0; ``` -> 找出 read_buf 中第一個換行 `\n`,把它換成字串結束字元 `'\0'` - 參見 [strcspn(3p) — Linux manual page](https://man7.org/linux/man-pages/man3/strcspn.3p.html) - strcspn — get the length of a complementary substring - `strcspn(read_buf, "\n"` 可取得 `"\n"` 在 `read_buf` 中的 index ``` c if (strcmp("live", read_buf)) { printf("kxo status : %s\n", read_buf); fclose(fp); return false; } ``` - 如果 read_buf 的內容為 live ,`strcmp("live", read_buf)` 回傳 0 - `if (0)` 表示不成立,不會進入 if 內 -> 換句話說,read_buf 如果不為 `"live"` ,則回傳 false 。 ## raw_mode_enable() ✅ 讀取現在的終端機設定 ✅ 註冊還原動作 ✅ 關掉 ICANON(即時讀取)和 ECHO(隱藏輸入) ✅ 啟用新的 raw 模式設定 ::: info 事先閱讀:[取得終端機屬性的筆記](https://hackmd.io/@EJ7289/HkXvFfkxex) ::: ``` c tcgetattr(STDIN_FILENO, &orig_termios); ``` - 參見 [stdin(3) — Linux manual page](https://man7.org/linux/man-pages/man3/stdin.3.html) > On program startup, the integer file descriptors associated with the streams stdin, stdout, and stderr are 0, 1, and 2, respectively. The preprocessor symbols STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO are defined with these values in <unistd.h>. - `STDIN_FILENO` 為標準輸入(讓程式接收鍵盤的訊號) - STDIN = standard input (通常為 [File Descriptor](https://wiyi.org/linux-file-descriptor.html) 的 0) - 參見 [tcgetattr(3) - Linux man page](https://linux.die.net/man/3/tcgetattr) > The termios functions describe a general terminal interface that is provided to control asynchronous communications ports. - termios 函式提供控制非同步溝通的 port > `int tcgetattr(int fd, struct termios *termios_p);` > tcgetattr() gets the parameters associated with the object referred by fd and **stores them in the termios structure referenced by termios_p**. -> tcgetattr 會將 `STDIN_FILENO` 這 file descriptor 存入 `orig_termios` 中 ==為什麼將特定的 file descriptor 存入 termios function 就是取得終端機的屬性?怎模確定放入的fd 一定是終端機的屬性,而不是其他file 的?== ➡️ 這樣等一下可以「改完之後,再還原」==chatGPT 說的== ``` c atexit(raw_mode_disable); ``` - 參見 [atexit(3) - Linux man page](https://linux.die.net/man/3/atexit) - 當一個正常的 process 結束後,註冊一個函式。 > The atexit() function registers the given function to be called at normal process termination, either via exit(3) or via return from the program's main(). > returns the value 0 if successful; otherwise it returns a nonzero value. ``` c struct termios raw = orig_termios; raw.c_lflag &= ~(ECHO | ICANON); tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); ``` - 設定 c_lflag 為 ICANON => canonical mode (須按 Enter 才會被程式讀取) - 設定 c_lflag 為 ECHO => 可以輸入 characters - `~(ECHO | ICANON)` 反轉以後又使用 `&` => 可以視為 mask -> 表示禁用 ECHO 和 ICANON - `tcsetattr(fd, optional_actions, termios_p)` 用來設定終端機屬性 - 將文件描述 (fd) 儲存至終端機屬性 (termios_p) - optional_actions 為修改終端機屬性的時機 - `TCSAFLUSH` 表示輸出傳完後才變更,而且清除還沒讀的輸入資料 - 解析 `atexit()` - 其中 `atexit()` 是用來註冊還原的動作,是為了之後需要還原的時候而設定的。 - 當 process 結束 (normal termination) 的時候,會自動啟動還原的動作。 - process結束是這指一個main()執行完return 0的時候。 設定完還原檔以後,就可以設定我們需要的模式,我們後續的操作需要使用 raw mode,因此將 ECHO 和 ICANON 都關掉。 -> 完成 raw mode 設定 ## listen_keyboard_handler() 1. 開啟 device attribute file -> `open(attr_fd, O_RDWR)` 2. 讀取 keyboard 的輸入 -> `read(STDIN, &input, 1)` - 將鍵盤輸入至 input 3. 查看對應 input 指令 -> `switch(input)` 4. 讀取 attr_fd 的資料 -> `read(attr_fd, buf, 6)` - 將 attr_fd 的資料存入 buf 中 - 執行 `kxo_state_show()` - buf 的 6 個 element -> `"%c %c %c\n"` - buf[0] = attr_obj.disply - buf[2] = attr_obj.resume - buf[4] = attr_obj.end 5. `buf[0]` 0,1互換 6. `read_attr ^= 1` 0,1互換 - read_attr = true -> 會印棋盤 - read_attr = false -> 不會印棋盤 7. 寫入 attr_fd 的資料 -> `write(attr_fd, buf, 6)` - 將 buf 的資料存入 attr_fd 中 - 執行 `kxo_state_store()` -> 變更 attr_fd ``` c #define XO_DEVICE_ATTR_FILE "/sys/class/kxo/kxo/kxo_state" ... int attr_fd = open(XO_DEVICE_ATTR_FILE, O_RDWR); ``` - device attribute 可是用來讓 user space 與 kernel space 溝通 (參考LKMPG-ch7) - `O_RDWR` is access mode ``` c if (read(STDIN_FILENO, &input, 1) == 1) { ... } ``` ``` c switch (input) { case 16: /* Ctrl-P */ read(attr_fd, buf, 6); buf[0] = (buf[0] - '0') ? '0' : '1'; read_attr ^= 1; write(attr_fd, buf, 6); if (!read_attr) printf("Stopping to display the chess board...\n"); break; case 17: /* Ctrl-Q */ read(attr_fd, buf, 6); buf[4] = '1'; read_attr = false; end_attr = true; write(attr_fd, buf, 6); printf("Stopping the kernel space tic-tac-toe game...\n"); break; } ``` - [為什麼 Ctrl + P = 16 ?](https://www.commfront.com/pages/ascii-chart) - `read(attr_fd, buf, 6)` -> kxo_state_show() - `attr_fd` : 為 kxo 的 device attribute,因此會連結到 main.c 中的 kxo_read() - `buf` : 是 user space 中準備好用來接收資料的 buffer。 - `6` : 是希望從 kernel module 中讀取的最大 byte 數。 - `buf[0] = (buf[0] - '0') ? '0' : '1';` - 如果 `buf[0] = '0'` -> `(buf[0] - '0') = 0(False)` -> `buf[0] = '1'` - 如果 `buf[0] != '0'` -> `(buf[0] - '0') = 1(True)` -> `buf[0] = '0'` - 結論:這行是在做 0 與 1 的切換(toggle)! - `read_attr ^= 1;` - `read_attr` 為布林變數 (bool) - 如果原本的 `read_attr = 1` -> `read_attr = 0` - 如果原本的 `read_attr = 0` -> `read_attr = 1` - 結論:這行同樣也在做 0 和 1 的切換。 - `write(attr_fd, buf, 6);` -> kxo_state_store() - 把新的 `buf` 寫回 `attr_fd`,代表更新這個屬性為新的值(`0` 或 `1`) ## main() ``` c int flags = fcntl(STDIN_FILENO, F_GETFL, 0); fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); ``` - 參見 [fcntl(3) - Linux man page](https://linux.die.net/man/3/fcntl) - fcntl : file control - F_GETFL : Get the file status flags and file access modes - Value of file status flags and access modes. The return value is not negative. - F_SETFL : Set the file status flag - Value other than -1. ![image](https://i.imgur.com/4Kgt8yI.png) -> 「可以利用 `fcntl(fd, F_GETFL)` 找出一個 file descriptor 所對應的 entry in open file table 中的 status flags」 -> 「可以利用 `fcntl(fd, F_SETFL, flags | O_NONBLOCK)` 重新設定 file descriptor 對應的 open file 的 status flag,並加上 O_NONBLOCK(非阻塞模式)」 - 如果沒有 `flags |`,直接寫: ``` c fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); ``` - 只保留 O_NONBLOCK,清除其他原本的 flags(像 O_RDWR, O_APPEND 等) - 有 `flags |` ,會保留原有的status flag,再額外增加一個 flag - 各種 open file status flags 的定義可以查看 [open(2) — Linux manual page](https://man7.org/linux/man-pages/man2/open.2.html) ``` c int device_fd = open(XO_DEVICE_FILE, O_RDONLY); ``` - 參見 [open(2) — Linux manual page](https://man7.org/linux/man-pages/man2/open.2.html) - The `open()` system call opens the file specified by *pathname*. - access modes : O_RDONLY, O_WRONLY, or O_RDWR. - `O_RDONLY` : opened for reading ``` c fd_set readset; ... int max_fd = device_fd > STDIN_FILENO ? device_fd : STDIN_FILENO; ... int result = select(max_fd + 1, &readset, NULL, NULL, NULL); ``` `max_fd` : 為了找出最大的 file descriptor,方便之後設定監聽範圍( `select()` ) - 參見 [select(2) — Linux manual page](https://man7.org/linux/man-pages/man2/select.2.html) - `select()` : allows a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O operation. - `fd_set` : A structure type that can represent a set of file descriptors. - `max_fd + 1` : 為最高的 file descriptor + 1 - `&readset` : 此集合中的 file descriptors 會被監聽著,看是否準備被 reading ,如果是準備被reading 的 file descriptor will **not block**. - `select(max_fd + 1, &readset, NULL, NULL, NULL)` - `select()` 在監聽多個 file descriptor(FD)中是否可讀(readable) - `max_fd + 1` 是 select 的第一個參數(因為 select 會檢查 [0, max_fd]) - `&readset` 是要監控的 fd_set(用 FD_SET() 設定) - `NULL`, `NULL` 表示不監控寫入與異常的 fd - `NULL` 表示無 timeout(=阻塞直到有事件) ``` c read_attr = true; end_attr = false; while (!end_attr) { FD_ZERO(&readset); FD_SET(STDIN_FILENO, &readset); FD_SET(device_fd, &readset); int result = select(max_fd + 1, &readset, NULL, NULL, NULL); if (result < 0) { printf("Error with select system call\n"); exit(1); } 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); printf("%s", display_buf); } } ``` - 參見 [select(2) — Linux manual page](https://man7.org/linux/man-pages/man2/select.2.html) - `FD_ZERO` : 清除所有 file descriptor in the set - `FD_SET` : adds the file descriptor fd to set - `FD_CLR` : removes the file descriptor fd from set. - `FD_ISSET` : 測試指定的 file descriptor 是否還存在於 set 中 # Expend File Descriptor Table 0: 1: 2: https://wiyi.org/linux-file-descriptor.html # other - [ ] 範例 ``` c // 寫入呼叫:使用者下指令,排入一個 work static ssize_t my_write(...) { queue_work(my_wq, &my_work); return 0; } // work 執行完畢時,喚醒 read() 等待中的 process static void my_work_func(struct work_struct *work) { compute_result(...); result_ready = 1; wake_up_interruptible(&my_waitq); } // 使用者程式中 read():等待結果出來 static ssize_t my_read(...) { wait_event_interruptible(my_waitq, result_ready == 1); result_ready = 0; copy_to_user(...); return ...; } ``` - [Process State and Wait Queue](https://hackmd.io/@MEME48/Hy65Z5A_h) - `wake_up_interruptible`:喚醒 wait queue 中的程式 - `wait_event_interruptible`:將使用者執行的程式放置 wait queue - 喚醒 wait queue 中的程式需要滿足兩個條件 1. 滿足 `wait_event_interruptible(waitq, condition)` 中的condition 2. 呼叫 `wake_up_interruptible(waitq)` - `read()` 與 `wake_up_interruptible(waitq)` 的差異 - `read()`: 使用這主動進入核心 - `wake_up`: 喚醒「核心中等待的 read() 」 讓它「繼續」執行