CSAPP Chapter 8:Exception Control Flow

  prcesssor在運行時,假設program counter的值爲a0, a1, ... , an-1,每一個ak表示相對應的instruction的地址。從ak到ak+1的變化被稱爲control transfer。一系列的control transfers被稱爲control flow。linux

  exceptions是指一些event,這些event代表當前的system、processor或executing program存在某些情況(詳見1.2)。exceptions會致使control flow的突變,典型的就是將控制從當前運行的程序或任務轉移到exception handler的執行(詳見1.1節)。在計算機程序中,咱們設計了jump和branch,call和return;他們經過program state的變化,引發了control flow的突變。exceptions中的control flow的突變是經過system state的變化來引起的,這種control flow的突變被稱爲exception control flow。interrupt做爲exceptions的一種,當由I/O devices complete引起interrupt後,I/O devices經過pin的變化給processor發送signal,並將exception number放到system bus上,processor接收到signal並從system bus上獲取到exception number,而後將控制轉移到exception handler中[1][2][3]。exception control flow除了應用於在exceptions中,也應用於signal(見第4節)、context switch(見2.3節)和setjump中(見第5節)。c++

1.exceptions

1.1 exceptions從產生處處理的流程

  exceptions做爲exception control flow的一種,它從產生處處理的流程由processor和operate system共同實現的。exceptions的流程中processor和operate system分別執行了哪些工做容易令人困惑。exceptions能夠由當前正在執行的instruction而產生,也能夠由I/O devices等外部緣由而產生,由外部緣由產生的異常也被稱爲external interrupt。exceptions在產生時,會出現processor state的變化,同時會生成exception  number。對於external interrupt和其它的exception,exception number的產生機制不一樣。當處理器感知到processor state的變化,就會將控制轉移到exception handler的執行(相似於call procedure)。exception handler是根據exception  number去exception table中匹配到的,exception table的地址存儲在register中。shell

1.1.1生成exception number

  intel 64和IA-32的架構爲每一個processor-detectable exception(包括faults,traps和aborts)定義了一個exception number。對於external interrupt,當local APIC(Advanced Programmable Interrupt Controller )啓用時,exception number由I/O APIC決定,並經過LINT[1:0] pin 告訴processor異常發生了。當local APIC關閉時,exception number由external interrupt controller 決定,並經過INTR/NMI pin 告訴processor異常發生了[2]編程

1.1.2exception handling

  如圖1所示,Interrupt descriptor table(IDT) 將每一個exception number(圖1中爲Interrupt Vector)和用於定位exception handler(圖1中爲interrupt procedure)的gate descriptor關聯起來。圖1適用於IDT爲interrupt/trap gate的狀況[2]。gate中的segment selector指向GDT/LDT中的segment descriptor,segment descriptor指向Destination code segment。gate中的offset指向了exception handler(圖1中爲interrupt procedure)的起始地址。數組

圖1 interrupt procedure call(也稱爲exception handler call)緩存

  IDT能夠存在於linear address space的任何位置。如圖2所示,IA32 processor(IA32e與其類似,只是每一個gate for interrupt的大小爲16bytes)使用IDRT register定位IDT。IDRT register包含32-bit的base address和16-bit的IDT limit[2]安全

  

圖2 IDTR和IDT的關係session

1.1.3 return from exception handler

  當IA32e processor發起對exception handler的調用時,且沒有stack switch的狀況下,IA-32e mode 加載一些變量到當前棧中,如圖3所示[2]。IA-32 mode會按順序保存SS和RSP到當前棧中;保存當前的RFLAGS;保存CS和RIP到當前棧中;若是exceptions發生時有保存error code,error code也會保存到當前棧中。CS(code segment selector)和RIP表示return address,指向faulting instruction的位置。SS(stack segment selector)和RSP指向interrupted procedure的位置。當exception handler執行結束時,會執行IRET instruction從當前的異常處理程序返回,IRET instruction與RET intruction的執行相似。當程序返回時,有下面三種狀況:1)返回到當前faultiong instruction的位置Icurr(參考圖4);2)返回到faulting instrution的下一條指令Inext;3)退出intrrupted program。架構

圖3 Interrupted Procedure’s and Handler’s Stack 併發

 

圖4 Anatomy of an exception 

1.2 exceptions的分類

  exceptions可分爲四類:interrupt、trap、fault和abort。圖5中的表格總結了這四類exceptions的一些特性,能夠看出它們在cause和return behavior上區別。另外,它們的exception handler也各不相同。

  

圖5 exceptionsd的分類

1.2.1 interrupt

  interrupt由I/O devices引發的,不是由當前指令的執行而致使的,它是異步的。像network adapters、disk controllers 和timer chips等I/O devices,它們經過pin的變化告知processor異常發生了,並將exception number 放到system bus上。異步的異常發生時,會先執行完當前instuction,而後纔會作出對異常的反應。當processor感知到interrupt pin走高時,他從system bus中獲取exception number,而後將控制轉移到合適的interrupt handler。當從interrupt handler返回時,會返回到interrupt發生時執行指令的下一條指令Inext

1.2.2 trap 和system calls

  User programs在執行read、fork、execve和exit等函數時,須要kernel中的services提供支持。爲了能訪問到kernel中的service,processor提供了syscall instruction。當syscall instruction執行時,會將控制轉移到exception handler的執行,exception handler先解析參數,而後依據參數調用對應的kernel routine。當從exception handler返回時,會返回到syscall指令的下一條指令Inext。當Unix的系統級函數遇到error時,他會返回-1並設置全局變量errno。

  圖6列出了幾種常見的system call的函數,每一個函數表示一種system call。每一種system call都與惟一的number對應,number表示在kernel中相對於jump table的offset(注意jump table與exception table 不一樣)。不一樣的類型的system call在進行syscall(彙編指令)以前,會將number存儲到參數(好比%rax)中。下圖中的write和_exit函數在編譯後的彙編代碼如圖7所示,write和_exit對應的number分別爲1和60,這些number在syscall指令前存儲到%rax中。

