導語:這是篇讀書筆記,每次重讀CSApp都有新的認知,尤其是在進入了后臺通道之后才感受到每天和進程打交道的感覺是如此深刻。
0x00 What is Process?
[ system structure ]
- 進程(Process)
經典定義是一個執行中的程序的實例,操作系統對一個正在運行的程序的一種抽象。并發運行,指的是一個進程的指令和另一個進程的指令交錯執行。操作系統實現這種交錯執行的機制稱為上下文切換。
- 線程(Thread)
一個進程可以由多個線程的執行單元組成,每個線程都運行在進程的上下文中,并共享同樣的代碼和全局數據。
- 內核(Kernel)
一個計算機程序,用來管理軟件發出的數據I/O(輸入與輸出)要求,將這些要求轉譯為數據處理的指令,交由中央處理器(CPU)及計算機中其他電子組件進行處理,是現代操作系統中最基本的部分。
- 外殼(Shell)
指“為使用者提供使用者界面”的軟件,通常指的是命令行界面的解析器。一般來說,這個詞是指操作系統中提供存取內核所提供之服務的程式。Shell也用于泛指所有為用戶提供操作界面的程序,也就是程序和用戶交互的層面。內核不提供交互。
- 搶占(Preemption)
分為非搶占式和搶占式。根據調度主體分用戶搶占與內核搶占。
非搶占式(Nonpreemptive)——讓進程運行直到結束或阻塞的調度方式。
搶占式(Preemptive)——允許將邏輯上可繼續運行的在運行過程暫停的調度方式??煞乐箚我贿M程長時間獨占CPU。
- 異??刂屏鳎‥CF,Exceptional Control Flow)
ECF發生在硬件層,操作系統層,應用層??刂妻D移(control transfer)是指程序計數器對應的指令序列的跳轉,控制轉移序列的叫做處理器的控制流(control flow)。
某些如跳轉、調用和返回是為了使得程序對內部狀態變化(event)做出反應而設計的機制,系統通過使控制流發生突變對發生各種狀態變化。
Exceptions
任何情況下,處理器檢測到event發生,通過異常表(exception table)跳轉到專門處理這類事件的操作系統子程序(exception handler)。
異步異常由事件產生,同步異常是執行一條指令的直接產物。
類別包含中斷(異步),陷阱(同步),故障(同步),終止(同步)。
- 中斷——異步發生,處理器IO設備信號的結果。
- 陷阱——有意的異常。最重要的用途是在用戶程序和內核之間提供一個像過程一樣的接口,叫做系統調用。
- 故障——潛在可恢復的錯誤造成的結果。如果能被修復,則重新執行引起故障的指令,否則終止。
- 終止——不可恢復的致命錯誤造成的結果。
有高達256種不同的異常類型,如出發錯誤(0)、一般保護故障(13)、缺頁(14)、機器檢查(18)、操作系統定義的異常(32-127,129-255)、系統調用(0x80)。
我們常見的段故障(Segmentation fault),是一般保護故障(異常13),通常是因為一個程序引用了一個未定義的虛擬存儲器區域,或者因為程序試圖寫一個只讀的文本段。
[ Examples of popular system calls ]
Processes
- 邏輯控制流(Logical Control Flow)
程序計數器PC值的序列叫做邏輯控制流(邏輯流)。PC對應于程序的可執行目標文件中的指令,或者是包含在運行時動態鏈接到程序的共享對象中的指令。
邏輯流看起來就像是在獨占處理器地執行程序,每個進程執行邏輯流的一部分然后就被搶占,實際上處理器通過上下文保護好進程間的信息,在不同的進程中切換。
- 并發流(Concurrent Flows)
并發流指邏輯流在執行時間上與另一個流重疊,多個就叫并發(concurrent)。
一個進程和其他進程輪流運行叫多任務(multitasking)。
進程占有CPU執行控制流的每一個時間段叫時間片(time slice)。
多任務也叫做時間分片(time slicing)。
如果兩個流并發運行在不同的處理器或者計算機,稱為并行流(parallel flow)。
- 私有地址空間(Private Address Space)
一般,進程間地址空間讀寫保護。進程地址空間32位進程,代碼段從0x08048000開始,64位進程從0x00400000開始:
[ Process address space ]
- 用戶模式和內核模式(User and Kernel Modes)
- 通過控制寄存器中的模式位(mode bit)描述進程當前享有的特權。
- 內核模式:(超級用戶)可執行指令集中任何指令,并且可以訪問系統中任何存儲器位置。
- 用戶模式:不允許執行特權指令,不允許直接引用地址空間中內核區內的代碼和數據,任何嘗試都會引發致命保護故障??梢酝ㄟ^系統調用接口間接訪問內核代碼和數據。
- 上下文切換(Context Switches)
- 內核為每個進程維持一個上下文(context),是內核重新啟動一個被搶占的進程所需的狀態。包括:
- 通用目的的寄存器、浮點寄存器、程序計數器、用戶棧、狀態寄存器、內核棧和各種內核數據結構(地址空間的頁表、有關當前進程信息的進程表、進程已打開文件的信息的文件表)
- 內核調度器(scheduler)負責調度進程,搶占當前進程,重新開始先前被搶占的進程。
0x01 101 Inside Process
Process Control
如何控制進程?
?
PID
pid > 0
#include <sys/types.h> // for pid_t #include <unistd.h> ? pid_t getpid(void); // 獲取進程ID pid_t getppid(void); // 獲取父進程ID
Creating and Terminating Process
從程序角度來看,進程總處于以下三種狀態:
- Running——要么處于CPU執行中,要么處于等待被執行且最終會被內核調度。
- Stopped——進程被掛起(suspend),且不會被調度。當收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信號時,進程停止,直到收到SIGCONT信號,進程再次開始運行。
- Terminated——進程永遠停止了。三種原因導致終止:
- 收到一個默認行為時終止進程的信號;
- 從主程序返回;
- 調用exit。
#include <sys/types.h> #include <unistd.h> /* 創建子進程 * 返回: 子進程=0,父進程=子進程PID,出錯=-1 */ pid_t fork(void); ? #include <stdlib.h> void exit(int status);
父進程通過調用fork創建一個新的運行子進程,最大的區別在于不同的PID。
- fork():一次調用,返回兩次。
- 在調用進程中(父進程),返回子進程PID;
- 在新創建的子進程中,在子進程中返回0。
- 并發執行:父子進程是并發運行的獨立進程。
- 相同但是獨立的地址空間。子進程與父進程用戶級虛擬地址空間相同的拷貝,相同的本地變量值、堆、全局變量、以及代碼。如代碼中print出來不一樣的x。
- 共享文件:任何打開文件描述符相同的拷貝,如stdout。
int main() { pid_t pid; int x = 1; ? pid = fork(); // 在此處分裂出了兩條時間線! if (pid == 0) {// 子進程 printf("child: x=%dn", ++x); exit(0); } // 父進程 printf("parent: x=%dn", --x); exit(0); ? return 0; }
out:
parent: x=0
child: x=2
child |————x=2———— father ——————————x=0———— fork exit
Reap Child Process
進程終止時,保持位已終止狀態,直到被父進程回收(reap)。當父進程回收已終止的子進程,內核將子進程的退出狀態傳遞給父進程,然后拋棄已終止的進程,此刻進程不復存在。
僵尸進程(zombie):一個終止了但還未被回收的進程。但是如果父進程沒有回收就終止了,則內核安排init進程(PID=1)回收僵尸進程。
#include <sys/types.h> #include <sys/wait.h> ? /* 進程可以調用waitpid等待子進程終止或者結束。 * 默認options=0,掛起調用進程,直到它等待集合中的一個子進程終止。如果等待集合中的一個進程在剛調用的時刻就已經終止了,那么waitpid立即返回。返回已終止的子進程PID,并去除該子進程。 ? *輸入參數pid: pid>0,等待集合就是一個單獨的子進程,進程ID等于pid。 pid=-1,等待集合是由父進程所有的子進程組成。 ? *輸入參數options: WNOHANGE:等待集合中任何子進程都還沒有終止,立即返回0;默認行為還是掛起調用進程直到子進程終止。 WUNTRACED:掛起調用進程執行,直到集合中有一個進程終止或停止。返回該進程PID。 WNOHANGE|WUNTRACED:立刻返回,0=如果沒有終止或停止的子進程;PID=終止或停止的子進程PID。 ? *輸入參數status: WIFEXITED:True=子進程是通過return或者exit終止的; WEXITSTATUS:返回exit狀態,只有WIFEXITED=True時被定義; WIFSIGNALED:True=子進程是因為一個未被捕獲的信號終止的; WTERMSIG:返回導致子進程終止信號量,只有WIFSIGNALED=True被定義; WIFSTOPPED:True=返回的子進程是停止的; WSTOPSIG:返回引起子進程停止的信號的數量,只有WIFSTOPPED=True被定義; ? 返回: 成功=子進程PID;if WNOHANG=0; 其他錯誤=-1(errno=ECHILD,沒有子進程;errno=EINTR,被一個信號中斷) */ pid_t waitpid(pid_t pid, int *status, int options); pid_t wait(int *status); //等價于waitpid(-1, &status, 0);
Sleep
#include <unistd.h> ? // 返回:seconds left to sleep unsigned int sleep(unsigned int secs); ? // 讓調用函數休眠,直到收到一個信號 // 返回:-1 int pause(void);
Loading and Running Programs
execve函數在當前進程的上下文中加載并運行一個新的程序,覆蓋當前進程的地址空間,但并沒有創建一個新進程,進程PID沒有改變。
#include <unistd.h> // 返回:成功=不返回;出錯=-1 int execve(const char *filename, const char *argv[], const char *envp[]); // 程序主入口: int main(int argc, char **argv, char **envp); int main(int argc, char *argv[], char *envp[]);
Signal
[ linux Signal(`man 7 signal`) ]
信號傳遞到目的進程包括兩個步驟:1)發送;2)接收。
- 一個發出卻沒被接收的信號叫做待處理信號(Pending Signal)。
- 一個進程有一個類型為k的待處理信號,后面發送到這個進程的k信號都會被丟棄。
- 也可以選擇性阻塞接收某個信號,信號被阻塞時仍可以發送,但產生的待處理信號不會被接收,直到進程取消對這種信號的阻塞。
- 一個待處理信號最多只能被接收一次,內核為每個進程在pending位向量中維護待處理信號集合,而在blocked位向量中維護被阻塞的信號集合。
- 只有接收了k信號,內核才會清除pending中的k位。
Sending Signal
- 每個進程都只屬于一個進程組,進程組ID標識。unix所有發送信號的機制都是基于進程組(process group)/
#include <unistd.h> ? // 返回:調用進程的進程組ID pid_t getpgrp(void); // 返回:成功=1,錯誤=-1 int setpgid(pid_t pid, pid_t pgid);
- 用/bin/kill程序發送信號
- 發送信號9到進程15213
- /bin/kill -9 15213
- 發送信號9到進程組15213中的每個進程。
- /bin/kill -9 -15213
- 從鍵盤發送信號
- unix使用作業(job)表示對每一個命令行執行而創建的進程,至多一個前臺作業和0個或多個后臺作業。通過|unix管道連接起多個進程。
- shell位每個作業創建一個獨立的進程組。進程組ID是取自job中父進程中的一個。
- Ctrl + C發送SIGINT信號到前臺進程組中的每一個進程,終止前臺作業。
[ 前臺進程子進程和父進程具有相同的進程組ID。]
- 用KILL函數發送信號。
#include <signal.h> // 輸入參數pid: // pid>0:發送SIGKILL給進程pid // pid<0:發送SIGKILL給進程組abs(pid) // 返回:成功=0,失敗=-1 int kill(pid_t pid, int sig);
- alarm函數發送信號
#include <unistd.h> // 發送SIGALRM給調用進程,如果secs位0,則不會調度alarm。任何情況,對alarm調用都將取消任何pending alarm,并返回pending alarm在被發送前還剩下的秒數。 // 返回:前一次alarm剩余的秒數,0=以前沒有設定alarm unsigned int alarm(unsigned int secs); ? ? /* 定時1s觸發alarm handler,5s結束 */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> ? ? void handler(int sig) { static int beeps = 0; printf("BEEPn"); if (++beeps < 5) { alarm(1); } else { printf("BOOM!n"); exit(0); } } ? ? int main() { signal(SIGALRM, handler); alarm(1); for(;;); exit(0); }
Receiving Signals
wtf: 當異常處理程序返回時,準備轉移控制權給進程p時,會檢查非被阻塞的待處理信號的集合(pending&~blocked) if 集合為空: 進程p的邏輯控制流下一跳指令 else: 選擇某個最小信號k,強制p接收信號k goto wtf
每個信號類型預定義的默認行為(查看Figure8.25):
- 進程終止
- 進程終止并轉存儲器(dump core)
- 進程停止直到被SIGCONT信號重啟
- 進程忽略該信號
#include <signal.h> ? ? // 定義信號處理函數(signal handler) // 輸入int為信號量 typedef void (*sighandler_t)(int); // 輸入函數sighandler_t: // handler=SIG_IGN,忽略類型為signum的信號; // handler=SIG_DFL,重置類型為signum信號的行為。 // // 返回:成功=指向前次處理程序指針,出錯=SIG_ERR(不設置errno) sighandler_t signal(int signum, sighandler_t handler); // installing the handler ? ? ? ? /* ctrl-c中斷sleep,并打印睡眠時間 */ #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <signal.h> ? ? void handler(int sig) {} // 改變SIGINT處理函數 ? ? int snooze(unsigned int sec) { int left_sleep_sec = sleep(sec); printf("Slept for %d of %d secs.tUser hits ctrl-c after %d secondsn", left_sleep_sec, sec, sec-left_sleep_sec); } ? ? int main(int argc, char *argv[]) { if (SIG_ERR == signal(SIGINT, handler)) { exit(-1); } ? ? unsigned int sleep_sec = 0; if (argc > 1) { sleep_sec = atoi(argv[1]); } else { exit(0); } printf("sleep for %d secondsn", sleep_sec); snooze(sleep_sec); exit(0); }
Signal Handling Issues
當程序需要捕獲多個信號時,問題產生了。
- 待處理信號被阻塞。Unix信號處理程序通常會阻塞當前處理程序正在處理的類型的待處理信號k。如果另一個信號k傳遞到該進程,則信號k將變成待處理,但是不會被接收,直到處理程序返回。再次檢查發現仍有待處理信號k,則再次調用信號處理函數。
- 待處理信號不會排隊等待。任意類型最多只有一個待處理信號。當目的進程正在執行信號k的處理程序時是阻塞的,當發送兩個信號k,僅第一個信號k會變成待處理,第二個則直接被丟棄,不會排隊等待。
- 系統調用可以被中斷。像read、wait和accept調用過程會阻塞進程的稱謂慢速系統調用,當捕獲到一個信號時,被中斷的慢速系統調用在信號處理返回時不再繼續,而是立即返回用戶一個錯誤條件,并將errno設置為EINTR。(即使sleep被信號處理捕獲后仍會返回)
Explicitly Blocking and Unblocking Signals
#include <signal.h> ? ? // how = SIG_BLOCK, blocked=blocked | set // how = SIG_UNBLOCK, blocked=blocked &~ set // how = SIG_SETMASK, blocked = set int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigemptyset(sigset_t *set); // 將每個信號添加到set int sigfillset(sigset_t *set); // 添加signum到set int sigaddset(sigset_t *set, int signum); // 從set中刪除signum int sigdelset(sigset_t *set, int signum); //Returns: 0 if OK, −1 on error ? ? int sigismember(const sigset_t *set, int signum); //Returns: 1 if member, 0 if not, −1 on error
Nonlocal Jump
作用允許從一個深層嵌套的函數調用中立即返回。
另一個作用是使一個信號處理程序分支到一個特殊的位置sigsetjmp/siglongjmp。
#include <setjmp.h> ? ? int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, int savesigs); // Returns: 0 from setjmp, nonzero from longjmps ? ? void longjmp(jmp_buf env, int retval); void siglongjmp(sigjmp_buf env, int retval); // Never returns ? ? jmp_buf env; rc=setjmp(env); // 保存當前調用環境 if(rc == 0) dosomething(); else if (rc == 1) dosomething1(); // 如果 else if (rc == 2) dosomething2(); ? ? int dosomething() { longjmp(buf,1); // 跳轉到setjmp,返回1 // longjmp(buf,2); // 跳轉到setjmp,返回2 }
操作進程工具
STRACE:打印一個正在運行的程序和它的子程序調用的每個系統調用的軌跡。
PS:列出當前系統中的進程(包括僵尸進程)。
TOP:打印關于當前進程資源使用的信息。
PMAP:顯示進程的存儲器映射。
/proc:一個虛擬文件系統,以ASCII輸出大量內核數據結構的內容。如cat /proc/loadavg,觀察Linux系統上的當前的平均負載。