or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
# shell lab
shell lab
此筆記主要紀錄 CS:APP 作業 shell lab 的解題思路,由於是一步一腳印撰寫,並非寫完再事後筆記,因此會顯得比較雜亂
TODO:
基礎知識複習 - Exceptions & Process
要完成 shell lab 需先對 exceptional control flow (ECL) 有基礎的認知,建議先至少閱讀過 CS:APP 第 8 章再進行此作業
fork
called once but return twice
RETURN
returned in the child
returned in the parent
waitpid
init (pid = 1)
reaps zombie after parent terminated, so explicit reaping are only necessary forpid_t pid
meaning wait for any child process whose process group ID is equal to the absolute value of pid
meaning wait for any child process
meaning wait for any child process whose process group ID is equal to that of the calling process
meaning wait for the child whose process ID is equal to the value of pid
int option
Return immediately (with a return value of 0) if none of the child processes in the wait set has terminated yet
Return the PID of the terminated or stopped child that caused the return (The default behavior returns only for terminated children)
Return immediately, with a return value of 0, if none of the children in the wait set has stopped or terminated, or with a return value equal to the PID of one of the stopped or terminated children.
int *status
If the status argument is non-NULL, then waitpid encodes status information about the child that caused the return in the status argument. The
wait.h
include file defines several macros for interpreting the status argument:Returns true if the child terminated normally, via a call to exit or a return.
Returns the exit status of a normally terminated child. This status is only defined if WIFEXITED returned true.
Returns true if the child process terminated because of a signal that was not handled.
Returns the number of the signal that caused the child process to terminate. This status is only defined if WIFSIGNALED(status) returned true.
Returns true if the child that caused the return is currently stopped.
Returns the number of the signal that caused the child to stop. This status is only defined if WIFSTOPPED(status) returned true.
RETURN
waitpid success
if WNOHANG was specified and one or more child specified by pid exist, but have not yet changed state
ERROR
ERRORS
Error message accordingly can be accessed through
strerror(errno)
The process specified by pid (waitpid()) or idtype and id (waitid()) does not exist or is not a child of the calling process.
WNOHANG was not set and an unblocked signal or a SIGCHLD was caught
wait(&status)
==waitpid(-1, &status, 0)
getpid
getpid
returns the PID of the calling processgetppid
returns the PID of its parentexecve
The execve function loads and runs the executable object file
filename
with the argument listargv
and the environment variable listenvp
. While it overwrites the address space of the current process, it does not create a new process. The new program still has the same PID, and it inherits all of the file descriptors that were open at the time of the call to the execve function.RETURN
execve success
ERROR
appendex -
envp
getenv
searches the envp[] for a string "name=value"setenv
searches the envp[] for a string "name=oldvalue" and replaces oldvalue with newvaluesetenv
adds "name=newvalue" to the envp[]unsetenv
searches the envp[] for a string "name=value" and deletes itfurther information
基礎知識複習 - Signals
kill
- send signal, not kill!signal
a cleaner approach using wrapper:
直接看 man 比較清楚!
->man sigaction
sigprocmask
how
oldset
is non-NULL, the previous value of the blocked bit vector is stored inoldset
int sigemptyset(sigset_t *set)
initializes set to the empty setint sigfillset(sigset_t *set)
adds every signal to setint sigaddset(sigset_t *set, int signum)
adds signum to setint sigdelset(sigset_t *set, int signum)
deletes signum from set解題思路
首先看教科書範例:
接下來會依照作業說明,根據 trace file 的順序完成作業
trace01
基本上一開始就是完成的=_=
stdin
trace02
就是簡單的字符偵測,先交給
parseline
分析輸入的指令串,再交給builtin_cmd
偵測trace03 & trace04
這兩個 trace 可以一起做,具體的邏輯順序如下
parseline
分析輸入的指令串,判斷是否為 FG/BG jobfork
出 child 並讓其execve
執行 jobtrace05
內鍵指令
jobs
的實作很簡單,就是登錄一個指令到builtin_cmd
,實作的部分則直接呼叫listjobs
來列出 job list 的內容 (作業題目一開始就已提供)接下來需要處理 job list 的登錄問題,共需處理以下幾個點:
fork
後需要呼叫addjob
來將 child 登錄到 job listdeletejob
,這會再細分為兩個狀況sigchld_handler
負責 reap在實作前我們先注意一下 writeup 中給予的提示:
writeup 中提到在
waitfg
及sigchld_handler
內皆使用waitpid
雖然可能是可行的方案,但建議還是交由sigchld_handler
來統一 reap child 以避免混亂,因此一開始在 trace03 部分的寫法是不行的,後面需再修改另外,需在 parent 執行
fork
前先阻擋住 SIGCHLD,並在 parent 執行addjob
後再解封 SIGCHLD,否則有可能會因為 child 先執行完畢,造成sigchld_handler
在 parent 執行addjob
前就先執行deletejob
並造成 race condition為了避免 ctrl-c 後將我們寫的 shell 一起砍掉,需使用
setpgid(0, 0)
來將 child 移到別的 group ID綜合上述的討論並修改 trace03 & trace04 的結果如下:
註:看
eval
的開頭註解可以發現尚未解決 SIGINT (ctrl-c) 與 SIGTSTP (ctrl-z) 的問題,這些會在 trace06 一起解決trace06 ~ trace08
當我們輸入 ctrl-c 或 ctrl-z 時,OS 會傳送 SIGINT 或 SIGTSTP 至我們寫的 shell,因此我們還需要使用
sigint_handler
來將此訊號再送往對應的 FG jobsigchld_handler
也需新增處理 SIGINT (ctrl-c) 與 SIGTSTP (ctrl-z) 的部分:trace09
首先修改
builtin_cmd
,增加bg
和fg
的判斷do_bgfg
實作的部分我們先看一下 writeup 給的提示因此整體的解題順序大致為:
kill
發送 SIGCONT 到目標 jobeval
在execve
後的處理邏輯一樣最後結果如下
trace12 ~ 16
這幾個 trace 基本上是用來檢驗前面幾個步驟是否有疏漏,在此不再贅述
延伸問題:
exit()
v.s._exit()
首先認真看一下
man 3 exit
與man 2 _exit
的內容因此若
fork()
後 child 的execve()
失敗,child 內應該使用_exit()
而不是exit()
,因為_exit()
會把 parent 的 stdio 沖掉、暫存檔刪掉還會呼叫已經不存在的atexit(3)
與on_exit(3)
。reference
延伸問題: async-signal-safe function
本章節之後會再行探討,現在的版本有待改善
到此為止雖然所有的 trace 都會顯示正確 (或是說有機會顯示全部正確),但如果有認真上課的話…會發現我們在撰寫 signal handler 的時候完全無視了 signal safety 的議題,根據 POSIX 規範,signal handler 裡面只能使用 async-signal-safe function,然而
printf
並不是!man 7 signal-safety
在我測試 trace file 時也確實發生過這種狀況,signal 在 main 執行某個 system call 的時候發生,導致 signal handler 會在中途執行
因此可行的方案為
我們將修改的流程分為兩個步驟
步驟1 - 確保只使用 async-signal-safe functions
CS:APP 其實有提供 csapp.c 的輔助文件,內含可以在 signal handler 內使用的方程式,節錄我們會用到的部分如下
unix_error
可以很簡單的改為sio_error
,但printf
的部分比較麻煩,因為sio_puts
不支援格式操作 (%d, %s 等),因此需要在 signal handler 內設置 flag,之後再main
中根據 flag 執行printf
步驟2 - 確保 global variables 的安全性
以本題的狀況考慮,共用的 global variable 是 jobs 這個物件,以下列出會存取這個物件的方程式
可行的方案有兩個
deletejob
和fgpid
移出 signal handler由於本題要求的關係,方案1反而更難實行 (主要是實作
waitfg
的部分會因此變得很麻煩),因此採用方案 2,也就是將全部取用 jobs 的方程式都用sigpromask
包起來。但仔細想想並非所有的都需要包起來,其中 eval 內的
pid2jid
以及 handler 內的fgpid
是不用特別處理也不會有問題,前者是就算衝突也只是列出不對的數據,後者是因為 main 中所有取用 jobs 的方程式都用sigpromask
包起來了,不可能衝突。為了避免版面太雜亂,以下僅列出關鍵的部分,只新增
sigpromask
的部分就不特別列出來了tags:
csapp