圖6 在x86-64系統中常見的system calls

int main() { write(1,"hello,world\n",13); _exit(0); }
.section .data string:   .ascii "hello, world\n" string_end:   .equ len, string_end - string .section .text .globl main main:   First, call write(1, "hello, world\n", 13)   movq $1, %rax          write is system call 1   movq $1, %rdi          Arg1:stdout has descriptor 1   movq $string, %rsi     Arg2: hello world string   movq $len, %rdx        Arg3: string length   syscall  Make the system call
  Next, call _exit(
0)   movq $60, %rax _exit is system call 60   movq $0, %rdi Arg1: exit status is 0   syscall Make the system call

圖7 syscall的示例代碼(文件位置code/ecf/hello-asm64.sa)

1.2.3 fault

  Faults一般是能夠糾正的異常,異常糾正後中斷的程序會繼續執行。因爲當前instruction執行引起fault時,processor會先將machine state恢復到該instuction執行前的狀態;而後開始執行fault hander。當從fault handler返回時,若是異常獲得糾正時則返回到當前執行instruction Icurr;不然返回到abort routine中,使程序終止。

  page fault exception是fault的典型例子,它發生在當virtual address所在virtual page沒有對應的physical page時。fault handler會在physical memory中選擇victim page做爲新的對應的physical page,而後從新執行fault insrtuction(詳見Chapter9 3.1)。★★引用連接

1.2.4 abort

   abort指不能恢復的fetal errors,好比在DRAM或SRAM崩潰是發生的parity errors等hardware error。abort handler從不會返回到當前執行的程序並繼續執行;它會返回到abort routine中,abort routine會終止程序。

2.process的概念   

  process指當一個正在執行的程序實例。每一個程序運行在process的context中。context包括程序運行須要的state,好比code and data、stack、register、program counter、environment variable和open file descriptor等。當一個程序運行時,shell會建立一個新的process,而後加載並運行executable object file。prrocess的context能夠抽象爲兩個部分:1)An indepent logical control flow;2)A private address space。

  An independent logical control flow是一個抽象概念,它表示在某一特定時刻,一個process中運行的程序對processor是獨享的。一個程序運行時的PC序列被稱爲logical control flow。如圖8所示,processor的control flow被分紅3個logical flow,它們分別被process A、B和C佔有。這3個logical flow是交叉輪流執行的,如圖Process A先執行,緊接着process B執行,而後processC執行,再而後processA執行,最後process C執行。若是兩個precess的logic flow有交叉(好比A-B-A),咱們認爲A、B是併發的。若是兩個logic flow在不一樣的processor上併發的運行,咱們稱它們是並行的。如圖8,A和B、A和C是併發執行的。

圖8 logic control flows

  A private address space也是一個抽象概念,它表示每一個process在硬盤上的virtual address space是私有的(見第8章★★)。圖9展現了virtual address space的結構。address space的上部分是爲kernel保留的,它包含kernel在執行時所需的code、data、heap和stack。address space的下部分是爲user program保留的,它包含code、data、heap和stack等部分。code部分的起始地址爲0x400000。

圖9 process address space

2.1User and Kernel Modes

  爲了給operation system kernel提供封閉的process abstraction,咱們將process分爲user mode和kernel mode,它們具備不一樣的權限。kernel mode能夠執行任何指令,訪問內存的任何位置;user mode不能執行像halt a processor,change the mode bit或者initiate the I/O operations,也不能訪問address space中的kernel virtual memory區域。mode的切換是經過某些control register中的mode bit進行控制的。當mode bit設置時,process處於kernel mode;當mode bit未設置時,process處於user mode。由user mode切換到kernel mode的惟一方式是經過interrupt、trap、fault和abort等exceptions。當exceptions發生時,控制轉移到exception hander,同時process由user mode切換到kernel mode;當從exception handler返回時,process再由kernel mode切換user mode。在user mode下,能夠經過system call的方式間接訪問kernel code and data。

  Linux提供/proc文件系統,能夠在user mode下訪問kernel data structures。 好比,經過/proc/cpuinfo能夠或取CPU型號,經過/proc/process-id/maps能夠獲取某個進程的memeory segments。在2.6版本中提供了/sys filesystem,能夠導出關於system buses和system devices的信息。

2.2 context switch 

  進程間的切換就是一個進程被搶佔,另外一個以前被搶佔的進程開始執行,進程調度由kernel中的scheduler來決定。當processor在多個進程間切換時,會發生context switch。context switch基於exception機制實現的,它也是exception control flow的一種。kernel爲每一個進程維護一個context。context switch包含3個步驟:1)保存當前進程的context;2)還原以前被搶佔進程的context;3)給予新還原的進程控制權。

  如圖10所示,剛開始process A在user mode下執行,當執行到read函數時,會發起system call,將控制轉移到kernel mode下的trap handler。trap handler經過DMA將disk的文件讀取到memory中,這個過程比較耗時(大約幾十毫秒)。此時,kernel中scheduler會進行context switch,由processA切換到processB,而後processB在user mode下執行。當disk controller將文件從disk讀取到memeory後,會引起interrupt。此時,kernel中scheduler會進行context switch,由process B切換到processA,而後process A在user mode下執行。★★★是否進入到interrupt handler。

  

圖10 剖析進程context switch

3.Process Control

  C程序中有許多對進程進行的操做的函數,它們是基於Unix爲C程序提供的system calls的。好比,咱們能夠經過getpid獲取當前執行進程的process ID,經過getppid獲取當前執行進程的父進程的process ID。

#include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void);

  進程的狀態能夠分爲running、stopped和terminated。running指process當前正在執行或者等待被kernel調度執行。stopped指進程暫停,不會被kernel調度,當running進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU signal時,進程變爲stopped;當stopped進程接收到SIGCONT,進程變爲running。terminated指進程永久中止,主要有3種狀況:1)接收終止進程的signal;2)從main任務返回;3)調用exit函數。

