# 2023q1 Homework5 (quiz6) contributed by < `Jerejere0808` > ## 測驗 1 --- ## 測驗 2 先解釋 ```c static void local_irq_save(sigset_t *sig_set) { sigset_t block_set; sigfillset(&block_set); sigdelset(&block_set, SIGINT); sigprocmask(SIG_BLOCK, &block_set, sig_set); } static void local_irq_restore(sigset_t *sig_set) { sigprocmask(SIG_SETMASK, sig_set, NULL); } ``` `local_irq_save` 函式接受一個指向 `sigset_t` 變數的指標 `sig_set` 作為參數。它建立了一個新的 signal 集合 `block_set`,並使用 sigfillset 函式將其中的所有信號填充為 1,表示將所有信號都加入集合中。接著,它使用 `sigdelset` 函式將 `SIGINT` 從集合中移除,表示解除 SIGINT 信號的遮罩。最後,使用 `sigprocmask` 函式將信號遮罩設置為 `block_set`,並將結果保存在 `sig_set` 中,以便稍後恢復。如此一來,除了中斷信號 SIGINT 之外,行程就不會受到其他的信號影響。 `local_irq_restore` 函式接受一個指向 `sigset_t` 變數的指標 sig_set 作為參數,該變數應先前由 `local_irq_save` 函式保存。它使用 `sigprocmask` 函式將遮罩恢復為 sig_set 中保存的值,並使用 `SIG_SETMASK` 標誌,表示解除所有信號的遮罩,包括之前被 `local_irq_save` 函式遮罩的信號。 這兩個函式可以確保再某段程式執行時防止 interrupt,以確保臨界區域內的資源操作不會被中斷,從而避免競爭條件的發生或是搶佔 (preempt) 然後執行完再恢復。 另外,可以看到程式也用了一個 `preempt_count` 判斷是否允許搶佔。 ```c static int preempt_count = 0; static void preempt_disable(void) { preempt_count++; } static void preempt_enable(void) { preempt_count--; } ``` 不過須注意的是 `preempt_count` 是在 timer handler 觸發之後被檢查是否可以 preempt 的,也就是說原本的 task 還是會被中斷,只是不會被 preempt。 ### 程式碼運作原理 利用 `SIGALRM` signal 來實作搶佔式多工,原理是藉由 `SIGALRM` signal 來模擬作業系統的 timer 中斷。 透過 ualarm 每過一段時間發送一個 SIGALRM 給行程。 ```c static void timer_create(unsigned int usecs) { ualarm(usecs, usecs); } ``` 暫停程式的執行直到收到 `SIGALRM` 達到等待定時器事件的功能 ```c static void timer_wait(void) { sigset_t mask; sigprocmask(0, NULL, &mask); sigdelset(&mask, SIGALRM); sigsuspend(&mask); } ``` 首先,宣告了一個名為 mask 的 `sigset_t` 型別的變數,用於存放 signal 遮罩的設定。 接著,使用 `sigprocmask` 函式獲取行程的 signal 遮罩設定,並將其保存到 mask 變數中。`sigprocmask(0, NULL, &mask)` 中的參數 0 表示不進行任何修改,NULL 表示不保存之前的 signal 遮罩設定。 接下來,使用 sigdelset 函式從 mask 變數中刪除 `SIGALRM`,即將 `SIGALRM` 從 signal 遮罩中解除封鎖,使得該 signal 可以被接收。 最後,使用 `sigsuspend` 函式將程式暫停,等待 signal 的到來。`sigsuspend` 會將 signal 遮罩設定為 mask 中的值,並等待任何 signal 的到來,一旦有 signal 進入,則會解除暫停,並執行 signal 處理函式。在這段程式碼中,只有 `SIGALRM` 被解除封鎖,因此只有當 `SIGALRM` 發生時,sigsuspend 才會返回。這樣就實現了等待定時器事件的功能。 設定當收到 SIGALRM 要去執行的 handler ```c static void timer_init(void) { struct sigaction sa = { .sa_handler = (void (*)(int)) timer_handler, .sa_flags = SA_SIGINFO, }; sigfillset(&sa.sa_mask); sigaction(SIGALRM, &sa, NULL); } ``` 首先,宣告了一個名為 sa 的 `struct sigaction` 型別的變數,用於設定對 `SIGALRM` 的處理方式。 接著,使用結構初始化器將 `sa_handler` 欄位設定為 `timer_handler` 函式的位址,表示當接收到 `SIGALRM` 時,要執行 `timer_handler` 函式來處理該 signal。 同時,設定 sa_flags 欄位為 `SA_SIGINFO`,表示希望使用擴展型的 signal 處理方式,以便在處理函式中獲取更多的訊息。 接下來,使用 sigfillset 函式將 sa_mask 欄位中的 signal 集合設定為擁有所有 signal,這表示在 timer_handler 函式中不會被其他 signal 中斷。 最後,使用 `sigaction` 函式將 sa 中的設定應用到 `SIGALRM` 上,即設定了當接收到 `SIGALRM` 時的處理方式為執行 timer_handler 函式。並且由於第三個參數為 NULL,表示不保存之前的處理方式。 這樣,當程式接收到 `SIGALRM` 時,會執行 timer_handler 函式來進行相應的處理,實現了定時器的初始化設定。 因為每當程式接收到 `SIGALRM` 時就會去執行 `timer_handler` , 所以若是將 `schedule` 的部分放在 `timer_handler`,就能實現每過一段時間就去排程並且切換到相對應的 context 執行。 ```c static void timer_handler(int signo, siginfo_t *info, ucontext_t *ctx) { if (preempt_count) /* once preemption is disabled */ return; /* We can schedule directly from sighandler because Linux kernel cares only * about proper sigreturn frame in the stack. */ schedule(); } static void schedule(void) { sigset_t set; local_irq_save(&set); struct task_struct *next_task = list_first_entry(&task_current->list, struct task_struct, list); if (next_task) { if (task_current->reap_self) list_move(&task_current->list, &task_reap); task_switch_to(task_current, next_task); } struct task_struct *task, *tmp; list_for_each_entry_safe (task, tmp, &task_reap, list) /* clean reaps */ task_destroy(task); local_irq_restore(&set); } ``` 另外注意到 `task_add` 的程式碼 ```c makecontext(&task->context, (void (*)(void)) task_trampoline, 2, ptr.i[0], ptr.i[1]); /* When we switch to it for the first time, timer signal must be blocked. * Paired with task_trampoline(). */ sigaddset(&task->context.uc_sigmask, SIGALRM); ``` `sigaddset(&task->context.uc_sigmask, SIGALRM)` 的部分,可以看到在加入新 task 的時候,把 `SIGALRM` 先遮蔽住,意義在於避免 task 在開始執行其 sort 之前就被 `SIGALRM` 影響而被切換掉。 而 `makecontext(&task->context, (void (*)(void)) task_trampoline, 2, ptr.i[0], ptr.i[1])` 就是在執行到這個新加入的 task 時,去執行 task_trampoline , 所以可以預期到 `task_trampoline` 應該要去執行 task 裡的 sort 函式。 ```c __attribute__((noreturn)) static void task_trampoline(int i0, int i1) { union task_ptr ptr = {.i = {i0, i1}}; struct task_struct *task = ptr.p; /* We switch to trampoline with blocked timer. That is safe. * So the first thing that we have to do is to unblock timer signal. * Paired with task_add(). */ local_irq_restore_trampoline(task); task->callback(task->arg); task->reap_self = true; schedule(); __builtin_unreachable(); /* shall not reach here */ } ``` :::warning TODO: 討論為何用 trampoline,及分析其作用。 :notes: jserv ::: 跟預期的一樣透過 `task->callback(task->arg)` 執行 task 裡的 sort(),另外,也可以發現 l`ocal_irq_restore_trampoline(task)` 把屏蔽 `SIGALRM` 的部分取消,這樣讓 task sort 時 就可以接收 `SIGALRM` 從而觸發 timer handler 去執行另外的 task。 :::danger 另外我也用 GDB 試圖追蹤程式的運作,但是發現在中斷的時候,程式的 timer 似乎依然在計時。原本我在 sort() 設立斷點預期可以在中斷後用 next 往下一行行執行,然後看到 task1 在執行 sort 的某行程式碼會突然被 timer interrupt 並切換到 task2 sort 的某行程式的這個過程。但是因為 GDB 中斷程式時 timer 繼續導致我在 next 時,永遠都卡在 sort() 的進入點,因為 GDB 執行 next 之前 timer 就到了,導致還沒執行 sort 裡面的下一行程式碼就被切換到另一個 task,如下: ```c (gdb) next Breakpoint 1, sort (arg=0x555555556065) at signal_round_robin.c:224 224 char *name = arg; (gdb) next Breakpoint 1, sort (arg=0x555555556067) at signal_round_robin.c:224 224 char *name = arg; (gdb) next Breakpoint 1, sort (arg=0x555555556063) at signal_round_robin.c:224 224 char *name = arg; ... ``` > 使用 GDB 時,要留意 [Signals](https://sourceware.org/gdb/onlinedocs/gdb/Signals.html),可事先閱讀 `info handle` 和 `info signal` 的輸出,以遮蔽/處理特定的 signal :notes: jserv ::: ## 測驗 3 ## 測驗 4