# Week 3 - Highlights Nice notes: https://drive.google.com/drive/folders/1UJyV17jx2l3RdIVhpEY7sOfxqcWYRjKJ?usp=sharing * @team1 * @team12 * @team15 * @team18 * @team23 * @team24 * @team28 * @team30 ## @team28 ### Unix Philosophy - Make each program do one thing well and let them work together - user can quickly assemble a desired program using many single-purpose binaries - 每個人使用電腦都有不同的需求,但這些需求拆到最後都是一些atomic的功能組合起來的 與其為了1000個很像功能設計1000個很像的指令,設計出重點的幾個功能,並善用組合來達成我們要的功能是更好的設計方法。 這裏就像是functional programming,盡量讓每個function都是pure、single purpose的,並用組合的方式來做出我們要的效果。 如此一來,不只增加可讀性,學習曲線較低,又便於優化、維護。 - handle text streams - for M$ powershell, it expects everything is a `.NET` object - Everything has same simply interface, no much domain knowledge required ## @team15 我們簡單實作了reading material中的hw,另外多處理了一些事情。他hw所需的code在refrence中的github。她作業中沒有實現 double fork 的技巧,所以我們可以簡單實現如下: 我們輸入 `./fork.py -A a+b,b+c,b+d,d+e,d-,b- -c` ![image](https://hackmd.io/_uploads/ryfP0hMsex.png) :::info `-c` 意思是顯示出答案,因為這個本來是給你action然後要你畫出每一步的process tree,但這裡我們只是好奇double fork的樹長甚麼樣子 `-A` 後面為自訂義腳本,`a+b`表示a生b,`-b`表示b exit ::: 所以我們會拿到 ![image](https://hackmd.io/_uploads/SJUckaGixg.png) ![image](https://hackmd.io/_uploads/SyOi1Tzieg.png) 可以看到d結束的時候e就直接回到a了,a相當於我們電腦的`PID = 1`。 另外我們好奇 zombie,很可惜的是這個作業提供的 `fork.py` ,沒辦法弄出zombie的情況。 reference: 1. reading material hw github :https://github.com/remzi-arpacidusseau/ostep-homework/ ## @team28 ### 為什麼`fork`的return值不同,剛fork過去時卻還是共用同一個記憶體空間? 假設以下的code: ```c= pid_t pid = fork(); if (pid == 0) { // child do something ... } ``` 在剛複製過去時,兩個process會共用同一塊記憶體空間,直到某個process率先執行寫入,觸發了CoW後,在獨立寫入該process的記憶體內。但是如果`fork()`的return值不一樣,記憶體為什麼還可以共用呢?在查詢資料後發現: 1. 雖然兩個process的`pid`看起來像是同一個memory address,但實際上的physical memory會map到不同的位址。 2. 有可能OS去更改了cpu register value,在沒必要的時候,並不會去將這個value寫到memory裡面,自然就不會有CoW的狀況發生。(待驗證) 補充:後來在 TA Time 問老師 ```c pid_t pid = fork() ``` 就會觸發 copy-on-wirte 紀錄有 pid 的 那個page會被複製一份 <p/> 而如果是 ```c if (fork() == 0) { print ("Child Processing...\n") } ``` 就組語觀點來說,只要把 fork() 的 return value 放在 register 裡面即可完成判斷(branch 跳轉) 是故雖然parent/child 的 PC 會不同,但還不會觸發 copy on write,因為不同的地方只有reg,還沒動到memory ## @team30 **fork() 之後,父與子 process 的記憶體空間明明相同,為什麼修改變數時卻互不影響?** A2. 因為雖然父跟子 process 在 fork() 時看起來擁有相同的虛擬地址空間,但現代 OS 採用 Copy-On-Write (COW) 機制:fork 時不會立即複製所有實體 page;取而代之的是把原本的 page 標記為可共享且唯讀,父子都指向相同的物理 page。當某一方嘗試寫入(修改變數)時,產生 page fault,kernel 才為該方分配一個新的物理 page 並把內容複製過去,然後那一方修改的是自己的新頁,另一方看不到變更。因此修改是「私有化」的,不會互相影響。 參考資料: Copy-On-Write 機制說明:https://www.geeksforgeeks.org/operating-systems/copy-on-write/ ## @team1 在查找資料過程中,我發現了一個有趣的情況並且寫了 code 來測試 ```c for(int i=0;i<3;i++){ cout << i; fork(); } // output : 012012012012012012012 ``` 這個 output 非常反直覺,因為前面 i = 0, 1 的時候有的 child process 還沒生出來,但最後卻變成所有 process 都輸出 012。 原因是 output 會先被 buffered 在 stdout 裡面,如果 child 直接複製的話會連 buffer 中的內容一起複製,直到最後才輸出。 ```c for(int i=0;i<3;i++){ cout << i << flush; fork(); } // output : 0112222 ``` 如果每次 fork 前都有把 buffer 給 flush 掉的話就可以確保 child 拿到的 stdout 是乾淨的。 ## @team23 Question: We learned that a zombie process occurs when a child process terminates but its parent does not call `wait()` or `waitpid()`. **Our question:** *If a zombie is really created, how can we observe it and how can it be reaped (collected)?* - Experiment (Our Hands-on Test) - creating a zombie - Program:use `fork()`, let the child immediately `exit(0)`, and let the parent **not** call `wait()` but sleep for 30 seconds.<img src="https://hackmd.io/_uploads/SJL0XZHsgx.png" width="350"> - Execution:run `./zombie &` to place it in the background.<img src="https://hackmd.io/_uploads/SJG-HbHsee.png" width="350"> - Observing the zombie - Use `ps` to filter processes with state `Z`:<img src="https://hackmd.io/_uploads/Sk8NrbHjel.png" width="650"> - After 30 seconds, the parent process also terminates. Running `ps` again shows that the zombie process has disappeared, since it has been reparented to `init` and reaped automatically:<img src="https://hackmd.io/_uploads/SklWIbHixg.png" width="650"> - If we want to remove the zombie before the parent exits :::warning Key point: a zombie cannot be killed directly. We must make its parent reap it (call wait()/waitpid()), or make the parent exit so that init will reap it. ::: - method 1: remind the parent to reap - We first tried to send `SIGCHLD` manually to the parent process while a zombie existed. But we found nothing happened, because the parent program had no signal handler and no `waitpid()`.![image](https://hackmd.io/_uploads/SJCqUqHsgx.png) - Adding a signal handler: We modified the parent code to install a handler for `SIGCHLD`. The handler simply calls `waitpid(-1, &status, WNOHANG)` in a loop to reap all terminated children:![image](https://hackmd.io/_uploads/S1YeVcBoxg.png) - Result: With this handler, whenever the child terminates, the kernel sends `SIGCHLD` automatically. The handler executes immediately and reaps the child, so the zombie never appears. **Therefore, in most cases, there is no need to manually send the signal.** - method 2: terminate the parent **Let `init` adopt and reap the zombie by ending the parent:**![image](https://hackmd.io/_uploads/HkfJeMHslg.png) - Result:Success! - Reference website - Baeldung – How to Clean a Linux Zombie Process https://www.baeldung.com/linux/clean-zombie-process?utm_source=chatgpt.com ## @team28 Q3. Why not just use command-line arguments: docker run postgres --user=myuser --password=mypassword?   在容器化部署中,使用環境變數而非命令列參數主要有兩個實質理由。首先,安全性上,命令列參數會暴露在進程的 /proc/<pid>/cmdline 或 ps 輸出中,容易被同一宿主機或命名空間下的其他進程窺探到;相比之下,環境變數不會直接出現在命令列中,且存取 /proc/<pid>/environ 需要更高權限,因此可降低敏感資訊(如資料庫密碼)意外洩漏的風險。   其次,環境變數提供更高的部署靈活性。在不同環境或多容器系統中,可以透過 docker-compose、Kubernetes ConfigMap 或 Secrets 等方式集中管理與注入設定,而無需每次修改啟動命令。這種方法不僅簡化了自動化部署流程,也方便在多環境間切換設定,對持續整合與持續部署(CI/CD)流程尤其有利 ## @team2 speaking of docker environment variable, we can also use docker compose or dockerfile to write is, instead of writing it all on the commandline. something like this ```dockerfile FROM ros:kinetic LABEL org.opencontainers.image.authors="ohin" ENV RUNNING_IN_DOCKER=true ENV TERM=xterm-256color ARG DEBIAN_FRONTEND=noninteractive ARG USER ARG USER_UID=1000 ``` ## @team30 **為什麼透過 bash & 建立的背景 bash 是 stopped 狀態?** A:bash會嘗試從 stdin 讀取使用者輸入(在背景也一樣),Linux 有一個保護機制:背景程式不能直接讀 stdin,所以kernel 會送 SIGTTIN 訊號給它 SIGTTIN 的預設行為是:暫停(Stopped)程式。 **Q: 要怎麼解決上問題?** A: 重定向 stdin,例如 bash < /dev/null & stdin 被重定向到 /dev/null,bash 不會嘗試讀鍵盤,所以它啟動後立刻執行完(因為沒有互動命令要執行),就退出了 → 顯示 Done。 ## @team15 `jobs -l` : 顯示當前的的工作,ex: ```bash root@ubuntu:/home/vagrant# bash & [2] 10190 root@ubuntu:/home/vagrant# jobs -l [1]- 10172 Stopped (tty input) bash [2]+ 10190 Stopped (tty input) bash ``` `fg %3`:%多少就是把那個工作拉到前面來(吃到stdin) ![image](https://hackmd.io/_uploads/BkCFyMloxl.png) ## @team18 **Q:為什麼透過 bash & 建立的背景 bash 是 stopped 狀態?** A:bash會嘗試從 stdin 讀取使用者輸入(在背景也一樣),Linux 有一個保護機制:背景程式不能直接讀 stdin,所以kernel 會送 SIGTTIN 訊號給它 SIGTTIN 的預設行為是:暫停(Stopped)程式。 **Q: 要怎麼解決上問題?** A: 重定向 stdin,例如 bash < /dev/null & ## @team15 **Q1, How is it possible to run so many “virtual CPUs” on a machine with only 2 physical CPU cores?** 在虛擬化層 `hypervisor`中,實體 CPU 的時間片`time slice`會被分配給多個虛擬機,通常會採取平均分配或公平排程的策略。乍看之下,這與一般作業系統中的排程機制非常相似,但在虛擬化環境下,這種操作有一個專門的名稱:`over-subscription`(超額分配)。 vCPU(虛擬 CPU)之所以能實現這種功能,主要在於它的分配方式就像是在多條並行公路上讓多個 VM guest 依序「排隊前進」。每個 VM 認為自己擁有完整的 CPU 資源,但實際上它們共享底層的物理核心,透過輪流使用時間片來模擬「同時運行」的效果。這就是為什麼即使一台實體機只有 2 顆核心,也能支援三個甚至更多 vCPU 的虛擬機運作。 這種設計能夠有效運作的關鍵在於 大部分時間內 vCPU 的使用率並不會飆滿。換句話說,多數 VM 並不是持續占用 100% 的 CPU,因此 hypervisor 可以安全地讓多個 vCPU 共用相同的物理核心而不會立即造成嚴重性能下降。 然而,如果某個 VM 突然開始執行高 CPU 消耗的程式(例如大型遊戲或密集計算任務),這種共享機制就會受到影響:其他 VM 可能會因為搶不到 CPU 時間而效能下降,甚至出現延遲或卡頓。這也是設計虛擬化環境時需要考慮 資源限制與負載管理 的原因。