#include <stdlib.h>

void exit(int status);

  能夠經過sleep函數讓進程暫停指定的時間。當sleep的時間到達後,函數返回0,進程變爲running;當在sleep過程當中,若是進程接收到signal,則sleep會提早中斷,函數返回剩餘的時間。也能夠經過pause函數來暫停進程,進程只有在接收到signal時纔會變爲running,函數老是返回-1。

#include <stdlib.h> unsigned int sleep(unsigned int secs);
#include <unistd.h>

int pause(void);

3.1 fork

  一個父進程能夠經過調用fork函數建立一個running的子進程。如圖11所示,經過fork建立子進程,子進程將父進程的user-level virtual address space拷貝了一份,子進程和父進程共享physical memeory(見第9章)。★★連接★fork函數在子進程中返回0,在父進程中返回1。圖11的程序運行結果爲:parent : x=1 child : x=2或者child : x=2 parent : x=1。fork函數的特色可總結以下:1)Call once,return twice。fork函數調用一次返回兩次,分別給父進程和子進程返回1和0;2)Corruent execution。父進程和子進程是併發執行的;3)seperated virtual address space,共享physical memory。子進程將父進程user-level vitual address space拷貝了一份,子進程和父進程共享physical memory。

#include <sys/types.h> #include <unistd.h> pid_t fork(void);
#include "csapp.h" 

int main(){ pid_t pid; int x =1; pid = fork(); if(pid==0){ /*child*/ printf("child : x=%d\n",++x); exit(0); }
  
  /*parent*/ printf(

"parent : x=%d\n",--x); exit(0); }

圖11 使用fork函數建立子進程

  一個程序中調用屢次fork函數的狀況容易令人混淆。根據程序代碼畫出進程執行流程圖是有幫助的,在進程流程圖中能夠直觀的看到進程的建立以及各個進程中的重要操做。如圖12所示,調用了兩次fork函數;第一次fork函數調用建立子進程,在新建立的子進程中再次調用fork函數。

圖12 圖解fork函數嵌套執行

  當unix的system-level函數遇到error時,它們一般返回-1並給errno賦值。咱們能夠對fork進行以下異常檢查,但這使得代碼可讀性變差。咱們可使用error-handling wrappers來簡化代碼。Wrapper調用原函數並檢查error。好比,圖13爲函數fork的error-handling wrappers。在後面的章節中都將使用error-handling wrappers,這能夠保持代碼簡潔。

