# 2023q1 Homework6 (quiz6) contributed by < `SPFishcool` > ## 開發環境 ```bash $ neofetch --stdout fico@fico-mac ------------- OS: Arch Linux ARM aarch64 Host: Apple MacBook Air (13-inch, M2, 2022) Kernel: 6.2.0-asahi-11-1-edge-ARCH Uptime: 16 hours, 7 mins Packages: 1235 (pacman) Shell: zsh 5.9 Resolution: 2560x1600 DE: Plasma 5.27.4 WM: KWin Theme: [Plasma], Breeze [GTK2/3] Icons: [Plasma], breeze [GTK2/3] Terminal: konsole Terminal Font: Source Code Pro Semibold 16 CPU: (8) @ 2.424GHz Memory: 4706MiB / 15649MiB $ gcc -v Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/aarch64-unknown-linux-gnu/12.1.0/lto-wrapper Target: aarch64-unknown-linux-gnu Configured with: /build/gcc/src/gcc/configure --enable-languages=c,c++,fortran,go,lto,objc,obj-c++ --enable-bootstrap --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://github.com/archlinuxarm/PKGBUILDs/issues --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-linker-build-id --enable-lto --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-multilib --disable-werror --host=aarch64-unknown-linux-gnu --build=aarch64-unknown-linux-gnu --with-arch=armv8-a --enable-fix-cortex-a53-835769 --enable-fix-cortex-a53-843419 Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 12.1.0 (GCC) ``` ## 測驗一 ### 程式原理分析 struct 結構: - `struct task` - `struct arg` function: - `task_add` - `task_switch` - `schedule` - `fib_sequence` - `task0`、`task1` 測驗一的程式碼運用 `setjmp`、`longjmp` 來執行分時多工,根據 man page 的敘述,`setjmp` 會使用 `jmpbuf` 物件設置跳躍 label,當執行 `setjmp(jmpbuf)` 時此函數會返回 `0`,當呼叫 `longjmp(jmpbuf, val)` 時會跳回 `setjmp` 設置的 label 並將此次 `setjmp` 的返回值設為 `val`。 在 `task0`、`task1` 代表要執行的任務,函式內運用下列的敘述來實現 Coroutine 的 await 和 yield ```c for (; task->i < task->n; task->i++) { if (setjmp(task->env) == 0) { printf("%s %d\n", task->task_name, task->i); task_add(task); task_switch(); } task = cur_task; printf("%s: resume\n", task->task_name); } ``` 在這裡可以看到,當第一次進入迴圈時,會先 `setjmp`,當完成第一次任務便會將 task 加到 `list_head` 的後面,接下來 `task_switch` 再將目前第一個 `task` 取出並用 `longjmp` 跳到下一個 `task` 的迴圈內,而 `longjmp` 會跳到程式碼內 `setjmp` 的位置並回傳 `1`,所以不會進入 `if` 內部,而下一個迴圈會再重新設置一次 `setjmp`。 ```c static void task_switch() { if (!list_empty(&tasklist)) { struct task *t = list_first_entry(&tasklist, struct task, list); list_del(&t->list); cur_task = t; longjmp(t->env, 1); } } ``` 當我再思考 `setjmp` 與 `longjmp` 使用時記憶體配置的問題時,決定試試 valgrind 測試 memory leak。 ```bash valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./coroutine ``` ``` ==9442== 352 bytes in 1 blocks are still reachable in loss record 1 of 3 ==9442== at 0x4865408: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so) ==9442== by 0x120F83: task1 (coroutine.c:110) ==9442== by 0x120C33: schedule (coroutine.c:54) ==9442== by 0x1211B7: main (coroutine.c:152) ==9442== ==9442== 352 bytes in 1 blocks are indirectly lost in loss record 2 of 3 ==9442== at 0x4865408: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so) ==9442== by 0x120DF7: task0 (coroutine.c:77) ==9442== by 0x120C33: schedule (coroutine.c:54) ==9442== by 0x1211B7: main (coroutine.c:152) ==9442== ==9442== 704 (352 direct, 352 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3 ==9442== at 0x4865408: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so) ==9442== by 0x120DF7: task0 (coroutine.c:77) ==9442== by 0x120C33: schedule (coroutine.c:54) ==9442== by 0x1211B7: main (coroutine.c:152) ``` 偶然發現執行程式有 memory leak 的問題,發現程式碼內部有 `malloc task` 卻沒有 `free`,因此推測這些 memory leak 應該是這個問題,因此當我再 task 結束時 `free` 掉已經結束的 task。 ```c printf("%s: complete\n", task->task_name); free(task); longjmp(sched, 1); } ``` 在執行一次 `valgrind`。 ``` ==9522== All heap blocks were freed -- no leaks are possible ``` 之後測試發現這個程式碼有一個限制,就是 task function 不可宣告會變化的區域變數(除了 `const`),因為同一種 task function 的 task 會共享這些變數,以下我將`task0` 迴圈內的 `i` 改成區域變數: ```c for (int i = 0; i < 10; i++) { if (setjmp(task->env) == 0) { printf("%s's i = %d\n", task->task_name, i); task_add(task); task_switch(); } task = cur_task; printf("%s: resume\n", task->task_name); } ``` 發現輸出變成 ``` Task 0's i = 1 Task 1: resume Task 1's i = 2 Task 2: resume Task 2 1 Task 0: resume Task 0's i = 3 Task 1: resume Task 1's i = 4 Task 2: resume Task 2 2 Task 0: resume Task 0's i = 5 Task 1: resume Task 1's i = 6 Task 2: resume Task 2 3 Task 0: resume Task 0's i = 7 Task 1: resume Task 1's i = 8 Task 2: resume Task 2 4 Task 0: resume Task 0's i = 9 Task 1: resume Task 1: complete ``` 而 `struct task` 內部也只有 `n` 和 `i` 兩個整數宣告,代表 task 的架構被變數數量綁死了。 為了增加 task function 變數宣告的彈性,學生覺得可以在 `struct task` 內部加入 `void **var`,`void *` 是一個未宣告任何資料形態的指標,因此可以將 `void **` 視為 pointer to pointer to any 的二維陣列,建立一個 `void **` 存取指向各個不同類型的陣列。 ```c struct task { jmp_buf env; struct list_head list; char task_name[10]; void **var; //variable array }; struct arg { void **var; //variable array char *task_name; }; ``` 因為這個變數是有彈性的,因此可以額外寫一個建構 task's arg alloc & free function,以 task0 為例: ```c void **task0_arg(int i, int n) { void **arg = malloc(sizeof(void *) * 2); arg[0] = malloc(sizeof(int) * 2); arg[1] = malloc(sizeof(long long) * n); ((int *)arg[0])[0] = i; ((int *)arg[0])[1] = n; ((long long *)arg[1])[0] = 0; ((long long *)arg[1])[1] = 1; return arg; } ``` :::warning 程式碼縮排使用 4 個空白字元 :notes: jserv ::: ```c void task0_free(struct task *task) { free(task->var[0]); free(task->var[1]); free(task->var); free(task); } ``` 我將 `task0` 的參數配置改成: - `arg[0][0]`: i - `arg[0][1]`: n - `arg[1]`: fibonacci array 不過使用這種方法,建構 task 時就會需要注意每一組陣列的形態,使用時也需要提供資料形態的資訊。 ```c for (;((int *)task->var[0])[0] < ((int *)task->var[0])[1]; ((int *)task->var[0])[0] += 2) { if (setjmp(task->env) == 0) { long long res = fib_sequence(((int *)task->var[0])[0], ((long long *)task->var[1])); printf("%s fib(%d) = %lld\n", task->task_name, ((int *)task->var[0])[0], res); task_add(task); task_switch(); } task = cur_task; printf("%s: resume\n", task->task_name); } ``` 編譯執行後達到預期輸出 ``` Task 0: n = 0 Task 1: n = 1 Task 2: n = 70 Task 0 fib(0) = 0 Task 1 fib(1) = 1 Task 2 0 Task 0: resume Task 0 fib(2) = 1 Task 1: resume Task 1 fib(3) = 2 Task 2: resume Task 2 1 ... Task 0 fib(68) = 72723460248141 Task 1: resume Task 1 fib(69) = 117669030460994 Task 2: resume Task 2 34 Task 0: resume Task 0: complete Task 1: resume Task 1: complete Task 2: resume Task 2 35 ... Task 2: resume Task 2 68 Task 2: resume Task 2 69 Task 2: resume Task 2: complete ``` 優點:變數宣告較具彈性 缺點:task 建立需要更謹慎,撰寫比較困難 ### minicoro 探討與分析 在 [minicoro](https://github.com/edubart/minicoro/blob/main/minicoro.h) 裡面有針對 cpu 種類對其做分類,某些程式碼對於不同 cpu 有不同程式碼,因此我針對我的 cpu 類型 - aarch64 找到其部份程式碼,並發現有幾行 inline assembly 程式碼。 #### a64 architecture register 在理解 inline assembly 時,參考了一下 [Learn the architecture - A64 Instruction Set Architecture](https://developer.arm.com/documentation/102374/0101/Registers-in-AArch64---general-purpose-registers) 認識 aarch64 暫存器的架構 在 aarch64 有 32 顆 一般目的暫存器(Generate Purpose Register),每一顆都可以當作 64-bit (`X0`~`X31`) 和 32-bit (`W0`~`W31`) 暫存器使用,只需要在操作上決定是 x 或 w 即可。 ```armasm ADD X0, X1, X2 ``` > 64-bit 加法操作 ```armasm ADD W0, W1, W2 ``` > 32-bit 加法操作 另外還有一組 32 顆專門運算浮點數以及向量的暫存器,這一組也可以當作 8-bit ( `B0` )、16-bit( `H0` )、32-bit ( `S0` )、64-bit ( `D0` )、128-bit ( `Q0` ) 浮點數和向量 ( `v0` ) ```armasm FADD S0, S1, S2 ``` 如果將這些視作向量暫存器( `v0` ),則可以將 128-bit 當作多個獨立的數值,就可以這樣寫: ```armasm FADD V0.2D, V1.2D, V2.2D ``` 也可以做整數操作 ```armasm ADD V0.2D, V1.2D, V2.2D ``` 還有 `sp`(stack pointer)、`xzr`(64-bit zero register)、`wzr`(32-bit zero register)、`LR`(Link Register) 等特殊用途暫存器。 #### minicoro inline assembly 接下來可以看看程式碼 ```c int _mco_switch(_mco_ctxbuf* from, _mco_ctxbuf* to); __asm__( ".text\n" #ifdef __APPLE__ ".globl __mco_switch\n" "__mco_switch:\n" #else ".globl _mco_switch\n" ".type _mco_switch #function\n" ".hidden _mco_switch\n" "_mco_switch:\n" #endif " mov x10, sp\n" " mov x11, x30\n" " stp x19, x20, [x0, #(0*16)]\n" " stp x21, x22, [x0, #(1*16)]\n" " stp d8, d9, [x0, #(7*16)]\n" " stp x23, x24, [x0, #(2*16)]\n" " stp d10, d11, [x0, #(8*16)]\n" " stp x25, x26, [x0, #(3*16)]\n" " stp d12, d13, [x0, #(9*16)]\n" " stp x27, x28, [x0, #(4*16)]\n" " stp d14, d15, [x0, #(10*16)]\n" " stp x29, x30, [x0, #(5*16)]\n" " stp x10, x11, [x0, #(6*16)]\n" " ldp x19, x20, [x1, #(0*16)]\n" " ldp x21, x22, [x1, #(1*16)]\n" " ldp d8, d9, [x1, #(7*16)]\n" " ldp x23, x24, [x1, #(2*16)]\n" " ldp d10, d11, [x1, #(8*16)]\n" " ldp x25, x26, [x1, #(3*16)]\n" " ldp d12, d13, [x1, #(9*16)]\n" " ldp x27, x28, [x1, #(4*16)]\n" " ldp d14, d15, [x1, #(10*16)]\n" " ldp x29, x30, [x1, #(5*16)]\n" " ldp x10, x11, [x1, #(6*16)]\n" " mov sp, x10\n" " br x11\n" #ifndef __APPLE__ ".size _mco_switch, .-_mco_switch\n" #endif ); ``` 上方程式碼是 `_mco_switch` 的 inline assembly,其中最多使用的指令為 `stp` 和 `ldp`。 - `stp`:可以一次將兩個暫存器資料移到指定記憶體位址,以下方程式為例,`x` 如之前所說是 `64-bit` 暫存器資料,因此這行程式碼會將 `x19` 和 `x20` 裡面存取的資料放到 `x0 + (0 * 16)` 的位置上。 ```armasm stp x19, x20, [x0, #(0 * 16)] ``` - `ldp`:與 `stp` 相反,會將 `x1 + (0 * 16)` 位置的資料存到 `x19` 和 `x20` 兩個暫存器上。 ```armasm ldp x19, x20, [x1, #(0*16)] ``` 在 aarch64 架構中,函式的參數會依序放到 `x0` ~ `x3` 裏面,若超過數量則會將剩下的參數存至 stack 中,`_mco_switch` 將兩個 `_mco_ctxbuf` 作為輸入,因此 `x0` 為 `_mco_ctxbuf *from`,`x1` 為 `_mco_ctxbuf *to`。 而 `_mco_ctxbuf` 為以下結構,可以看出來它將暫存器數據都存在裡面 ```c // context information typedef struct _mco_ctxbuf { void *x[12]; /* x19-x30 */ void *sp; void *lr; void *d[8]; /* d8-d15 */ } _mco_ctxbuf; ``` 而這行 inline assembly 做的事就是將暫存器的數據依序放到 `_mco_ctxbuf` 裡面來,並把要切換的 `_mco_ctxbuf` 載入暫存器當中,裡面的資訊包括 `sp` ( stack pointer ) 和 `lr`( link register, `x30` ),完成 context switch 後使用 `br` 跳至切換後的 link register,因此可以做到 coroutine 與 main function 之間 switch 的功用。 ```c mco_result mco_resume(mco_coro* co) { if(!co) { MCO_LOG("attempt to resume an invalid coroutine"); return MCO_INVALID_COROUTINE; } if(co->state != MCO_SUSPENDED) { /* Can only resume coroutines that are suspended. */ MCO_LOG("attempt to resume a coroutine that is not suspended"); return MCO_NOT_SUSPENDED; } co->state = MCO_RUNNING; /* The coroutine is now running. */ _mco_jumpin(co); return MCO_SUCCESS; } ``` 要切換進入 ( `resume` ) 到一個 corotine function 時,他會先檢查狀態是否為 `MCO_SUSPENDED`, 並設定成 `MCO_RUNNING`,並呼叫 `_mco_jumpin` 正式跳入 coroutine。 ```c static void _mco_jumpin(mco_coro* co) { _mco_context* context = (_mco_context*)co->context; _mco_prepare_jumpin(co); _mco_switch(&context->back_ctx, &context->ctx); /* Do the context switch. */ } ``` 在 `_mco_jumpin` 會使用 `_mco_switch` 將目前的 context 紀錄到 `mco_coro` 裡 `context` 的 `back_ctx` ( `back_ctx` 的作用就是在 `jumpin` 時將跳躍點記下來,好讓 yield 時跳回原來的地方)。 ```c typedef struct _mco_context { #ifdef MCO_USE_VALGRIND unsigned int valgrind_stack_id; #endif _mco_ctxbuf ctx; _mco_ctxbuf back_ctx; } _mco_context; ``` 而跳出( `yield` )coroutine function 時基本上跟 `resume` 相反,檢查狀態是否為 `MCO_RUNNING` 並設置為 `MCO_SUSPENDED`,然後呼叫 `_mco_jumpout` 跳出。 ```c mco_result mco_yield(mco_coro* co) { if(!co) { MCO_LOG("attempt to yield an invalid coroutine"); return MCO_INVALID_COROUTINE; } #ifdef MCO_USE_ASYNCIFY /* Asyncify already checks for stack overflow. */ #else /* This check happens when the stack overflow already happened, but better later than never. */ volatile size_t dummy; size_t stack_addr = (size_t)&dummy; size_t stack_min = (size_t)co->stack_base; size_t stack_max = stack_min + co->stack_size; if(co->magic_number != MCO_MAGIC_NUMBER || stack_addr < stack_min || stack_addr > stack_max) { /* Stack overflow. */ MCO_LOG("coroutine stack overflow, try increasing the stack size"); return MCO_STACK_OVERFLOW; } ``` --- ## 測驗二 ## 測驗三 ## 測驗四