1. 程式人生 > >CSAPP第八章-異常控制流(二)

CSAPP第八章-異常控制流(二)

程序

  1. 程序是一個執行中的程式的例項。系統中的每個程式都是執行在某個程序的上下文中。上下文由程式正確執行所需的狀態組成。這個狀態包括,存放在儲存器中的程式碼和資料,它的棧,通用目的暫存器內容,程式計數器,環境變數,以及開啟檔案描述符的集合。

  2. 每次使用者向外殼(shell)執行一個可執行目標檔案,shell會建立一個新的程序,然後再這個新程序的上下文中執行這個程式。應用程式也可以自己建立程序,並在建立的新程序中執行自己的程式碼或其他應用程式。

私有地址空間

  1. 程序為每個應用程式提供一個假象,好像它獨佔地使用系統地址空間。在一臺有n位地址的機器上,地址空間是2n個可能地址的集合,0 ~ 2n

    -1。一般而言,和這個程序地址空間中的某個地址相關聯的儲存器位元組,是不能被其他程序讀寫的。(肯定啊)從這個意義上說,這個地址空間是私有的。

  2. 地址空間的頂部(?~ 2n-1)是保留給核心的。這個部分包含核心的程式碼、資料、堆、棧等。

使用者模式和核心模式

  1. 通過某個控制暫存器的一個模式位(mode bit),來控制程序執行在哪個模式。

  2. 執行應用程式程式碼的程序初始時是在使用者模式中。程序從使用者模式變為核心模式的唯一方法是通過中斷、故障或者陷入陷阱進行系統呼叫。

  3. Linux的/proc檔案系統。它允許使用者模式程序訪問核心資料結構的內容。/proc檔案系統將許多核心資料結構的內容輸出為一個使用者程式可以讀的文字檔案的層次結構。比如/proc/cpuinfo檢視cpu型別,/proc//maps檢視程序使用的儲存器段。

上下文切換

  1. 發生時機

    • 當程式切換到核心模式執行系統呼叫時,可能發生。如果系統呼叫因為某個等待的時間發生而阻塞,那麼核心可以讓當前程序休眠,切換到另一個程序。比如,一個read系統呼叫請求一個磁碟訪問,核心可以選擇執行上下文切換,執行另一個程序,而不是等待資料從磁碟到達。另一個示例是sleep系統呼叫,它顯式的請求讓呼叫程序休眠。

    • 中斷也可能引發上下文切換。每次發生定時器中斷時,核心就判定當前程序已經運行了足夠長的時間,並切換到一個新的程序。
      上下文切換

  2. 中斷處理程式/上下文切換 汙染快取記憶體
    中斷處理程式如果訪問了足夠多的表項,那麼再切換回應用程式時,快取是冷的。上下文切換也會出現同樣的情況。(那怎麼辦?)

系統呼叫錯誤處理

當Unix系統級函式遇到錯誤時,它們會典型地返回-1,並設定全域性整數變數errno來表示出了什麼錯。可以用
strerror(errno)來返回errno此時相關聯的錯誤。

程序控制

程序ID

  • 每個程序都有一個唯一的正數ID,稱為PID。

  • 可以用getpid()來獲取。getppid()可以返回它父程序的PID。

建立、終止程序。

  1. 相同、獨立的地址空間。父子程序擁有相同的使用者棧、相同的本地變數值、相同的堆、相同的全域性變數值、以及相同的程式碼。子程序得到與父程序使用者級虛擬地址空間相同,但是獨立的一份拷貝。包括文字、資料和bss段、堆以及使用者棧。對比一下第七章講的程序儲存器映像,除下核心地址空間和共享庫,其他都拷貝了。

  2. 子程序還獲得和父程序任何開啟的檔案描述符相同的拷貝。這就意味著呼叫fork後,子程序可以讀寫fork之前父程序開啟的任何檔案。

  3. 阻塞,程序的執行被暫時掛起(suspend)。當收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU訊號時,程序就會阻塞掛起,直到它收到一個SIGCONT訊號才會繼續。訊號是一種軟體中斷的形式。

  4. 終止,程序永遠停止。程序會因為三種原因終止:

    • 收到一個訊號,訊號的預設行為是終止程序。(比如: kill -9)

    • 從主程式(main)返回

    • 呼叫exit函式。exit函式以status退出狀態來終止程序(另一種設定退出狀態的是從主程式返回一個數值)

回收子程序

僵死程序

  1. 當一個程序由於某種原因終止時,核心並不是立即把它從系統中清除。相反,程序會保持在一種已終止的狀態中,知道被它的父程序回收。當父程序回收已終止的子程序時,核心將子程序的退出狀態(上一小節說的exit status?)傳遞給父程序,然後拋棄已終止的程序,從此時開始,該程序就不存在了。一個終止了,但還未被回收的程序稱為僵死程序。

  2. 如果父程序沒有回收它自己的僵死子程序就終止了,那麼核心會安排init程序來回收它們。init程序PID為1,並且是由系統初始化時核心建立的。

等待子程序結束

1. waitpid(pid_t pid, int *status, int options)

2. wait(int *status) ,相當於呼叫waitpid(-1, &status, 0)

休眠

#include <unistd.h>

unsigned int sleep(unsigned int secs);

如果請求的時間量到了,sleep返回0,否則返回還剩下的要休眠的秒數。後一種情況,可能發生在sleep函式被一個訊號中斷而提前返回的情況。

#include <unistd.h>

int pause(void);

pause()函式使呼叫者休眠,直到該程序收到一個訊號。

載入並執行程式,execve

#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);

execve函式載入並執行可執行檔案filename,且帶引數列表argv和環境變數列表envp。execve呼叫一次從不返回,除非出現錯誤。main函式也是三個引數,不過envp是隱藏的預設引數。

int main(int argc, char *argv[], char *envp[]);

使用者棧典型結構

使用者棧典型結構,注意啦,複習下程序的儲存器映像,棧是從高地址向地地址分配,所以棧底的地址比棧頂大。argv和envp,都是以一個null元素結尾,因此即使不知道argv和envp的長度,也可以依次打印出來,而避免越界。

幾個操作envp的函式

#include <stdlib.h>

char *getenv(const char *name);

int setenv(const char *name, const char *newvalue, int overwrite);

void unsetenv(const char *name);

fork和execve

區別

fork函式在新的子程序中執行相同的程式,新的子程序時父程序的一個複製品。execve函式在當前程序的上下文中執行一個新的程式。它會覆蓋當前程序的地址空間,但是並沒有建立一個新程序。新的程式仍然有相同的PID,並且繼承了呼叫execve函式時,已開啟的所有檔案描述符。

利用fork和execve執行程式

shell程序就是這樣來執行命令。簡單版如下,再結合waitpid就可以實現相應的後臺執行等功能。

if(pid = fork() == 0)
{
   if(execve(argv[0], argv, environ) < 0)
   {
      printf("%s: Command not found.\n", argv[0]);
      exit(0);
   }
}