if((pid = fork()<0){ fprintf(stderr,"fork error: %s\n", strerror(errno)); exit(0); }
void unix_errot(char *msg) /*Unix-style error*/ { fprintf(stderr,"%s: %s\n",msg,strerror(errno)); exit(0); } pid_t Fork(void) { pid_t pid; if((pid = fork())<0)   unix_error("Fork error");   return pid; }

圖13 error-handling wrappers函數Fork

3.2 wait

  當一個process執行exit後,process相關的內存和資源都會被釋放,可是process在process table中的process‘s entry仍然保留。狀態爲terminated且還沒有從process table中移除的進程被稱爲zombie。當子進程exit後,會向父進程發送SIGCHLD signal;父進程能夠經過wait函數來接收SIGCHLD signal,而後將zombie從process table中移除。若是父進程沒有成功調用wait函數,zombie將會在process table中遺留。當一個子進程變爲zombie後,能夠經過終止它的父進程來清除zombie,這是由於init process的存在。init process 是全部進程的祖先,他的PID爲1,在系統啓動時即建立,且永遠不會terminated。當子進程的父進程終止時,子進程變爲orphan。kernel將init process做爲全部orphan的父進程。以init process做爲父進程的進程終止後,kernel會安排init process去移除zombie。init process會週期地移除父進程爲init的zombie。在像shells或servers等長期運行的程序中,應該老是移除zombie;若是zombie不能及時移除,可能會引發process table entries不足。

#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *statusp);

  進程在執行wait函數時,若是已經有子進程終止,wait函數會當即返回pid;若是沒有終止的子進程,那麼該進程會暫停執行直到出現子進程終止,而後wait函數返回pid。在wait函數執行過程當中,kernel已經將terminated child在系統中的痕跡移除。相比於wait函數,waitpid函數適用於更復雜的狀況。當參數pid大於0時,只有該pid的進程在父進程的等待範圍內;當參數等於-1時,全部的子進程都在父進程的等待範圍內。options表示進程的等待策略。當一個進程執行waitpid函數且options的值爲默認值0時,若是等待集合中的進程已經有終止的,waitpid函數會當即返回pid;若是等待集合中的進程都沒有終止,那麼該進程會暫停直到等待集合中的進程終止。options的值還能夠爲WNOHANG、WUNTRACED和WCONTINUED。相比於options爲默認值時,當options的值爲WNOHANG時,若是等待集合中的進程都沒有終止,waitpid函數也會當即返回,返回值爲0。相比於options爲默認值時,當options的值爲WUNTRACED時,若是等待集合中的進程發生terminated或stopped時,waitpid函數都會返回。相比於options爲默認值時,當options的值爲WCONTINUED時,若是等待集合中的進程發生terminated或由stopped狀態變爲running狀態時,waitpid函數都會返回。

#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *statusp, int options);

  若是waitpid的參數statusp不爲Null,那麼子進程的信息會被存儲在statusp指向的位置。能夠經過宏命令來解釋statusp,好比WIFEXITED(statusp*)爲true表示子進程是經過exit或return正常終止的。其它的宏命令有WEXITSTATUS、WIFSIGNALED、WTERMSIG、WIFSTOPPED、WSTOPSIG和WIFCONTINUED等。

  若是調用waitpid函數的進程沒有子進程,函數會返回-1,並將errno設置爲ECHILD;若是waitpid函數被一個signal中斷,函數會返回-1,並將errno設置爲EINTR。wait(*statusp)函數能夠當作是waitpid(-1,*statusp,0)。

  圖14爲waitpid函數的使用示例。它的輸出結果爲:

      linux>./waitpid1

      chilld 22966 terminated normally with exit status=100

      chilld 22967 terminated normally with exit status=101。

#include "csapp.h"
#define N 2

int main() {   int status, i;   pid_t pid[N], retpid; /* Parent creates N children */
for (i = 0; i < N; i++)   if ((pid[i] = Fork()) == 0) /* Child */     exit(100+i); /* Parent reaps N children in order */   i = 0;   while ((retpid = waitpid(pid[i++], &status, 0)) > 0) {     if (WIFEXITED(status))       printf("child %d terminated normally with exit status=%d\n",         retpid, WEXITSTATUS(status));     else       printf("child %d terminated abnormally\n", retpid);   } /* The only normal termination is if there are no more children */
  if (errno != ECHILD)     unix_error("waitpid error");   exit(0); }

圖14 按子進程的建立順序移除zombie children

3.3 execve

  execve函數在當前進程的context中加載並運行一個新的程序。當execve正常執行時,沒有返回值;當execve運行error時,返回值爲-1。argv變量表示null-terminated的指針數組,每一個指針指向一個argument string。按照慣例,argv[0]是filename。envp變量表示null-terminated的指針數組,每一個指針指向一個argument string,string的格式是「name=value」。當execve加載filename後,調用start-up code。start-up code會進行stack的設置並進入到程序的main routine。main routine的格式爲int main(int argc, char *argv[], char *envp[]),參數argc表示argv[]中的指針個數。

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

  當main函數開始執行時,user stack的結構如圖15所示。stack的中間是*envp[]和*argv[]表示的指針數組,每一個指針指向一個底端的variable string;stack的頂端是爲start-up函數libc_start_main保留的。

圖15 新程序啓動時的user stack

3.4 使用fork和execve運行程序

  在Unix shells和 Web servers中,fork和execve被大量使用。當打開一個control terminal時,就會運行一個shell程序,shell程序會根據用戶輸入加載並開始運行新的程序。圖16展現了一個簡單的shell程序。這個shell打印命令提示符,等待用戶在stdin輸入command line,而後解析command line並運行command line指定的程序。(★★★圖16的程序還有疑問)

 1  1 #include "csapp.h"
 2  2 #define MAXARGS 128
 3  3 
 4  4 /* Function prototypes */
 5  5  void eval(char *cmdline);  6  6  int parseline(char *buf, char **argv);  7  7  int builtin_command(char **argv);  8  8  
 9  9 int main() 10 10 { 11 11    char cmdline[MAXLINE]; /* Command line */
12 12 
13 13    while (1) { 14 14      /* Read */
15 15      printf("> "); 16 16      Fgets(cmdline, MAXLINE, stdin); 17 17      if (feof(stdin)) 18 18        exit(0); 19 19 
20 20    /* Evaluate */
21 21    eval(cmdline); 22 22    } 23 23 } 24 24 
25 25 /* eval - Evaluate a command line */
26 26 void eval(char *cmdline) 27 27 { 28 28   char *argv[MAXARGS]; /* Argument list execve() */
29 29   char buf[MAXLINE]; /* Holds modified command line */
30 30   int bg; /* Should the job run in bg or fg? */
31 31   pid_t pid; /* Process id */
32 32 
33 33   strcpy(buf, cmdline); 34 34   bg = parseline(buf, argv); 35 35   if (argv[0] == NULL) 36 36   return; /* Ignore empty lines */
37 37 
38 38   if (!builtin_command(argv)) { 39 39     if ((pid = Fork()) == 0) { /* Child runs user job */
40 40       if (execve(argv[0], argv, environ) < 0) { 41 41         printf("%s: Command not found.\n", argv[0]); 42 42         exit(0); 43 43       } 44 44     } 45 45 
46 46     /* Parent waits for foreground job to terminate */
47 47     if (!bg) { 48 48       int status; 49 49       if (waitpid(pid, &status, 0) < 0) 50 50         unix_error("waitfg: waitpid error"); 51 51     } 52 52     else
53 53       printf("%d %s", pid, cmdline); 54 54     } 55 55     return; 56 56   } 57 57 
58 58   /* If first arg is a builtin command, run it and return true */
59 59   int builtin_command(char **argv) 60 60   { 61 61     if (!strcmp(argv[0], "quit")) /* quit command */
62 62       exit(0); 63 63     if (!strcmp(argv[0], "&")) /* Ignore singleton & */
64 64       return 1; 65 65   return 0; /* Not a builtin command */
66 66 } 67 67 
68 68 /* parseline - Parse the command line and build the argv array */
69 69 int parseline(char *buf, char **argv) 70 70 { 71 71   char *delim; /* Points to first space delimiter */
72 72   int argc; /* Number of args */
73 73   int bg; /* Background job? */
74 74 
75 75   buf[strlen(buf)-1] = ’ ’; /* Replace trailing ’\n’ with space */
76 76   while (*buf && (*buf == ’ ’)) /* Ignore leading spaces */
77 77     buf++; 78 78 
79 79 /* Build the argv list */
80 80   argc = 0; 81 81   while ((delim = strchr(buf, ’ ’))) { 82 82     argv[argc++] = buf; 83 83     *delim = ’\0’; 84 84     buf = delim + 1; 85 85     while (*buf && (*buf == ’ ’)) /* Ignore spaces */
86 86       buf++; 87 87   } 88 88   argv[argc] = NULL; 89 89 
90 90   if (argc == 0) /* Ignore blank line */
91 91     return 1; 92 92 
93 93 /* Should the job run in the background? */
94 94   if ((bg = (*argv[argc-1] == ’&’)) != 0) 95 95     argv[--argc] = NULL; 96 96 
97 97   return bg; 98 98 }
shellex.c

 圖 16 一個簡單的shell程序(文件路徑爲code/ecf/shellex.c)

 4.siginal

  signal是由一些system event引發,由kernel發送給指定進程,而後進程做出反應。kernel 經過更改指定進程上的state以向其發送信號,通知進程某些system event的發生。進程對不一樣的signal有默認處理,如圖17;它也能夠經過install signal handler(見4.3)來對signal進行處理。siganl handler的實如今user mode下,這與運行於kernel mode下的exception handler不一樣。signal容許kernel中斷進程,並將控制轉移到signal handler中,它也是exception control flow的一種。

圖17 Linux signals (a)多年前,main memory被core memory的技術實現。「Dumping core」是遺留詞彙,它表示將code和data segments寫入到disk中; (b)表示signal不能被caught或ignored。

4.1 sending signal

  在Unix systems中,有多種方法向進程發送signal,每種方法都基於process group的概念。每一個進程都屬於惟一的process group,每一個進程組有一個process group ID。能夠經過getgprp()獲取當前進程的process group ID。經過setpgid函數將進程pid的進程組改成pgid。若是pgid爲0,則將建立pgid爲進程pid的進程組,並將進程pid加入到該進程組。

#include <unistd.h> pid_t getpgrp(void); int setpgid(pid_t pid, pid_t pgid);

  能夠經過control terminal給進程發送信號,好比經過termianl input和terminal-generated signals。control terminal和進程的關係如圖18所示,一個control terminal能夠對應一個foreground group和多個background groups。若是在control termianl中運行linux> proc3 | proc4 | proc5命令,control terminal中運行的login shell進程(參見3.5)會建立一個foreground job,它包含由Unix pipe鏈接的3個foreground process,分別用來加載運行proc三、proc4和proc5。若是在命令後加上&,則表示在後臺進程運行程序,如linux> proc1|proc2 &表示control termial的login shell進程爲兩個background process分別建立了background job,用來加載運行proc1和proc2。login shell和先後臺進程組的關係如圖19所示。control terminal打開時(沒有顯示的control terminal也會存在★★★),login shell是一直運行的後臺進程。咱們把foreground process和background歸爲同一個session,以login shell做爲session leader(詳見 深刻了解進程 foreground進程組只有一個,是否能夠詳細講解?★★★)。control terminal能夠經過terminal input(後臺進程好像也能夠經過此方法)和terminated-generated signals(好比快捷鍵ctrl+c)向foreground process發送信號;只能經過modern disconnect或關閉control terminal向background process發送SIGHUP信號。

圖18 controlling termianl以及對應的session和process group

圖19 shell和先後臺進程組的關係

  咱們能夠經過/bin/kill程序向其餘進程發送任意signal。好比,當咱們運行命令linux>/bin/kill -9 15213時,將會發送signal 9(SIGKILL)給 進程15213。若是命令中的pid爲負數,則會向進程所在process group的全部進程發送signal,好比linux>/bin/kill -9 -15213。也能夠經過kill函數發送signal給其它進程。若是pid大於0,向進程pid發送信號 sig;若是pid等於0,向當前進程所在進程阻的全部進程發送信號sig;若是pid小於0,向進程組|pid|中的全部進程發送信號sig。一個進程能夠經過調用alarm函數發送SIGALRM信號給本身。若是secs爲0,沒有alarm被調度。在對alarm調用時,若是有pending alarm,則取消pending alarm,並返回其剩餘的seconds;若是沒有pending alarm,則返回0。

#include <sys/types.h> #include <signal.h>

int kill(pid_t pid, int sig);            Returns: 0 if OK,-1 on error
#include <unistd.h> unsigned int alarm(unsigned int secs);               Returns:remaining seconds of previous alarm,or 0 if no previous alarm

4.2 Reciving Signals

   進程接收信號後,默認狀況下會進行以下反應,1)進程terminate;2)進程terminate而且dumps core;3)進程stops直到接收SIGCONT信號;4)進程忽略signal。一個進程經過signal函數來指定進程對某種signal的反應;SIGSTOP和SIGKILL信號除外,進程對它們的默認反應不能被修改。若是handler爲SIG_IGN,信號signum被ignored;若是handler爲SIG_DFL,進程對信號signum的反應還原爲默認;若是handler是user-defined的函數,進程在接收到信號signum時,這個函數會被調用。咱們稱這個函數爲signal handler。經過signal函數把進程對信號的默認反應修改成handler的過程稱爲installing the handler;handler被調用的過程被稱爲catching handler;handler的執行被稱爲handling the handler。當進程接收到signal k後,將控制轉移到signal k對應的signal handler;當signal handler執行完成後,返回到進程中斷的位置繼續執行。

