# 2025q1 Homework3 (kxo) contributed by < `leonnig` > ## 教材閱讀紀錄 [linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module) **`__attribute__`** 是 GNU C 提供的一種語法,用來對**函式/變數/型別加上一些額外的屬性**,像是 `noreturn` 、`pure`、`malloc` 等等,而 `alias` 就是其中一種。 ```c // alias ("target") //The alias attribute causes the declaration to be emitted as an alias for another symbol, which must be specified. For instance, void __f () { /* Do something. */; } void f () __attribute__ ((alias ("__f"))); ``` `f()` 是 `__f()` 的別名,不會真的生成 code,而是會轉向 `__f()`。 當 device driver 掛載進入後,使用 `read` 去讀取 kernel space 的資訊,`write` 可以將資料傳送給 kernel space [<Linux Kernel Module Programming Guide> 閱讀紀錄](https://hackmd.io/@Cool-55/B1rJP9nxgx) ## 將棋盤的畫面呈現移動至使用者層級 參考: [rota1001](https://github.com/sysprog21/kxo/commit/b73de5f1a1bf6914c4c7cba97e1a6e7fab46807e) 我的理解是,在原本的實作中,draw_buffer 會透過 `kfifo_in` 寫到 kfifo 中,它的功用是儲存即將要傳送到 user space 的 data,也就是棋盤的資料。 在原本的 `draw_board` 函式中,先觀察一下程式碼 ```c 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(); } ``` 可以看到每當進行了 `draw_buffer` 資料的寫入,後面就會接上 `smp_wmb` ,這邊我認為是為了確保當兩邊的 CPU 上的演算法再進行對弈時,要嚴格保證資料的寫入順序。 這段程式碼其實就是在繪製整個棋盤,總共有 4 列,每列包括了 4 個格子以及 3個 `|` 再加上一個換行字符,所以每列總共8個字元,而每列下面也都會有一個分隔線,也是 8 個字元,總計 8*8 加上開頭兩個換行字符共 66 個字元。 每次都會繪製 66 個字元,並且再經由 `produce_board` insert 到 kfifo buffer 中,在傳給 user space,但因為像是棋盤中除了作答的部分在 kernel space 完成以外,其實其他的棋盤部分繪製的任務可以在 user space 完成,便可以節省傳輸的成本。 將原本 `draw_board` 的部分修改成只傳輸對弈過程寫入 `table` 的資料,也就是 16 個答案格,而剩下 66 - 16 個字元則移動到 user space ```c static int draw_board(char *table) { int i = 0; while (i < N_GRIDS){ draw_buffer[i++] = table[i++]; smp_wmb(); } return 0; } ``` Commit [e904be0](https://github.com/leonnig/kxo/commit/e904be0af6acb2ce37683f867489e706768c83e6#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ec) ## 在對弈過程中顯示當下時間 使用 C 標準函式庫的 [<time.h>](https://pubs.opengroup.org/onlinepubs/7908799/xsh/time.h.html),我讓時間顯示在使用者層級,所以在 `xo.user.c` 中新增以下 ```diff= +time_t now; +struct tm *time_info; +char t_str[20]; /* Draw the board in user space */ static int draw_board(const char *table) { int i = 0, k = 0; printf("\n"); printf("\n"); while (i < 4) { for (int j = 0; j < (BOARD_SIZE << 1) - 1 && k < N_GRIDS; j++) { printf("%c", j & 1 ? '|' : table[k++]); } printf("\n"); for (int j = 0; j < (BOARD_SIZE << 1) - 1; j++) { printf("%c", '-'); } printf("\n"); i++; } + time(&now); + time_info = localtime(&now); + strftime(t_str, 20, "%Y-%m-%d %H:%M-%S", time_info); + printf("\n%s \n", t_str); return 0; } ``` 使用 `strftime` 可以客製化的呈現時間的表示方式。 ## coroutine 待閱讀教材: [〈並行程式設計:排程器原理〉](https://hackmd.io/@sysprog/concurrency-sched) ## corotine 簡而言之,就是快速的在工作間切換以達到像是同時處理多個工作的感覺 ![image](https://hackmd.io/_uploads/HkfURx3Vlx.png) 這邊來觀摩 [coro](https://github.com/sysprog21/concurrent-programs/tree/master/coro) 來學習協同式多工 這邊在看程式碼前,先來了解一下 `setjmp` 跟 `longjmp` ### setjmp ```c int setjmp(jmp_buf env); ``` 此函式能夠在指定位置設定一個目標,而後透過 `longjmp` 轉移到此目標位置。 而 `setjmp()` 會儲存當前執行環境的相關資訊,包括堆疊指標,program counter,可能還包含其他暫存器的值,當呼叫 `setjmp()` 時,會回傳 0。 ### longjmp ```c void longjmp(jmp_buf env, int val); ``` 此函式會利用 `env` 中提供的資訊,把程式控制流程跳回到當初呼叫 `setjmp()` 的位置。 在成功跳回去後,堆疊會恢復成當初呼叫 `setjmp()` 時的狀態,而當 `longjmp` 成功執行後,程式會像是 `setjmp()` 呼叫 `return` 一樣繼續執行。 在 `setjmp()` 第一次被呼叫的時候會回傳 0,再來 `longjmp()` 跳回來的時候,會讓 `setjmp()` 回傳 `longjmp()` 提供的 `val` 參數值。 這邊做個小實驗來體驗一下這兩個函式的使用方式 我們利用 `setjmp()`、`longjmp()` 來實現 c 語言中沒有的 `exception` 功能 ```c #include <stdio.h> #include <stdlib.h> #include <setjmp.h> jmp_buf env; double compute_avg(double *values, size_t length) { if(length <= 0){ longjmp(env, 1); } double sum = 0; for (int i=0; i < length; ++i){ sum += values[i]; } return sum / (double)length; } int main(int argc, char **argv){ int arrlength = argc-1; double *darr = malloc(sizeof(double) * arrlength); for (int i=1; i < argc; ++i){ darr[i-1] = atof(argv[i]); } if (setjmp(env) == 0){ double avg = compute_avg(darr, arrlength); printf("Average = %f\n", avg); } else { printf("ERROR! Invalid inputs. \n"); } return 0; } ``` 該程式利用 `setjmp()` 去判斷 `lonngjmp()` 的回傳值,若輸入值 $\leq 0$ 時,會呼叫 `longjmp()` 轉移到 `setjmp()` 的位置,並輸出錯誤訊息。 以下為 > [coro](https://github.com/sysprog21/concurrent-programs/blob/master/coro/coro.c): 使用 setjmp/longjmp 首先定義了結構體 `task` ,其中有兩個成員,分別為用於 `setjmp()` 的 `jmp_buf` `env` 以及用於排程的 `list`。 在 `main` 函式中,`registered_task` 是函式指標陣列,存放本實驗中的的 3 個任務函式指標。 在 `schedule` 中,會先呼叫 `setjmp()`,每當新的任務加進排程,會呼叫 ` longjmp(sched, 1)`, 讓 `schedule()` 可以繼續將新任務增加到排程,`tasks[i++](&arg)` 則是取出當前的任務,並且 `i++` 輪到下一個排程中的任務,接著 `arg` 這個區域變數的指標傳給 coroutine,這樣 coroutine 可以知道自己是哪個任務的參數,最後 `task_switch()` 去執行下一個 coroutine。 可以發現 `while (ntasks-- > 0)`,其實 `schedule` 只有把一開始的 3 個任務加入到循環中就沒他的事了,後續真正的 corotine 是交由 `task_switch` 來執行。 ```c void schedule(void) { static int i; setjmp(sched); while (ntasks-- > 0) { struct arg arg = args[i]; tasks[i++](&arg); printf("Never reached\n"); } task_switch(); } ``` `task0` 和 `task1` 的結構都一樣,前半段都會先到 `schedule` 執行完 ``` printf("%s: n = %d\n", task->task_name, task->n);``` 後,再用 `task_add()` 加入到 `tasklist` 中,並再次呼叫 `longjmp` 跳回到 `schedule`,直到 `schedule` 的 `ntasks` 都執行完畢,再藉由 `task_switch` 的 ` longjmp(t->env, 1);` 跳回 `task` 執行主要的操作。