Unix V6 on PDP-11 architecture - Process === ###### tags: `unix_v6` `kernel` ## 目錄 [TOC] ## process lifetime ```flow st1=>start: Parent process st2=>start: Child process e1=>end: Parent process keep exec e2=>end: Child process end sub1=>subroutine: System Call: fork() cond1=>condition: Parent? cond2=>condition: Child end? op1_0=>operation: Parent exec op1_1=>operation: Parent wait op2_1=>operation: Child exec op2_2=>operation: Child exit st1->op1_0->sub1->cond1->op1_1->cond2->e1 st2->op2_1->op2_2->e2 cond1(yes)->op1_1 cond1(no)->st2 cond2(yes)->e1 cond2(no)->op1_1 ``` ## process creation - 透過將 parent process 的 Data 複製到 child process,來創建新的 process,這個過程透過 system call - `fork()` 來完成 - 被複製的 data 有: 1. proc\[\] 裡的 elements,並把新複製出來的其中一個 element: proc.p_ppid point 到舊的 proc.p_pid。(即把 child parent_pid 欄位設為 parent 的 pid) 2. WHOLE Data Segment - including PPDA - 讓 Child process 的 `struct user` 的 `user.u_procp` points to 對應的 `struct proc` - Text Segment 不會被複製,Parent and Child share the same Text Segment - 若 Child process 執行了其他 process,那會解除共享 Text Segment 的關係。 ### How do parent find his children? Iterate all the elements in proc\[\] to check the proc.p_ppid,若這個有指到自己的 proc.p_pid,那他就是自己的 child。 ### 實際上的 creation 以及 kernel 裡的運作 整體流程: ```graphviz digraph dfd { node[shape=record] call1 [label="<f0> user invoked\n fork()"]; proc2 [label="{<f0> \n\nsystem call \nfork()\n\n\n | {<f1> subroutuine:\nnewproc() | <f2> subroutine:\npanic()} }" shape=Mrecord]; return [label="<f1> return back to user space \n and head for next instruction."]; call1:f0->proc2; proc2->return:f1; } ``` ### System call - `fork()` 先看以下程式碼: **list 1.** C語言呼叫 fork 的 assembly, is at user space ```clike= .globl _fork, cerror, _par_uid _fork: mov r5,-(sp) mov sp,r5 sys fork br 1f bec 2f jmp cerror 1: mov r0,_par_uid clr r0 2: mov (sp)+,r5 rts pc .bss _par_uid: .=.+2 ``` ======================================= **list 2.** system call fork(), is at kernel mode ```c= fork() { register struct proc *p1, *p2; p1 = u.u_procp; for(p2 = &proc[0]; p2 < &proc[NPROC]; p2++) if(p2->p_stat == NULL) goto found; u.u_error = EAGAIN; goto out; found: if(newproc()) { u.u_ar0[R0] = p1->p_pid; u.u_cstime[0] = 0; u.u_cstime[1] = 0; u.u_stime = 0; u.u_cutime[0] = 0; u.u_cutime[1] = 0; u.u_utime = 0; return; } u.u_ar0[R0] = p2->p_pid; out: u.u_ar0[R7] =+ 2; } ``` ======================================= 先看 list 1 & 2 1. 首先在 list_1:line_6,使用了 assembly 指令 -> `sys fork` 這個指令會呼叫 system call fork(), 透過這個指令呼叫 system call,切換到 kernel space 來做事。 2. 再來看 list_2。 這裡就是 fork() 主要在做的事了,這邊要分成 parent process 跟 child process 來看 - Parent,從第一行開始執行 1. line_5: `p1 = u.u_procp` 這是把 p1 設為指到 parent 的 struct proc 2. line_6 ~ line_10: Iterate proc[] to find 尚未使用的 element,找到後讓 p2 指向他,若是沒有找到則設置 error code。 3. 所以,p1 -> parent process; p2 -> child process 4. line_13: invoke `newproc()` to create a new process. **NOTE: In parent, the `newproc()` return 0, so the if-block won't be executed.** 5. 接著,來到了 line_23,此行將 parent process 的 r0 暫存器設置為 child process's pid。Note: 透過 u.u_ar0 可以 access user space process 的 register。 6. line_26: move to next instruction,相當於 ARM or MIPS 裡的 PC+4 (因為在 pdp-11 每一條指令長度為 16-bits = 2 bytes) - Child,從 line_13 開始執行 1. `newproc()` 對 child process return 1,所以會執行 if-block 2. line_14 ~ 21: 將一些執行時間的欄位 init to 0, **並將 user space process 的 r0 設為 parent pid**,作為 system call fork() 對 child 的回傳值,**不是 user space 的 fork() !** 3. 到這裡,list_1 的 `sys fork` 已經執行結束了,以下再根據 parent / child 的不同,分別探討。 - Parent 1. 由與前面有做 `pc += 2`,所以接著會做 list_1:line_8 的指令 2. line_8: `bec 2f` 這是用來檢查 PSW register 的第 0 位 (carry) 為是否為 0,system call 出錯時這位會被設為 1 , if (PSW~\[0\]~ == 0) goto line_13 3. 接著透過 `rts pc` 的指令返回到 C 語言函式庫呼叫的地方。**Return Value = 保存在 r0 的 child pid.** - Child 1. 跟 parent 不同,照順序做,接著要做的是 list_1:line_7 2. `br lf` 無條件的直接跳到 label 1 (line 11) 3. line_11 ~ 12: `_par_uid = r0; r0 = 0` 這段的意義主要在使得 invoke C 語言函式庫的 user space process 可以取得 parent process pid,**最後將 r0 set to 0** 4. 接著執行 line_15 的 `rst pc` 指令返回到 C 語言函式庫呼叫的地方,**Return Value = 0 (r0 = 0, 被 `clr r0` clear 成 0 了)** 4. 統整一下 user space(C library) 和 kernel space(system call) 的 fork() 的回傳值: - | Parent / Child | Parent | Child | | -------------- | ------ | ----- | | user space | child's pid | **0** | | kernel space | child's pid | **parent's pid** | ### system call fork() 內部 procedure - `newproc()` 在 list_2 (system call fork()) 裡面有呼叫到 `newproc()`,這是主要用來產生新的 process 的 procedure。 **list 3.** newproc(), invoked at list 2. ```c= newproc() { int a1, a2; struct proc *p, *up; register struct proc *rpp; register *rip, n; p = NULL; /* * First, just locate a slot for a process * and copy the useful info from this process into it. * The panic "cannot happen" because fork has already * checked for the existence of a slot. */ retry: mpid++; if(mpid < 0) { mpid = 0; goto retry; } for(rpp = &proc[0]; rpp < &proc[NPROC]; rpp++) { if(rpp->p_stat == NULL && p==NULL) p = rpp; if (rpp->p_pid==mpid) goto retry; } if ((rpp = p)==NULL) panic("no procs"); /* * make proc entry for new proc */ rip = u.u_procp; up = rip; rpp->p_stat = SRUN; rpp->p_flag = SLOAD; rpp->p_uid = rip->p_uid; rpp->p_ttyp = rip->p_ttyp; rpp->p_nice = rip->p_nice; rpp->p_textp = rip->p_textp; rpp->p_pid = mpid; rpp->p_ppid = rip->p_pid; rpp->p_time = 0; /* * make duplicate entries * where needed */ for(rip = &u.u_ofile[0]; rip < &u.u_ofile[NOFILE];) if((rpp = *rip++) != NULL) rpp->f_count++; if((rpp=up->p_textp) != NULL) { rpp->x_count++; rpp->x_ccount++; } u.u_cdir->i_count++; /* * Partially simulate the environment * of the new process so that when it is actually * created (by copying) it will look right. */ savu(u.u_rsav); rpp = p; u.u_procp = rpp; rip = up; n = rip->p_size; a1 = rip->p_addr; rpp->p_size = n; a2 = malloc(coremap, n); /* * If there is not enough core for the * new process, swap out the current process to generate the * copy. */ if(a2 == NULL) { rip->p_stat = SIDL; rpp->p_addr = a1; savu(u.u_ssav); xswap(rpp, 0, 0); rpp->p_flag =| SSWAP; rip->p_stat = SRUN; } else { /* * There is core, so just copy. */ rpp->p_addr = a2; while(n--) copyseg(a1++, a2++); } u.u_procp = rip; return(0); } ``` 1. Line 16 ~ 20: 這段是用來分配 pid 給 child。 `mpid` 是一個 global variable, declared at system.h `int mpid;` int type 會 overflow ,若 overflow(變成負數) 則 reset to 0 然後再 increment Note: 當時的 C 語言還沒有 unsigned type 2. Line 21 ~ 28: 尋找 proc\[\] 中尚未使用的 element,並檢查已存在的 process 的 pid 是否和當前要被分配的 mpid 一樣,若相同則 goto retry,重新生成 ID 若尋找完,發現沒有空的元素,代表 proc\[\] 空間已經耗盡,呼叫 `panic()` 來處理,後面會介紹。 3. Line 34 ~ 35: 開始對 child process 的 struct proc 做 initialize。 先透過 u.u_procp 取得當前執行 process (that is, the parent process),assign 到 rip 4. Line 36 ~ 44: 複製 parent's struct proc 的 element 值到 child's struct proc ,大部分的值一樣,有些不一樣。 - SRUN: 可執行狀態 - SLOAD: 指該 process 位於 memory 中 - **Line 42: 將 child's struct proc 的 p_pid element 設為 mpid** - **Line 43: 同上,只是這次是設定 p_ppid,將 child 的 p_ppid 設為 parent's pid (即 current execute process's p_pid),用以表示 child 的 "parent" 是誰。** - | p | rpp (register) | up | rip (register) | u.u_procp | | ----- | ------ | --- | --- | -- | | proc\[\] 中尚未被使用的元素,即 child's struct proc | 同 p | parent's struct proc | 同 up | parent's struct procp | 5. Line 51 ~ 58: Counter increment. 因為 child 繼承了 parent 開過的文件,所以這些用來記錄文件 reference 的 counter 都要 +1 parent & child share TEXT SEGMENT, Line 54 ~ 57 就是在做 TEXT SEGMENT 方面的 counter increment Line 58: inode 方面的 coutner increment 有關 counter increment 的,後面會解釋,這邊先帶過。 6. Line 64: invoke `savu()` 將 r5 (fp), r6 (sp) 當前的值暫存進 user.u_rsav; Note: savu(), assembler routine 7. Line 65 ~ 67: rpp, rip 在前面都當作 for-loop 的 iterator,所以把他們值 assign 回 p, up (child's, parent's struct proc, respectivly) 接著,還有很重要的一步 **Line 66: `u.u_procp = rpp;`** 這行暫時把 u.u_procp 指到 child's struct proc,從原本指到 parent's struct proc,換成 child 的,原本的值備份在 rip 中。 - | p | rpp | up | rip | u.u_procp | | -- | -- | -- | -- | -- | | child's struct proc | same as `p` | parent's struct proc | same as `up` | **child's struct proc** | 8. Line 68 ~ 70: 先拿到 parent process's data segment size,並將 child process's 的設成跟他一樣; `a1` 指到的是 parent process data segment 的 address 9. Line 71: `malloc(), size = n = parent data segment size` 10. Line 77 ~ 83: 這段主要是在做 malloc failed 時的處理。 失敗時,parent 的 data segment 會被複製出一段存在 swap 裡,作為 child 的 data segment。 1. Line 78: 將 parent process 設為 SIDL。在這個狀態的 process 不會被選為 current execute process,也不會被 swapped out。這是避免在進行 data segment 複製時,parent process 出意外。 2. Line 79: Assign child's data segment address to the same as parent's data segment address 3. Line 80: save r5(fp), r6(sp) to u.u_ssav,因為 data segment 包含 u.u_ssav,所以 u.u_ssav 也會被複製到 child process 4. Line 81: invoke `xswap`,去把 memory 中的 data segment 複製一個到 swap。重點是前兩個參數,第一個是要複製的東西的 source address (rpp, 即 parent's data segment address, set at Line 79),第二個是 0,代表複製結束後不會去 release 在 memory 中的 data segment,讓其保留在那邊。 5. Line 82: set child process to SSWAP 6. Line 83: Restore parent process to SRUN 11. Line 84 ~ 91: `malloc()` 成功,將 child process 的 data segment address assign to 新 malloc 的 address(a2),並使用 `copyseg()` 來複製。 12. Line 92: Restore `u.u_procp` to parent's struct proc 13. Line 93: **return 0**,前面有說過,`newproc()` 對 parent 的回傳值是 0 14. Note: newproc() 對 child 的回傳值是 **1** ,詳情請參考 `swtch()` ### `newproc()` 內,資源耗盡時處理的 procedure - `panic()` **list 4.** panic(), invoked at list 3. ```c= /* * Panic is called on unresolvable * fatal errors. * It syncs, prints "panic: mesg" and * then loops. */ panic(s) char *s; { panicstr = s; update(); printf("panic: %s\n", s); for(;;) idle(); } ``` **list 5.** idle(), invoked at list 4. ```asm= .globl _idle _idle: mov PS,-(sp) bic $340,PS wait mov (sp)+,PS rts pc ``` 在資源耗盡時, `panic()` 透過 invoke `idel()` 來把處理器的優先級調為 0 ,並進入 `wait` 等待 interrupt 的狀態。 ## Process Switch - 切換 current execute process ### Overview - 現在正在執行的 process,在執行 kernel function `sleep()` 後會進入休眠狀態並中斷當前的處理,再透過 `swtch()` 去把當前執行的 process 切換到別的 process。 - `sleep()` 一般在以下情況被調用 1. user process invoke `wait()` 2. 等待週邊設備處理完畢 3. wait for resource release ![](https://i.imgur.com/GKtztHm.png) - Process 的狀態由 `proc.p_stat` 來表示,有 SRUN, SSLEEP, SWAIT 三種 | state | meaning | 要用哪個 function 切換到這個狀態 | | ----- | ------- | -------- | | SRUN | exectutable | wakeup() | | SSLEEP, SWAIT | 休眠狀態,兩個狀態的差別在於被喚醒時的執行 priority 不同 | sleep() | ```graphviz digraph process_FSM { rankdir=LR; size="8,5" node [shape = circle]; S1[label="SRUN"]; S2[label="SSLEEP / SWAIT"]; S1 -> S2 [label="sleep()"]; S2 -> S1 [label="wakeup()"]; } ``` - `swtch()` 是用來選擇下一個 execute process 是哪一個的 procedure,被選擇的 process 要滿足一下三個條件: 1. proc.p_stat == SRUN 2. 擁有最高的執行 priority (proc.p_pri 的值最小) 3. process didn't be swapped out - 將某個 process 指定為下一個 current execute process 是無法做到的,因為無法確定在切換時是否有比該 process priority 更高的 process - process 的 priority 是由 `setpri()` 所設定的,佔用 CPU 時間越久的 process 他的 priority 就會被降低,**所以,`setpri()` 中對 process priority 設定的方法影響了到底是哪一個 process 會被選為 current execute process。** - process 的執行被中斷後,會將當前的狀態存進 `struct user` ,當該 process 再次被選為 current execute process 時,會再去從 `struct user` 中把資料 load 出來,恢復到被中斷時的狀態。這種處理就叫做 **context switch 上下文交換**,主要是由以下 assembly procedure 所實現: 1. `savu()` \/\/ save the status to struct user 2. `retu()` \/\/ load status from struct user 3. `aretu()` \/\/ load status from struct user ### `sleep()` - switch process state from SRUN to SSLEEP / SWAIT - Prototype: `sleep(chan, pri)` - Arguments: 1. @chan: address, 將會執行`current_execute_proc.p_wchan = chan;` wchan 代表的是 waiting channel,代表該 process 正在等待其所指到的位置的資源釋放。 2. @pri: priority,將會執行 `current_execute_proc.p_pri = pri;` 根據 value 的大小,在 **process switch** 前和**該 process 再次被選為 current execute process 後**,會對 signal 進行 or 不進行處理。 - | pri value | 狀態 | | ------- | ---- | | >=0 | 設為 SWAIT,對 signal 進行處理 | | <0 | 設為 SSLEEP,**不**對 signal 進行處理 | **list 6.** sleep() ```c= /* * Give up the processor till a wakeup occurs * on chan, at which time the process * enters the scheduling queue at priority pri. * The most important effect of pri is that when * pri<0 a signal cannot disturb the sleep; * if pri>=0 signals will be processed. * Callers of this routine must be prepared for * premature return, and check that the reason for * sleeping has gone away. */ sleep(chan, pri) { register *rp, s; s = PS->integ; rp = u.u_procp; if(pri >= 0) { if(issig()) goto psig; spl6(); rp->p_wchan = chan; rp->p_stat = SWAIT; rp->p_pri = pri; spl0(); if(runin != 0) { runin = 0; wakeup(&runin); } swtch(); if(issig()) goto psig; } else { spl6(); rp->p_wchan = chan; rp->p_stat = SSLEEP; rp->p_pri = pri; spl0(); swtch(); } PS->integ = s; return; /* * If priority was low (>=0) and * there has been a signal, * execute non-local goto to * the qsav location. * (see trap1/trap.c) */ psig: aretu(u.u_qsav); } ``` :::info NOTE: 接下來下面的解說,有些提到的 function 會先帶過,後面會再解說 ::: 1. Line 16: `PS` points to the address of register PSW, see the follow code (defined at param.h) `#define PS 0177776 // octal, physical address`; 此行目的為保存 PSW 當前值。 2. Line 17: let `rp` points to current process's `struct proc`, one of the element of `proc[]`. 3. Line 18 ~ 32: 處理傳進來的參數: `pri` >= 0 的情況。 - 入睡,檢查是否有 signal,透過 `issig()` 來檢查,若有,`goto psig` (Line 51, 52) 交給那個 label 處理。 - Line 21: set CPU priority to '6',因為此處可能會發生 iterrupt,處理時會呼叫 `wakeup()`,為避免下面三行的 data 被修改,所以提升 priority,詳細的以後文章再會說明。 - Line 22 ~ 24: set wchan, stat, pri; 其中在 `pri >= 0` 的情況可以看到 `stat = SWAIT;` - Line 25: data 設定結束,CPU priority restore to '0' - Line 26 ~ 29: `runin` is a flag variable. `if(runin == 1)` 表示**不存在**要被 swapped 到 swap space 的對象。詳細的後面會再說。 - Line 30: invoke `swtch()` to switch process. - Line 31 ~ 32: 喚醒,檢查 signal。 4. Line 33 ~ 40: `pri` < 0 的情況。基本上處理同上,少了對 signal 的處理以及狀態是改為 SSLEEP 而非 SWAIT。 5. Line 41 ~ 42: 為了再次執行被中斷的 process,restore the value we stored at Line 16. ### `swtch()` - The main procedure of process switch 其主要的做法是 iterate `proc[]` 找出處於 SRUN 狀態且 priority 最高的,將該 process 選為 current execute process。 Iterate `proc[]`,找出符合條件的 process 然後再去做事是 kernel 常做的方法,但是 `swtch()` 比較不一樣,他不是從頭 (`proc[0]`) 開始 iterate 的,而是從 current execute process 的 element 位置開始找的。 如果有兩個處於 SRUN 狀態且 priority 相同的 process,那麼下一個被選到的 process 會是在 current execute process element 後面,且離他比較近的那個會被選到。 - 選出下一個 process 是哪一個 process 後,接下會做以下動作 (以下簡稱原本的 process 為 p1, 新的被選為的 process 為 p2): 1. Modify kernel APR 使得 p2 的 struct user 可以透過 global variable `_u` 來 access。(參見:[前一篇文章](https://hackmd.io/96Y70AqnTPyA3O8t4fHtQQ#How-to-access-current-process’s-“struct-user”)) 2. Access p2 的 struct user,裡面有之前保存的 r5, r6 的值,把他們恢復成 fp, sp 3. 恢復在 p2 的 struct user 中保存的 user APR 值。