#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);  Returns:pointer to previous handler if OK,SIG_ERR on error

  進程中還存儲有pending和blocked的bit vectors信息。pending和blocked分別表示進程pending signal set和blocked signal set。pending signal指已經發送,但還沒被接收的signal;一個進程對於特定類型的signal只能有一個pending signal,也就是說當一個進程對於特定類型的signal已經有了pending signal,那麼發送到該進程的特定類型的signal將被忽略。一個進程能夠鎖定某些signal,這些signal能夠被髮送到該進程,可是不會被接收,除非進程解鎖這些signal。當signal k被髮送時,pending中的bit k被設置;當signal被接收時,pending中的bit k被清除,如圖20所示。(圖示是否正確?★★★)可經過sigprocmask函數來設置或清除blocked中的bit k,以實現對blocked signal的添加和刪除。sigprocmask的參數how能夠爲如下值:1)SIG_BLOCK表示將set中的全部signal添加到blocked中(blocked = blocked | set);2)SIG_UNBLOCK表示將set中的signal從blocked中移除(blocked=set&~blocked);3)SIG_SETMASK表示blocked=set。參數oldset表示以前的狀態爲blocked的signal set。能夠經過如下函數對signal set進行操做:sigemptyset函數將set初始化爲empty set;sigfillset函數將全部的signal添加到set中;sigaddset函數將信號signum添加到set中;sigdelset函數刪除set中的信號signum;當signum是set的成員時sigismember返回1,不是set的成員時返回0。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t  *oldset); int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int 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

