# 2025q1 Homework4 (quiz3+4)
contributed by: <``Beethovenjoker``> (章元豪) [Github](https://github.com/Beethovenjoker/lab0-c)
## 2025q1 第 3 週測驗題
### 測驗 `1`
```c
/* ceiling division without needing floating-point operations. */
static size_t ceil_div(size_t n, size_t d)
{
return (n+d-1)/d;
}
```
取上界的除法,可以將情況歸納成:
- 有餘數 (1~d-1)。
- 當 n 除以 d 有餘數時,餘數的範圍是 1 到 d - 1,這時候我們希望商要進位,也就是要加一。
- 沒有餘數 (0)。
- 而當沒有餘數時,餘數為 0,商本來就是正確的,不需要改變。
為了讓這兩種情況可以統一處理,我們可以在除法之前先對被除數加上 d - 1。這樣做的好處是如果原本沒有餘數,像是 n = 93、d = 31,加上 d - 1 = 30 之後變成 123,除以 31 仍然是 3,不會改變原本的商。但如果原本有餘數,像是 n = 94、d = 31,加上 30 變成 124,這時候除以 31 就會從 3 變成 4,達到進位的效果。
```c
#define INTMAX 0x7fffffff
void mpi_set_u64(mpi_t rop, uint64_t op)
{
size_t capacity = ceil_div(64, 31);
mpi_enlarge(rop, capacity);
for (size_t n = 0; n < capacity; ++n) {
rop->data[n] = op & INTMAX;
op >>= 31;
}
for (size_t n = capacity; n < rop->capacity; ++n)
rop->data[n] = 0;
}
```
這個函數是將一個 64 位元的無號整數轉換成多精度整數 mpi_t。由於 mpi_t 中的 data 陣列每個元素只使用 31 個位元儲存有效資料,因此會先用 `ceil_div(64, 31)` 計算將 64 位元整數分割成幾個 31 位元區段所需的容量,這裡會得到 3,表示最多需要 3 個 31 位元的欄位。
接著使用 `& 0x7fffffff` 取出最低的 31 位元並存入 `rop->data[n]`,再將 op 向右位移 31 位元,準備處理下一段資料。這樣每次迴圈就會處理一個 31 位元區段,直到整個 64 位元數字被分解完。若 `rop->capacity` 比實際需要的容量更大,就將剩下未使用的欄位填為 0,確保沒有殘留舊資料。
```c
for (size_t n = 0; n < op1->capacity; ++n) {
for (size_t m = 0; m < op2->capacity; ++m) {
uint64_t r = (uint64_t) op1->data[n] * op2->data[m];
uint64_t c = 0;
for (size_t k = m+n; c || r; ++k) {
if (k >= tmp->capacity)
mpi_enlarge(tmp, tmp->capacity + 1);
tmp->data[k] += (r & INTMAX) + c;
r >>= 31;
c = tmp->data[k] >> 31;
tmp->data[k] &= INTMAX;
}
}
}
```
類似直式乘法的運算,被乘數第 n 位與乘數第 m 位相乘時,其結果從第 n + m 位開始儲存。
```c
for (size_t i = start; i != (size_t) -1; --i) {
mpi_mul_2exp(r, r, 1);
if (mpi_testbit(n0, i) != 0)
mpi_setbit(r, 0);
if (mpi_cmp(r, d0) >= 0) {
mpi_sub(r, r, d0);
mpi_setbit(q, i);
}
}
```
也是類似直式除法,從被除數的最高位元開始逐位處理。每次迴圈會先將目前的餘數乘以 2,也就是左移一位元。接著從被除數中取出當前位元補到餘數的最低位,再判斷是否能夠減去除數,若可以就執行減法並在商中對應的位置設為 1,否則進入下一輪處理。
```c
void mpi_gcd(mpi_t rop, const mpi_t op1, const mpi_t op2)
{
if (mpi_cmp_u32(op2, 0) == 0) {
mpi_set(rop, op1);
return;
}
mpi_t q, r;
mpi_init(q);
mpi_init(r);
mpi_fdiv_qr(q, r, op1, op2);
mpi_gcd(rop,op2,r);
mpi_clear(q);
mpi_clear(r);
}
```
輾轉相除法,把餘數和 op2 代入 `mpi_gcd()`。
### 測驗 `2`
```c
/* Nonzero if X (a long int) contains a NULL byte. */
#define DETECT_NULL(X) \
(((X) - (0x0101010101010101)) & ~(X) & (0x8080808080808080))
```
這個巨集透過 SWAR 技巧,如果 X 中包含 NULL byte(0x00)時回傳 `非 0`。
```c
#define DETECT_CHAR(X, mask) DETECT_NULL(X^mask)
```
這個巨集的用途是快速判斷 X 中是否出現過指定字元。它會先將 X 中的每個 byte 與 mask 進行 XOR 運算,若其中有 byte 和目標字元相同,該位元組會變為 0x00,接著透過 `DETECT_NULL` 偵測是否存在 NULL byte,以此達成匹配檢查。
```c
unsigned long mask = d << 8 | d;
mask |= mask << 16;
for (unsigned int i = 32; i < LBLOCKSIZE * 8; i <<= 1)
mask |= mask<<i;
```
```c
mask = 0xdddddddddddddddd
```
這段使用了典型的 SWAR 技巧,用來把單一字元 d 複製到整個 32 位或 64 位元的 mask 裡,每個 byte 都會變成 d。
```c
while (len >= LBLOCKSIZE) {
if (DETECT_CHAR(*asrc, mask))
break;
asrc++;
len -= LBLOCKSIZE;
}
```
在字串長度足夠,且指標已對齊至 long 邊界的情況下,這段程式會使用 SWAR 技巧,一次處理一個 word(也就是一個 block)大小的資料,加速搜尋目標字元。此時 asrc 是指向當前區塊的指標,而 *asrc 才是實際要拿來比對的資料內容,因此傳入 DETECT_CHAR 的應該是 *asrc,用來檢查該區塊中是否包含目標字元。
```c
while (len--) {
if (*src == d)
return (void *) src;
src++;
}
```
這段是標準的fallback 搜尋階段,當 SWAR 無法處理的資料(例如剩下不足一整個 word 的尾端)或在未對齊的初始區段中,就使用這段程式碼逐 byte 比對目標字元,直到找到或資料結束為止。
### 測驗 `3`
```c
int coro_create(coro_entry entry, void *args)
{
/* Search for an available environment slot */
int env;
for (env = 0; env < NENV; env++) {
if (envs[env].status == ENV_UNUSED) /* Found a free environment */
break;
}
if (env == NENV) /* No free environments available */
return -1;
envs[env].status = ENV_RUNNABLE;
/* Initialize the context for the new user-level thread */
getcontext(&envs[env].state);
make_stack(&envs[env].state);
envs[env].state.uc_link = &exiter;
makecontext(&envs[env].state, (void (*)(void)) entry, 1, args);
/* Return the identifier of the newly created user-level thread */
return env;
}
```
建立一個新的使用者層級執行緒(user-level thread / coroutine),設定其執行狀態與context,並回傳其 ID(index)。
```c
int env;
for (env = 0; env < NENV; env++) {
if (envs[env].status == ENV_UNUSED)
break;
}
if (env == NENV)
return -1;
```
這段會從 envs[] 陣列中尋找一個狀態為 `ENV_UNUSED` 的空槽,如果全部都在使用中,就回傳 -1 代表失敗。
```c
envs[env].status = ENV_RUNNABLE;
```
找到空槽後,把這個槽位標記為 `RUNNABLE`,表示這個 coroutine 可以被排程執行。
```c
static void coro_schedule(void)
{
int attempts = 0;
while (attempts < NENV) {
int candidate = (curenv + attempts + 1) % NENV;
if (envs[candidate].status == ENV_RUNNABLE) {
curenv = candidate;
/* Request delivery of TIMERSIG after 10 ms */
timer_settime(timer, 0, &ts, NULL);
setcontext(&envs[curenv].state);
}
attempts++;
}
exit(0);
}
```
這段程式碼是 coroutine scheduler 的實作,負責選出下一個應該執行的 task(user-level thread,即 coroutine),並將執行控制權轉移給它。排程策略採用round-robin 方法,從目前執行中的 curenv 的下一個位置開始,依序搜尋狀態為 `ENV_RUNNABLE` 的 coroutine。一旦找到,就設定定時器並切換至該 coroutine 執行 context;若沒有任何 coroutine 可執行,則結束程式。
```c
int candidate = (curenv + attempts + 1) % NENV;
```
`+1` 是為了讓排程從目前 coroutine 的「下一個」位置開始尋找 `ENV_RUNNABLE` 的 coroutine,避免排程器重新選到當前正在執行的 coroutine 本身,這樣才能實現真正的 round-robin排程效果。
```c
void coro_yield(void)
{
envs[curenv].state_reentered = 0;
getcontext(&envs[curenv].state);
if (envs[curenv].state_reentered++ == 0) {
/* Context successfully saved; schedule the next user-level thread to
* run.
*/
coro_schedule();
}
/* Upon re-entry, simply resume execution */
}
```
這段程式碼的功能是讓目前正在執行的 coroutine 主動讓出 CPU,暫停自己,並交由排程器選擇下一個 coroutine 來執行。
```c
static void preempt(int signum UNUSED,
siginfo_t *si UNUSED,
void *context UNUSED)
{
coro_yield();
}
```
這段程式碼是 coroutine 系統中用來模擬 preemption 的 signal handler。當定時器送出 signal(例如 SIGRTMIN)時,系統就會進入 preempt() 函數。其目的在於中斷目前正在執行的 coroutine,並觸發排程器進行 coroutine 切換。因此,應呼叫的是主動讓出控制權的函式 `coro_yield()`。
## 2025q1 第 4 週測驗題
### 測驗 `1`
## Reference
- [2025q1 第 3 週測驗題](https://hackmd.io/@sysprog/linux2025-quiz3)
- [2025q1 第 4 週測驗題](https://hackmd.io/@sysprog/linux2025-quiz4)