圖20  當signal被髮送時,pending中的bit k被設置

  當kernel將進程p由kernel mode切換到user mode(好比從system call返回或完成一個context switch)時,kernel會檢查進程 p的unblocked pending signals(pending&~blocked)。若是unblocked pending signals爲空,進程會繼續執行下一條指令Inext;若是unblocked pending signals不爲空,則先從unblocked pending signals中最小的非零bit k開始,進程接收signal k並進入到signal handler(多個signal handler會不會交叉?★★),而後以bit k遞增的順序重複上面的操做。當全部的signal handler執行完畢,返回到進程終止的位置繼續執行Inext

  signal handlers能夠被其它handlers中斷,如圖21所示。如圖,main程序catch signal s,中斷main程序並將控制轉移到handler S。當S在運行時,主程序catch signal t≠s,它將中斷S並將控制轉移到handler T。當T返回時,S從中斷位置恢復執行。最後,S返回並將控制轉移到main程序的中斷位置以恢復執行。

圖21 Handlers能夠被其它handlers中斷

4.3 signal handler

  signal handling是Linux系統級編程中的一個棘手問題。如圖21所示,Handers和main程序是併發執行的;若是他們共用全局變量,會有併發安全的問題。爲了幫助大家寫出併發安全的程序,下面給出了handler函數的幾條書寫指南,它們在大多數時候能夠保證併發安全。

  1)保證handlers儘量簡單。好比,在handler僅設置一個global flag並當即返回,全部進程在signal接收後的操做都由main程序經過週期性檢查(和重置)那個global flag來實現。

  2)在handler中只調用async-signal-safe的函數。圖22展現了Linux中async-signal-safe的函數,它們執行時不會其它的signal handler打斷。咱們也能夠在函數中只使用局部變量,以保證併發安全。在signal handler中向終端輸出的函數中只有write是安全的,像printf和sprintf是不安全的。咱們開發了Sio(Safe I/O)包,用於打印signal handler中的一些信息。函數sio_putl 和sio_puts分別向終端輸出一個long和string。函數sio_error打印異常信息並終止。(具體的例子?★★★)

#include "csapp.h" ssize_t sio_putl(long v); ssize_t sio_puts(char s[]); Returns:number of bytes transferred if OK,-1 on error void sio_error(char s[]);

圖22 Async-signal-safe functions

  3) 在handler中保存並恢復errno。許多Linux中的async-signal-safe函數因爲error返回時會設置errno。若是在handler調用這些函數,會對main程序中依賴errno的部分形成干擾。爲了handler可能引發的干擾,咱們handler的入口處將errno保存爲局部變量,在返回前還原errno的值。若是handler不返回,而是直接exit,那麼就沒有必要這要作了。

  4)在訪問共用全局變量時block all signals。好比,handler和main程序共用全局變量,那麼在handler和main程序對全局變量訪問時要暫時block all signal。這是由於對某個data structure d的訪問可能包含一個instruction序列,若是main程序在intruction序列中間發生中斷,那麼handler極可能發現d處在不連續的狀態並致使意外的結果。(暫時沒想到好例子★★★)

  5)使用volatile修飾全局變量。好比,handler和main程序共用全局變量g,當handler對g進行修改後,main程序讀取g。因爲編譯器的優化,main程序中可能緩存了g的複本,致使在handler對g修改後main程序讀取的g仍然不變。用volatile修飾全局變量後,編譯器不會再緩存該變量。

  6)聲明sig_atomic_t類型的flags。當像條目1)中同樣只對flag進行讀寫操做時,flag可使用sig_atomic_t類型以保證讀寫的原子性。當對flag進行諸如flag ++和flag = flag + 10等操做時,使用sig_atomic_t類型也沒法保證原子性,這些操做包含多條instruction。

4.4 signal handler相關案例

4.4.1 correct signal handling 

  如圖23所示,main函數中先爲信號SIGCHLD installing handler,而後循環建立子進程。當子進程exit時,會向main程序發送SIGCHLD信號,main程序跳轉到installing handler執行。在installing handler中,waitpid函數會移除狀態爲terminated的子進程殘留記錄。圖23的執行結果爲:(結果是否惟一★★★)

  linux> ./signal1

  Hello from child 14073

  Hello from child 14074

  Hello from child 14075

  Handler reaped child

  Handler reaped child

  CR

  Parent processing input

  從執行結果能夠看出,共建立了3個子進程,但只清除了2個狀態爲terminated的子進程(zombie children)的殘留記錄,這與預想的不一致。這是因爲當handler接收到第一個SIGCHLD信號後,休眠了1s。當第三個SIGCHLD發送給主程序時,正在執行第一個SIGCHLD的handler,第二個SIGCHLD正處於pending狀態,因爲pending signal最多隻能有一個,第三個SIGCHLD將被忽略。爲了解決這個問題,咱們要明白pending signal的存在代表進程至少接收到了一個SIGCHLD信號,所以咱們在signal handler中儘量多的清除zombie children。圖24展現了修改後的程序。

 1 /* WARNING: This code is buggy! */
 2 void handler1(int sig)  3 {  4   int olderrno = errno;  5 
 6   if ((waitpid(-1, NULL, 0)) < 0)  7     sio_error("waitpid error");  8   Sio_puts("Handler reaped child\n");  9   Sleep(1); 10   errno = olderrno; 11 } 12 
13 int main() 14 { 15   int i, n; 16   char buf[MAXBUF]; 17 
18   if (signal(SIGCHLD, handler1) == SIG_ERR) 19     unix_error("signal error"); 20 
21   /* Parent creates children */
22   for (i = 0; i < 3; i++) { 23     if (Fork() == 0) { 24       printf("Hello from child %d\n", (int)getpid()); 25       exit(0); 26     } 27   } 28 
29   /* Parent waits for terminal input and then processes it */
30   if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0) 31     unix_error("read"); 32 
33   printf("Parent processing input\n"); 34   while (1) 35   ; 36 
37   exit(0); 38 }

圖23 signal1;這個程序有缺陷,它假定了signal能夠排隊

 1  void handler2(int sig)  2 {  3   int olderrno = errno;  4 
 5   while (waitpid(-1, NULL, 0) > 0) {  6     Sio_puts("Handler reaped child\n");  7   }  8   if (errno != ECHILD)  9   Sio_error("waitpid error"); 10   Sleep(1); 11   errno = olderrno; 12 }

 圖24 signal2;這是對圖19中signal1的改進,它考慮到了signal不會排隊

4.4.2 Synchronizing Flows to avoid race (★★★訪問全局變量須要鎖定其它信號,不鎖定其它信號有什麼後果?)

  併發程序在相同的存儲位置上讀寫的安全問題是幾代計算機科學家的挑戰。程序控制流交錯的數量在指令數量上是指數級的。圖25所示的程序就存在併發安全的問題,程序中main任務和signal-handling控制流之間交錯着,函數deletejob可能在函數addjob以前執行,致使在job list上遺留一個incorrect entry。這種經典的併發錯誤被稱爲race。main任務中的addjob和handler中的deletejob進行race,若是addjob贏得race,那麼結果正確;不然結果錯誤。爲了解決這個問題,能夠經過在調用fork以前blocking SIGCHLD並在調用addjob以後unblocking SIGCHLD,以保證全部子進程在加入job list後被清除,如圖26所示。

 1 void handler(int sig)  2 {  3   int olderrno = errno;  4   sigset_t mask_all, prev_all;  5   pid_t pid;  6 
 7   Sigfillset(&mask_all);  8   while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
 9     Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); 10     deletejob(pid); /* Delete the child from the job list */
11     Sigprocmask(SIG_SETMASK, &prev_all, NULL); 12   } 13   if (errno != ECHILD) 14     Sio_error("waitpid error"); 15     errno = olderrno; 16} 17 
18 int main(int argc, char **argv) 19 { 20   int pid; 21   sigset_t mask_all, prev_all; 22 
23   Sigfillset(&mask_all); 24   Signal(SIGCHLD, handler); 25   initjobs(); /* Initialize the job list */
26 
27   while (1) { 28     if ((pid = Fork()) == 0) { /* Child process */
29       Execve("/bin/date", argv, NULL); 30     } 31     Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent process */
32     addjob(pid); /* Add the child to the job list */
33     Sigprocmask(SIG_SETMASK, &prev_all, NULL); 34   } 35   exit(0); 36 }

圖25 一個有同步錯誤的shell程序(code/ecf/promask1.c)

 1 void handler(int sig)  2 {  3   int olderrno = errno;  4   sigset_t mask_all, prev_all;  5   pid_t pid;  6 
 7   Sigfillset(&mask_all);  8   while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
 9     Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); 10     deletejob(pid); /* Delete the child from the job list */
11     Sigprocmask(SIG_SETMASK, &prev_all, NULL); 12   } 13   if (errno != ECHILD) 14     Sio_error("waitpid error"); 15     errno = olderrno; 16 } 17 
18 int main(int argc, char **argv) 19 { 20   int pid; 21   sigset_t mask_all, mask_one, prev_one; 22 
23   Sigfillset(&mask_all); 24   Sigemptyset(&mask_one); 25   Sigaddset(&mask_one, SIGCHLD); 26   Signal(SIGCHLD, handler); 27   initjobs(); /* Initialize the job list */
28 
29   while(1){ 30     Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
31     if ((pid = Fork()) == 0) { /* Child process */
32       Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
33       Execve("/bin/date", argv, NULL); 34     } 35     Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
36     addjob(pid); /* Add the child to the job list */
37     Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
38   } 39   exit(0); 40 }

圖26 對圖21中程序的改進;使用Sigprocmask同步進程(code/ecf/promask2.c)

4.4.3 explictly waiting for Signals

  有時main程序須要等待直到特定的signal handler開始執行。好比說,當一個Linux shell建立了一個foreground job,它在執行下一個用戶命令前須要等待job終止並經過SIGCHLD handler移除job殘留。如圖27所示,main程序在建立子進程後,經過while循環等待子進程終止以及SIGCHLD handler將子進程殘留清除。可是圖中沒有循環體的while循環很是浪費處理器的資源,咱們能夠在while的循環體中添加pause函數。當main程序接收到SIGCHLD信號後,會從pause中被喚醒並跳轉到signal handler;使用while循環的緣由是pause也可能被其它信號打斷。這種方式的缺陷是當程序在while後pause前接收到SIGCHLD,那麼pause可能會永遠休眠。使用sleep代替pause能夠避免程序永遠休眠,可是函數sleep的參數secs(見第3節)很差設定。若是程序在while後sleep前接收到SIGCHLD,且sleep的參數secs設置過大,好比sleep(1),那麼程序將會等待很長時間(相對而言)。若是sleep的參數secs設置太小,則會浪費處理器的資源。

while(!pid) pause();

while(!pid) sleep(1);

  更好的方式是使用sigsuspend函數。sigsuspend函數等同於如下代碼(具備原子性):

sigprocmask(SIG_SETMASK,&prev,NULL); pause(); sigprocmask(SIG_BLOCK,&mask,&prev);
#include <signal.h>

int sigsuspend(const sigset_t *mask);

sigsuspend函數暫時地將blocked set設置爲prev,以解鎖mask,直到接收到信號,信號通知進程運行handler或者終止進程。若是信號通知終止進程,那麼進程將不會從sigsuspend返回。若是信號通知進程運行handler,那麼sigsuspend會在handler返回後返回,並在返回前將blocked set恢復到sigsuspend剛被調用時的狀態。圖28中展現了在圖27的while循環中填充sigsuspend後的程序,它節約了處理器的資源。sigsuspend相比pause而言,避免了進程一直休眠的狀況;相比sleep而言更高效。

#include "csapp.h"
volatile sig_atomic_t pid; void sigchld_handler(int s) {   int olderrno = errno;   pid = waitpid(-1, NULL, 0);   errno = olderrno; } void sigint_handler(int s) { } int main(int argc, char **argv) {   sigset_t mask, prev;   Signal(SIGCHLD, sigchld_handler);   Signal(SIGINT, sigint_handler);   Sigemptyset(&mask);   Sigaddset(&mask, SIGCHLD);   while (1) {     Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
    if (Fork() == 0) /* Child */       exit(0);     /* Parent */     pid = 0;     Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock SIGCHLD */
  
    /* Wait for SIGCHLD to be received (wasteful) */
    while (!pid)     ;     /* Do some work after receiving SIGCHLD */     printf(".");   }   exit(0); }

圖27 使用spin loop等待信號;程序是正確的,可是浪費處理器資源

 1 while (1) {  2   Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
 3   if (Fork() == 0) /* Child */
 4     exit(0);  5 
 6   /* Wait for SIGCHLD to be received */
 7   pid = 0;  8   while (!pid)  9     sigsuspend(&prev); 10 
11   /* Optionally unblock SIGCHLD */
12   Sigprocmask(SIG_SETMASK, &prev, NULL); 13 
14   /* Do some work after receiving SIGCHLD */
15   printf("."); 16 }

圖28 使用sigsuspend函數等待信號(請結合圖22)

5.nonlocal jump

  nonlocal jump將控制直接從一個函數轉移到另外一個當前正在執行的函數,它是user-level exception control flow。nonlocal jump經過函數setjmp和longjmp來實現。函數setjmp保存current calling environment在env buffer中,並返回0;env buffer在後面的函數longjmp會使用。current calling environment包含the program counter,stack pointer和general-purpose registers。因爲某些超出咱們知識範圍的緣由,函數setjmp的返回值不能被變量接收,好比rc = setjmp(env)是錯誤的。可是,函數setjmp卻能夠在switch或條件語句的條件判斷中使用。函數longjmp從env buffer中恢復the calling environment並將longjump的參數retval做爲setjmp的返回值。

#include <setjmp.h>

int setjmp(jmp_buf env); int sigsetjmp(sigjmp_buf env, int savesigs); void longjmp(jmp_buf env,int retval); void siglongjmp(sigjmp_buf env, int retval);

  longjmp的一個應用是從函數調用時深度嵌套的代碼中直接返回,一般是因爲檢測到error condition。從深度嵌套的代碼中直接返回相比於常規的call-and-return,不用先彈出調用棧。如圖29所示,main函數首先調用setjmp保存current calling environment,而後調用函數foo,函數foo中又調用了函數bar。當函數foo或函數bar中遇到error時,會經過longjmp直接將控制轉移到the calling environment,也就是轉移到函數setjmp。setjmp將有一個不爲0的返回值,它表示error的類型;接着error將獲得處理。longjmp相比於常規的call-and-return,避開了直接函數調用的意想不到的後果。好比,在函數調用時,咱們allocate了一些data structure,在尚未deallocate這些data structure時出現了error,那麼deallocation將不會進行,可能會引發數據泄露。

 1 #include "csapp.h"
 2 
 3 jmp_buf buf;  4 
 5 int error1 = 0;  6 int error2 = 1;  7 
 8 void foo(void), bar(void);  9 
10 int main() 11 { 12   switch(setjmp(buf)) { 13   case 0: 14     foo(); 15     break; 16   case 1: 17     printf("Detected an error1 condition in foo\n"); 18     break; 19   case 2: 20     printf("Detected an error2 condition in foo\n"); 21     break; 22     default: 23   printf("Unknown error condition in foo\n"); 24   } 25   exit(0); 26 } 27 
28 /* Deeply nested function foo */
29 void foo(void) 30 { 31   if (error1) 32     longjmp(buf, 1); 33   bar(); 34 } 35 
36 void bar(void) 37 { 38   if (error2) 39     longjmp(buf, 2); 40 }

圖29  nonlocal jump exception(文件位置code/ecf/setjmp.c)

  nonlocal jumps的另外一個應用是在signal handler中直接將控制轉移到特定的代碼位置,而不是以前因爲接受到信號而中斷的指令。圖30所示爲使用sigsetjmp和siglongjmp的程序。程序在終端運行時,前後屢次按下ctrl+C時,輸入出的結果以下:

  linux> ./restart

  starting

  processing...

  processing...

  ctrl+C

  restarting

  processing...

  ctrl+C

  restarting

  processing...

 1 #include "csapp.h"
 2 
 3 sigjmp_buf buf;  4 
 5 void handler(int sig)  6 {  7   siglongjmp(buf, 1);  8 }  9 
10 int main() 11 { 12   if (!sigsetjmp(buf, 1)) { 13     Signal(SIGINT, handler); 14     Sio_puts("starting\n"); 15   } 16   else
17     Sio_puts("restarting\n"); 18 
19   while(1) { 20     Sleep(1); 21     Sio_puts("processing...\n"); 22     } 23   exit(0); /* Control never reaches here */
24 }

圖30 一個nolocal jump程序,當用戶按下ctrl+C後會重啓(文件位置code/ecf/restart.c)

  注意圖30中的main程序中的exit(0)是不會執行到的,這保證了調用longjmp進行控制轉移時main程序是在執行中的。

參考資料:

[1] Randal E.Bryant,David R. O'Hallaron.Computer Systems:A Programmer's Perspective,3/E(CS:APP3e).

[2] Chapter 6 Interrupt and exception handling  ★★

[3] I/Odevice 引發pin變化的資料; ★★

相關文章
相關標籤/搜索