寫在前面的話:此系列文章為筆者學習CSAPP時的個人筆記,分享出來與大家學習交流,目錄大體與《深入理解計算機系統(tǒng)》書本一致。因是初次預(yù)習時寫的筆記,在復(fù)習回看時發(fā)現(xiàn)部分內(nèi)容存在一些小問題,因時間緊張來不及再次整理總結(jié),希望讀者理解。
《深入理解計算機系統(tǒng)(CSAPP)》第3章 程序的機器級表示 - 學習筆記_友人帳_的博客-CSDN博客
《深入理解計算機系統(tǒng)(CSAPP)》第5章 優(yōu)化程序性能 - 學習筆記_友人帳_的博客-CSDN博客
《深入理解計算機系統(tǒng)(CSAPP)》第6章 存儲器層次結(jié)構(gòu) - 學習筆記_友人帳_的博客-CSDN博客
《深入理解計算機系統(tǒng)(CSAPP)》第7章 鏈接- 學習筆記_友人帳_的博客-CSDN博客
《深入理解計算機系統(tǒng)(CSAPP)》第8章 異常控制流 - 學習筆記_友人帳_的博客-CSDN博客
《深入理解計算機系統(tǒng)(CSAPP)》第9章虛擬內(nèi)存 - 學習筆記_友人帳_的博客-CSDN博客
第八章 異??刂屏?/h2>
**控制流:**處理器執(zhí)行的指令序列。
- 可以對(由程序變量表示)程序狀態(tài)的變化做出反應(yīng):跳轉(zhuǎn)和分支、調(diào)用和返回。
- 難以對系統(tǒng)狀態(tài)的變化(不能用程序變量表示)做出反應(yīng):磁盤或網(wǎng)絡(luò)適配器的數(shù)據(jù)到達、除零錯誤、用戶的鍵盤輸入(Ctrl-C)、系統(tǒng)定時器超時。
**異??刂屏?Exceptional Control Flow, ECF):**通過使控制流發(fā)生突變對這些情況做出反應(yīng)。
發(fā)生在計算機系統(tǒng)的所有層次:
- 低層機制(硬件層)
- 異常:由操作系統(tǒng)和硬件共同實現(xiàn),硬件檢測到的事件會觸發(fā)控制轉(zhuǎn)移到異常處理程序。
- 高層機制
- 進程切換(Process context switch):通過操作系統(tǒng)和硬件定時器實現(xiàn)
- 信號(Signals):操作系統(tǒng)實現(xiàn)
- 非本地跳轉(zhuǎn)(Nonlocal jumps):setjmp()和longjmp(),C運行庫實現(xiàn),跨越函數(shù)之間的控制權(quán)跳轉(zhuǎn)
1. 異常
異常(exception)就是控制流中的突變,用來響應(yīng)處理器狀態(tài)中的某些變化。(為響應(yīng)某個事件將控制權(quán)轉(zhuǎn)移到操作系統(tǒng)內(nèi)核中的情況(內(nèi)核:操作系統(tǒng)常駐內(nèi)存的部分))。
在處理器中,狀態(tài)被編碼為不同的位和信號。狀態(tài)變化稱為事件(event)。事件可能和當前指令的執(zhí)行直接相關(guān)(虛擬內(nèi)存缺頁、算術(shù)溢出,除以零),方也可能和當前指令的執(zhí)行沒有關(guān)系(系統(tǒng)定時器產(chǎn)生信號、一個I/O請求完成)。
在任何情況下,當處理器檢測到有事件發(fā)生時,它就會通過一張叫做異常表(exception table)的跳轉(zhuǎn)表,進行一個間接過程調(diào)用(異常),到一個專門設(shè)計用來處理這類事件的操作系統(tǒng)子程序(異常處理程序(exception handler))。
當異常處理程序完成處理后,根據(jù)引起異常的事件的類型,會發(fā)生以下3種情況中的一種:
1)處理程序?qū)⒖刂品祷亟o當前指令 I c u r r I_{curr} Icurr?,即當事件發(fā)生時正在執(zhí)行的指令。
2)處理程序?qū)⒖刂品祷亟o I n e x t I_{next} Inext?,如果沒有發(fā)生異常將會執(zhí)行的下一條指令。
3)處理程序終止被中斷的程序。Abort
1.1 異常處理
需要硬件和軟件緊密合作。系統(tǒng)中可能的每種類型的異常都分配了一個唯一的非負整數(shù)的異常號(exception number)。
-
在系統(tǒng)啟動時,操作系統(tǒng)分配和初始化一張稱為異常表的跳轉(zhuǎn)表,使得表目k包含異常k的處理程序的地址。
-
在處理器檢測到發(fā)生了一個事件,并且確定了相應(yīng)的異常號k時,處理器觸發(fā)異常(執(zhí)行間接過程調(diào)用),通過異常表的表目k跳轉(zhuǎn)到相應(yīng)的處理程序。
-
由異常處理程序在軟件中完成處理工作。處理完成后,執(zhí)行一條特殊的“從中斷返回”指令。如果異常中斷的是一個用戶程序,就將狀態(tài)恢復(fù)為用戶模式,然后將控制返回給被中斷的程序。
- 異常號是到異常表中的索引,異常表的起始地址放在一個叫做異常表基址寄存器(exception table base register)的特殊CPU寄存器里。
異常與過程調(diào)用的不同之處:
- 過程調(diào)用時,在跳轉(zhuǎn)到處理程序之前,處理器將返回地址壓入棧中。而根據(jù)異常類型,返回地址或是 I c u r r I_{curr} Icurr?或是 I n e x t I_{next} Inext?。
- 如果控制從用戶程序轉(zhuǎn)移到內(nèi)核,所有這些項目都被壓到內(nèi)核棧中,而不是壓到用戶棧中。
- 異常處理程序運行在內(nèi)核模式下,這意味著它們對所有的系統(tǒng)資源都有完全的訪問權(quán)限。
1.2 異常的類別
- 異步異常是由處理器外部的I/O設(shè)備中的事件產(chǎn)生的。同步異常是執(zhí)行一條指令的結(jié)果。
- 分為中斷(interrupt)、陷阱(trap)、故障(fault)和終止(abort)。
(1)中斷
- 中斷是異步發(fā)生的,是來自處理器外部的I/O設(shè)備的信號的結(jié)果。(硬件中斷不是由任何一條專門的指令造成的,從這個意義上來說它是異步的)
Examples: I/O設(shè)備,例如網(wǎng)絡(luò)適配器、磁盤控制器和定時器芯片,通過向處理器芯片上的一個引腳發(fā)信號,并將異常號放到系統(tǒng)總線上,來觸發(fā)中斷,這個異常號標識了引起中斷的設(shè)備。
(2)陷阱和系統(tǒng)調(diào)用
- 陷阱是有意的異常,是執(zhí)行一條指令的結(jié)果。
- 陷阱最重要的用途是在用戶程序和內(nèi)核之間提供一個像過程一樣的接口,叫做系統(tǒng)調(diào)用。
系統(tǒng)調(diào)用:
用戶程序經(jīng)常需要向內(nèi)核請求服務(wù),比如讀一個文件(read)、創(chuàng)建一個新的進程(fork)、加載一個新的程序(execve),或者終止當前進程(exit)。
為了允許對這些內(nèi)核服務(wù)的受控的訪問,處理器提供了一條特殊的“syscall n”指令,當用戶程序想要請求服務(wù)n時,可以執(zhí)行這條指令。執(zhí)行syscal1指令會導致一個到異常處理程序的陷阱,這個處理程序解析參數(shù),并調(diào)用適當?shù)膬?nèi)核程序。
從程序員的角度來看,系統(tǒng)調(diào)用和普通的函數(shù)調(diào)用是一樣的。然而,它們的實現(xiàn)非常不同。
- 普通的函數(shù)運行在用戶模式中,用戶模式限制了函數(shù)可以執(zhí)行的指令的類型,而且它們只能訪問與調(diào)用函數(shù)相同的棧。
- 系統(tǒng)調(diào)用運行在內(nèi)核模式中,內(nèi)核模式允許系統(tǒng)調(diào)用執(zhí)行特權(quán)指令,并訪問定義在內(nèi)核中的棧。
(3)故障
-
由錯誤情況引起,可能能夠被故障處理程序修正。
-
當故障發(fā)生時,處理器將控制轉(zhuǎn)移給故障處理程序。如果處理程序能夠修正這個錯誤情況,它就將控制返回到引起故障的指令,從而重新執(zhí)行它。否則,處理程序返回到內(nèi)核中的abort例程,abort例程會終止引起故障的應(yīng)用程序。
Examples:缺頁故障(可恢復(fù)),保護故障(protection faults, 不可恢復(fù)),浮點異常(floating point exceptions)。
(4)終止
- 不可恢復(fù)的致命錯誤造成的結(jié)果,通常是一些硬件錯誤。
- 終止處理程序從不將控制返回給應(yīng)用程序。將控制返回給一個abort例程,該例程會終止這個應(yīng)用程序。
Examples: 比如DRAM或者SRAM位被損壞時發(fā)生的奇偶錯誤。
1.3 Linux/x86-64系統(tǒng)異常示例
Linux的系統(tǒng)調(diào)用:
- 每個系統(tǒng)調(diào)用都有一個唯一的整數(shù)號,對應(yīng)于一個到內(nèi)核中跳轉(zhuǎn)表的偏移量。(注意:這個跳轉(zhuǎn)表和異常表不一樣。)
- 將系統(tǒng)調(diào)用和與它們相關(guān)聯(lián)的包裝函數(shù)都稱為系統(tǒng)級函數(shù)。
- 所有到Linux系統(tǒng)調(diào)用的參數(shù)都是通過通用寄存器而不是棧傳遞的。寄存器號%rax包含系統(tǒng)調(diào)用號,%rdi、%rsi、%rdx、%r10、%r8、%r9包含最多6個參數(shù)。從系統(tǒng)調(diào)用返回時,寄存器號%rcx和%r11都會被破壞,%rax包含返回值。
2. 進程 process
上下文(context):程序正確運行所需的狀態(tài)的集合。包括存放在內(nèi)存中的程序的代碼和數(shù)據(jù),它的棧、通用目的寄存器的內(nèi)容、程序計數(shù)器、環(huán)境變量以及打開文件描述符的集合。
進程是一個正在運行的程序的實例,系統(tǒng)中的每個程序都運行在某個進程的上下文中。
進程提供給應(yīng)用程序的關(guān)鍵抽象:
- 邏輯控制流(Logical control flow):它提供一個假象,好像我們的程序獨占地使用處理器。由OS內(nèi)核通過上下文切換機制實現(xiàn)。
- 私有的地址空間(Private address space):它提供一個假象,好像我們的程序獨占地使用內(nèi)存系統(tǒng)。由OS內(nèi)核的虛擬內(nèi)存機制實現(xiàn)。
2.1 邏輯控制流
邏輯控制流:PC值的序列,對應(yīng)于該程序所要執(zhí)行的那些指令。
多個進程輪流使用處理器。每個進程執(zhí)行它的流的一部分,然后被搶占(preempted)(暫時掛起),然后輪到其他進程。對于一個運行在這些進程之一的上下文中的程序,它看上去就像是在獨占地使用處理器。
2.2 并發(fā)流
(1)并發(fā)流和多任務(wù)的概念
一個邏輯流的執(zhí)行在時間上與另一個流重疊,稱為并發(fā)流(concurrent flow)。如上圖8-12,進程A和B,A和C的運行時間有重疊,屬于并發(fā)流;而進程C在B結(jié)束后才開始,二者不重疊,是順序的。
一個進程和其他進程輪流運行的概念稱為多任務(wù)(multitasking)。一個進程執(zhí)行它的控制流的一部分的每一時間段叫做時間片(time slice)。單處理器在并發(fā)地執(zhí)行多個進程,地址空間由虛擬內(nèi)存系統(tǒng)管理,未執(zhí)行進程的寄存器值保存在內(nèi)存中。
(2)多任務(wù)執(zhí)行過程
執(zhí)行當前進程 -> 將寄存器的值保存到內(nèi)存 -> 轉(zhuǎn)向下一個進程,進行上下文切換(裝載保存的寄存器等信息、切換地址空間) -> 將控制傳遞給新的進程
(3)多核處理器
而對于多核處理器:單個芯片有多個CPU,共享主存,有的還共享cache,每個核可以執(zhí)行獨立的進程,kernel負責處理器的內(nèi)核調(diào)度。
2.3 私有地址空間
進程為每個程序提供它自己的私有地址空間。一般而言,這個空間中的內(nèi)存字節(jié)是不能被其他進程讀或者寫的。
通用結(jié)構(gòu):
-
地址空間底部是保留給用戶程序的,包括通常的代碼、數(shù)據(jù)、堆和棧段。代碼段總是從地址0x400000開始。
-
地址空間頂部保留給內(nèi)核(操作系統(tǒng)常駐內(nèi)存的部分)。地址空間的這個部分包含內(nèi)核在代表進程執(zhí)行指令時(比如當應(yīng)用程序執(zhí)行系統(tǒng)調(diào)用時)使用的代碼、數(shù)據(jù)和棧。
2.4 用戶模式和內(nèi)核模式
用以限制一個應(yīng)用可以執(zhí)行的指令以及它可以訪問的地址空間范圍。通常是用某個控制寄存器中的一個模式位(mode bit)來標記當前進程所享有的特權(quán)。
- 設(shè)置模式位,進程處在內(nèi)核模式,可以執(zhí)行指令集中的任何指令、訪問系統(tǒng)中的任何內(nèi)存位置。
- 沒有設(shè)置模式位,進程運行在用戶模式。不允許執(zhí)行特權(quán)指令(停止處理器、改變模式位,發(fā)起一個I/O操作);不允許直接引用地址空間中內(nèi)核區(qū)內(nèi)的代碼和數(shù)據(jù),必須通過系統(tǒng)調(diào)用接口間接地訪問內(nèi)核代碼和數(shù)據(jù)。
2.5 上下文切換
進程由常駐內(nèi)存的操作系統(tǒng)代碼塊(內(nèi)核)管理。內(nèi)核不是一個單獨的進程,而是作為現(xiàn)有進程的一部分運行。通過上下文切換,控制流通從一個進程傳遞到另個進程,實現(xiàn)多任務(wù)。
3. 系統(tǒng)調(diào)用錯誤的處理
當Linuⅸ系統(tǒng)級函數(shù)遇到錯誤時,通常返回-1并設(shè)置全局整數(shù)變量errno來標示出錯原因。
硬性規(guī)定:
- 必須檢查每個系統(tǒng)級函數(shù)的返回狀態(tài)。
- 只有少數(shù)是返回空的函數(shù)。
strerror函數(shù)返回一個文本串,描述了和某個errno值相關(guān)聯(lián)的錯誤。
4. 進程控制
4.1 獲取進程ID
每個進程都有一個唯一的正數(shù)(非零)進程ID(PID)。
- getpid函數(shù)返回調(diào)用進程的PID。
- getppid函數(shù)返回它的父進程的PID(創(chuàng)建調(diào)用進程的進程)。
4.2 創(chuàng)建和終止進程
從程序員的角度,我們可以認為進程總是處于下面三種狀態(tài)之一:
- 運行。進程要么在CPU上執(zhí)行,要么在等待被執(zhí)行且最終會被內(nèi)核調(diào)度(選中并執(zhí)行)。
- 停止。進程的執(zhí)行被掛起(suspended),且不會被調(diào)度,直到收到一個SIGCONT通知信號。
- 終止。進程永遠地停止了。
(1)終止進程
進程會因為三種原因終止:
- 收到一個信號,該信號的默認行為是終止進程。
- 從主程序返回。
- 調(diào)用exit函數(shù)。
程序運行過程中,exit函數(shù)只能被調(diào)用一次,且不返回到調(diào)用函數(shù)中。
(2)創(chuàng)建進程
父進程通過調(diào)用fork
函數(shù)創(chuàng)建一個新的運行的子進程。
int fork(void)
- 子進程返回0,父進程返回子進程的PID,出錯則返回-1。(調(diào)用一次,返回兩次)
- 新創(chuàng)建的子進程幾乎但不完全與父進程相同:
- 子進程得到與父進程用戶級虛擬地址空間相同的(但是獨立的)一份副本
- 子進程獲得與父進程任何打開文件描述符相同的副本:意味著子進程可以讀寫父進程中打開的任何文件。
- 子進程有不同于父進程的PID。
父進程和子進程是并發(fā)運行的獨立進程。內(nèi)核能夠以任意方式交替執(zhí)行它們的邏輯控制流中的指令,不能預(yù)知執(zhí)行順序。
(3)用進程圖刻畫fork
進程圖:
- 每個頂點對應(yīng)一條語句的執(zhí)行。
- 有向邊a->b表示語句a發(fā)生在語句b之前。
- 邊上可以標記信息,如變量的當前值。
- printf語句的頂點可以標記上printf的輸出。
- 每張圖從一個沒有入邊的頂點開始。
- 圖的任何拓撲排序?qū)?yīng)于程序中語句的一個可行的全序排列(每條邊都是從左到右的)
4.3 回收子進程
當進程終止時,進程被保持在一種已終止的狀態(tài)中,直到被它的父進程回收(reaped),仍然消耗系統(tǒng)資源。終止但未被回收的進程稱為僵死進程(zombie)。
回收
- 父進程執(zhí)行回收(使用函數(shù)wait或waitpid)。
- 父進程收到子進程的退出狀態(tài)。
- 內(nèi)核刪掉僵死子進程。
父進程不回收子進程的后果
如果父進程沒有回收它的僵死子進程就終止了,內(nèi)核安排init進程去回收它們(init進程PID為1,系統(tǒng)啟動時創(chuàng)建,不會終止,是所有進程的祖先)。
- 若子進程已結(jié)束,父進程卡死,則無人回收僵死子進程;
- 若父進程已結(jié)束,而子進程卡死,則會一直卡著。
(1)waitpid
pid_t waitpid(pid_t pid, int *statusp, int options);
默認情況下(當options=0時),waitpid掛起調(diào)用進程的執(zhí)行,直到它的等待集合(wait set)中的一個子進程終止。如果等待集合中的一個進程在剛調(diào)用的時刻就已經(jīng)終止了,那么waitpid就立即返回已終止子進程的PID。此時,已終止的子進程已經(jīng)被回收,內(nèi)核會從系統(tǒng)中刪除掉它的所有痕跡。
(int* statusp指的是要存放返回狀態(tài)變量的地址,如child status!=NULL,則在該指針指向的整型量表明子進程終止原因和退出狀態(tài)信息)
由參數(shù)pid來確定等待集合的成員:
- pid>0,那么等待集合就是一個單獨的子進程,它的進程ID等于pid。
- pid=-1,那么等待集合就是由父進程所有的子進程組成的。
- pid< -1,等待ID為|pid|的進程組的任何子進程。
- pid=O時,等待同一個進程組中的任何子進程。
由參數(shù)options修改默認行為:
- WNOHANG:不掛起,立即返回,若無子進程終止返回0值。
- WUNTRACED:掛起,直到等待集合中的一個進程終止或停止。
- WCONTINUED:掛起,直到等待集合中的一個進程終止或收到SIGCONT而從停止狀態(tài)重新開始。
用wait.h頭文件中定義的宏函數(shù)來檢查已回收子進程的退出狀態(tài):
-
WIFEXITED(status):如果子進程通過調(diào)用exit或者一個返回(return)正常終止,就返回真。
-
WEXITSTATUS(status):返回一個正常終止的子進程的退出狀態(tài)。
-
WIFSIGNALED (status):如果子進程是因為一個未被捕獲的信號終止的,那么就返回真。
-
WTERMSIG(status):返回導致子進程終止的信號的編號。
-
WIFSTOPPED(status):如果引起返回的子進程當前是停止的,那么就返回真。
-
WSTOPSIG(status):返回引起子進程停止的信號的編號。
-
WIFCONTINUED(status):如果子進程收到SIGCONT信號重新啟動,返回真。
子進程結(jié)束的順序是任意的(沒有固定的順序),可用宏函數(shù)WIFEXITED和WEXITSTATUS獲取進程的退出狀態(tài)信息。
(2)wait
int wait(int *statusp)
調(diào)用wait(&status)等價于調(diào)用waitpid(-1, &status, 0)。 (等待所有子進程結(jié)束)
4.4 加載并運行程序
int execve(char *filename, char *argv [], char *envp[])
- filename:可執(zhí)行文件(目標文件或腳本,用#!指明解釋器,如#!/bin/bash)。
- argy:參數(shù)列表。慣例:argv[0]==filename。
- envp:環(huán)境變量列表
- "name=value"strings (e.g.,USER=droh)
- getenv, putenv, printenv
在當前進程中載入并運行程序,覆蓋當前進程的代碼、數(shù)據(jù)、棧,有相同的PID,繼承已打開的文件描述符和信號上下文。
調(diào)用一次,從不返回(除非有錯誤,指定的文件不存在返回-1)。
參數(shù)列表如下所示。argv變量指向一個以null結(jié)尾的指針數(shù)組,其中每個指針都指向一個參數(shù)字符串。按照慣例,argv[0]是可執(zhí)行目標文件的名字。
環(huán)境變量的列表結(jié)構(gòu)如下所示。envp變量指向一個以ul結(jié)尾的指針數(shù)組,其中每個指針指向一個環(huán)境變量字符串,每個串都是形如“name-value”的名字-值對。
新程序?qū)采w當前進程的代碼、數(shù)據(jù)、棧。
NULL作為分隔標識符。
5. Shell程序
shell是一個交互型應(yīng)用級程序,代表用戶運行其他程序。
Shell操作模擬:
6. 信號signal
6.1 定義
signal就是一條小消息,它通知用戶進程系統(tǒng)中發(fā)生了一個某種類型的系統(tǒng)事件。
- 類似于異常和中斷
- 從內(nèi)核發(fā)送到(有時是在另一個進程的請求下)一個進程
- 信號類型是用小整數(shù)ID來標識的(1-30)
- 信號中唯一的信息是它的ID和它的到達
例如:
特別注意:
- SIGKILL、SIGTOP不可忽略、阻塞、捕獲:這兩個信號為root用戶、kernel在任意情況下kill或stop任何進程提供了一種途徑,默認行為分別是終止和停止。
- 信號值小于SIGRTMIN(34)的信號都是不可靠信號。信號值位于SIGRTMIN和SIGRTMAX之間是實時信號,都是可靠信號,支持排隊,不會丟失。本課程只介紹不可靠信號。
6.2 信號術(shù)語
**(1)發(fā)送信號:**內(nèi)核通過更新目的進程上下文中的某個狀態(tài)項(struct sigpending pending),來實現(xiàn)發(fā)送(遞送)一個信號給目的進程。
發(fā)送信號的原因:
- 內(nèi)核檢測到一個系統(tǒng)事件,如除零錯誤(SIGFPE)或者子進程終止(SIGCHLD)。
- 一個進程使用系統(tǒng)調(diào)用函數(shù)kill,顯式地/直接請求內(nèi)核發(fā)送一個信號到目的進程。
- 一個進程可以給自己發(fā)信號
**(2)接收信號:**目的進程被內(nèi)核以某種方式對發(fā)送來的信號做出反應(yīng)時,它就接收了信號。
反應(yīng)的方式:
- 忽略這個信號(do nothing)
- 終止進程(with optional core dump)
- 通過執(zhí)行一個稱為信號處理程序(signal handler)的用戶層函數(shù)捕獲這個信號,類似于響應(yīng)異步中斷而調(diào)用的硬件異常處理程序。
**(3)待處理信號:**一個發(fā)出而沒有被接收的信號叫做待處理信號,也稱:未決信號。存放在pending里。
- 非實時/不可靠信號的任一類型(1-31/SIGRTMIN-1)最多有一個待處理信號。
- 非實時信號不會排隊等待。如果一個進程有一個類型為k的待處理信號,那么任何接下來發(fā)送到這個進程的類型為k的信號都會被丟棄。
- 一個待處理信號最多只能被接收一次。只要傳送了一個類型為k的信號,內(nèi)核就會設(shè)置pending中的第k位,而只要接收了一個類型為k的信號,內(nèi)核就會清除pending中的第k位。
**(4)阻塞信號:**一個進程可以選擇阻塞接收某種信號,將blocked對應(yīng)bit位置為1。阻塞的信號仍可以被發(fā)送,但不會被接收,直到進程取消對該信號的阻塞。
6.3 信號在內(nèi)核中的表示
進程控制塊:內(nèi)核為每個進程維護著待處理位向量(pending)、阻塞位向量(blocked)。
pending:待處理信號集合,也稱未決信號集合
- 若傳送了一個類型為k的信號,內(nèi)核會設(shè)置pending中的第k位為1(注冊);
- 若接收了(開始處理)一個類型為k的信號,內(nèi)核將立即清除pending中的第k位。
blocked:被阻塞信號的集合
- 通過sigprocmask函數(shù)設(shè)置和清除;
- 也稱信號掩碼mask。
6.4 進程組
Unix shell使用作業(yè)(job)這個抽象概念來表示為對一條命令行求值而創(chuàng)建的進程。在任何時刻,至多只有一個前臺作業(yè)和0個或多個后臺作業(yè)。
每個進程只屬于一個進程組,用pgid表示組號。
6.5 發(fā)送信號
(1)用/bin/kill程序發(fā)送信號
/bin/kill程序可以向另外的進程或進程組發(fā)送任意的信號。
Examples:
- /bin/kill -9 24818:發(fā)送信號9(SIGKILL)給進程24818
- /bin/kill -9 -24817:發(fā)送信號SIGKILL給進程組24817中的每個進程(負的PID會導致信號被發(fā)送到進程組PID中的每個進程)
(注意,在此我們使用完整路徑/ bin/ki1l,因為有些Unix shell有自己內(nèi)置的ki11命令。)
(2)從鍵盤發(fā)送信號
ctrl-c:內(nèi)核發(fā)送一個SIGINT信號到前臺進程組中的每個進程。默認情況下,結(jié)果是終止前臺作業(yè)。
ctrl-z:內(nèi)核發(fā)送一個SIGTSTP信號到前臺進程組中的每個進程。默認情況下,結(jié)果是停止(掛起)前臺作業(yè)。
(3)用kill函數(shù)發(fā)信號
int kill(pid_t pid, int sig);
- 功能:將信號號碼sig指定的信號發(fā)送給參數(shù)pid指定的進程;
- 參數(shù)pid:
- pid>0 將信號發(fā)給id為pid的進程。
- pd=0 將信號發(fā)給和當前進程在同一進程組的所有進程,包括調(diào)用進程自己。。
- pid=-1 將信號廣播發(fā)送給系統(tǒng)內(nèi)所有的進程。
- pid<0 將信號發(fā)送給進程組id為pid絕對值的所有進程。
- 參數(shù)sig最好使用信號名(宏定義),而非編碼值,便于代碼移植
- 返回值:執(zhí)行成功則返回0,如果有錯誤則返回-1。
6.6 接收信號
(1)接收時機與過程
假設(shè)內(nèi)核正在從異常處理程序返回,并準備將控制權(quán)傳遞給進程p時:
- 內(nèi)核計算進程p的未被阻塞的待處理信號的集合pnb=pending& ~blocked
- if(pnb==O),即集合pnb為空,則將控制傳遞到的邏輯控制流中的下一條指令;
- else,即pnb不為空
- 選擇集合pnb中最小的非零位k(優(yōu)先級最高),強制p接收信號k,觸發(fā)進程p采取某種行為;
- 對所有的非零k,重復(fù)上述操作;
- 控制傳遞到p的邏輯控制流中的下一條指令;
(2)接收行為
-
默認行為:每個信號類型都有一個預(yù)定義默認行為,是下面中的一種:
- 進程終止
- 進程停止(掛起)直到被SIGCONT信號重啟
- 進程忽略該信號
-
指定行為:調(diào)用并執(zhí)行預(yù)先設(shè)置好的信號處理程序。
(3)修改默認接收行為:設(shè)置信號處理程序
使用signal函數(shù)設(shè)置信號signum的處理函數(shù)或恢復(fù)默認函數(shù)/行為:
sighandler_t signal(int signum, sighandler_t handler)
-
sighandler_t:
typedef void (*sighandler_t)(int)
-
handler的不同取值:
- SIG_IGN:忽略類型為signum的信號
- SIG_DFL:類型為signum的信號行為恢復(fù)為默認行為
- handler:用戶自定義函數(shù)的地址,這個函數(shù)稱為信號處理程序。
-
返回值
- 設(shè)置成功,返回原處理函數(shù)指針
- 設(shè)置失敗,返回SIG_ERR #define SIG_ERR(void (*)0)-1
7. 信號處理
7.1 信號處理流程
- 只要進程接收到類型為sig的信號就會“自動”調(diào)用信號處理程序。
- 設(shè)置信號處理程序:使用signal函數(shù),設(shè)定指定信號的處理程序(地址),從而改變默認行為。
- 捕獲/處理信號:調(diào)用(執(zhí)行)信號處理程序。
- 當信號處理程序執(zhí)行return時,控制會傳遞到控制流中被信號接收所中斷的指令處。
7.2 用信號處理程序捕獲SIGINT信號
它捕獲用戶在鍵盤上輸人Ctrl+C時發(fā)送的SIGINT信號。SIGINT的默認行為是立即終止該進程。在這個示例中,我們將默認行為修改為捕獲信號,輸出一條消息,然后終止該進程。
7.3 作為并發(fā)流的信號處理程序
信號處理程序是與主程序同時運行、獨立的邏輯流(不是進程),并發(fā)運行。
7.3 嵌套的信號處理程序
- 信號處理程序可以被其他信號處理程序中斷
7.4 阻塞和解除阻塞信號
(1)隱式阻塞機制
內(nèi)核默認阻塞與當前正在處理信號類型相同的待處理信號。例如,一個SIGINT信號處理程序不能被另一個SIGINT信號中斷(此時另一個SIGINT信號被阻塞)。
(2)顯示阻塞和解除阻塞機制
sigprocmask
函數(shù)及其輔助函數(shù)可以明確地阻塞/解除阻塞選定的信號。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
作用:改變當前阻塞的信號集合(blocked位向量)
how的值決定行為:
SIG_BLOCK:把set中的信號添加到blocked中(blocked=blocked|set)。
SIG_UNBLOCK:從blocked中刪除set中的信號(blocked=blocked &~set)。
SIG_SETMASK:block=set。
如果oldset非空,那么blocked位向量之前的值保存在oldset中。
輔助函數(shù)(信號集合操作):
sigemptyset:初始化set為空集合。
sigfillset:把每個信號都添加到set中。
sigaddset:把指定的信號signum添加到set。
sigdelset:從set中刪除指定的信號signum。
(3)臨時阻塞接收信號
可以通過sigprocmask來臨時阻塞接收SIGINT信號。
- 使用mask作為位向量掩碼。
- 先將需要阻塞的信號add進這個set。
- 然后對進程的blocked進行設(shè)置,并使用prev_mask保存之前的掩碼。
- 在臨時阻塞某些信號的情況下進行操作。
- 再將原先的掩碼情況重新恢復(fù),恢復(fù)阻塞。
7.5 編寫信號處理程序
信號處理程序很麻煩,因為:
- 處理程序與主程序并發(fā)運行,共享同樣的全局變量,因此可能與主程序和其他處理程序互相干擾;
- 如何以及何時接收信號的規(guī)則常常有違人的直覺;
- 不同的系統(tǒng)有不同的信號處理語義。
給出編寫安全、正確和可移植的信號處理程序的一些基本規(guī)則。
(1)安全的信號處理
異步信號安全的函數(shù):函數(shù)要么是可重入的(不引用任何共享數(shù)據(jù),如只訪問局部變量),要么不能被信號處理程序中斷。
(2)正確的信號處理
**未處理的信號是不排隊的:**pending位向量每種類型最多只能有一個未處理的信號。如果兩個類型k的信號發(fā)送給一個目的進程,而因為目的進程當前正在執(zhí)行信號k的處理程序,所以信號被阻塞了,那么第二個信號就簡單地被丟棄了;
不能以信號出現(xiàn)的次數(shù)來對其他進程中發(fā)生的事件計數(shù),如子程序的終止
在fork14函數(shù)中,父進程使用for循環(huán)創(chuàng)建N個子進程,用ccount記數(shù)作為父進程結(jié)束的條件。對于每個子進程來說,在被創(chuàng)建后sleep1秒后即退出,然后內(nèi)核給父進程發(fā)送一個SIGCHILD信號,父進程調(diào)用指定的child_handler處理程序。在每個處理程序中進行回收子進程的操作,回收后則ccount–。當所有子進程回收完畢后ccount=0,父進程結(jié)束。
而這段程序的問題在于:父進程的for循環(huán)中,每個if在父進程中都不會執(zhí)行,也即不會睡眠1s,導致5個子進程幾乎同時被創(chuàng)建,而最終同時退出。故會產(chǎn)生,同時向父進程發(fā)送5個SIGCHILD信號,而child_handler中睡眠1s,處理較慢。當接收第一個信號時,pending對應(yīng)位置1,然后立馬開始處理,再置0,然后第2個SIGCHILD信號進入待處理,將pending置1。后續(xù)的3個信號由于位置被占,會被丟棄。
故這段程序最終會產(chǎn)生5個子進程,而僅回收2個,導致ccount=3,卡死。
針對于該程序的一種解決方法:將wait放入while循環(huán),從而回收所有終止的子進程。
(3)可移植的信號處理
不同的Unix版本有不同的信號處理語義,解決辦法:sigaction函數(shù),可明確指定信號處理語義。
7.6 同步流以避免并發(fā)錯誤(競爭)
如何編寫讀寫相同存儲位置的并發(fā)流程序的問題
對于父進程的main程序和信號處理流的某些交錯,可能會在addjob之前調(diào)用de1 etejob。這導致作業(yè)列表中出現(xiàn)一個不正確的條目,對應(yīng)于一個不再存在而且永遠也不會被刪除的作業(yè)。
一種解決方法消除競爭,避免并發(fā)錯誤:
在調(diào)用fork之前,阻塞SIGCHLD信號,然后在調(diào)用addjob之后取消阻塞這些信號,我們保證了在子進程被添加到作業(yè)列表中之后回收該子進程。注意,子進程繼承了它們父進程的被阻塞集合,所以我們必須在調(diào)用execve之前,小心地解除子進程中阻塞的SIGCHLD信號。
7.7 等待信號
(1)顯式地等待信號
先阻塞SIGCHILD信號,置一個子進程結(jié)束的標記為(此例中pid)不去回收子進程,然后接觸阻塞,一直去判斷這個標記位,直到子進程被回收后再繼續(xù)父進程的操作。
(2)用sigsuspend等待信號
int sigsuspend(const sigset_t *mask);
- 設(shè)定臨時的信號屏蔽字為mask
- 等待信號
- 收到信號后自動恢復(fù)原信號屏蔽字,函數(shù)返回
沒看懂
8. 非本地跳轉(zhuǎn)
它將控制直接從一個函數(shù)轉(zhuǎn)移到另一個當前正在執(zhí)行的函數(shù),而不需要經(jīng)過正常的調(diào)用-返回序列。強大的(但危險的)用戶級機制。
8.1 實現(xiàn)機制
非本地跳轉(zhuǎn)是通過setjmp和longjmp函數(shù)來提供的。
int setjmp(jmp_buf envbuf)
- 必須在longjmp之前被調(diào)用
- 在envbuf中保存當前調(diào)用環(huán)境(包括寄存器、棧指針和程序計數(shù)器),供后續(xù)longjmp使用
- 被調(diào)用一次,返回多次
- 返回0
- 返回值不能給變量賦值:rc=setjmp(env);//Error,但可以用在switch里
void longjmp(jmp_buf envbuf, int iRet)
- 從envbuf中恢復(fù)調(diào)用環(huán)境,并從對應(yīng)的setjmp返回(跳轉(zhuǎn)至保存在envbuf中的PC所指示的位置)
- 返回數(shù)值iRet,而非0,存放在%eax中
- 在setjmp之后被調(diào)用
- 被調(diào)用一次,從不返回
8.2 示例
從深層嵌套函數(shù)調(diào)用中直接返回:
main函數(shù)首先調(diào)用setjmp以保存當前的調(diào)用環(huán)境,然后調(diào)用函數(shù)foo,foo依次調(diào)用函數(shù)bar。如果foo或者bar遇到一個錯誤,它們立即通過一次longjmp調(diào)用從setjmp返回。setjmp的非零返回值指明了錯誤類型,隨后可以被解碼,且在代碼中的某個位置進行處理。
8.3 非本地跳轉(zhuǎn)的局限
工作在堆棧規(guī)則下:只能跳到被調(diào)用但尚未完成的函數(shù)環(huán)境中。同時會清空中間過程的棧幀(丟失)。
若在A設(shè)置跳轉(zhuǎn)點,A調(diào)用B,B調(diào)用C,在C中跳到A,沒問題。
若在B設(shè)置跳轉(zhuǎn)點,A調(diào)用B,然后A調(diào)用C,但在C中跳轉(zhuǎn)回B,則出問題。(B已經(jīng)結(jié)束,B的棧幀被C覆蓋,所以跳轉(zhuǎn)點實際上指向的位置為位置地點,是原來B的那里)
8.4 用于信號處理程序的非本地跳轉(zhuǎn)函數(shù)
(可以選擇是否保存信號屏蔽字)文章來源:http://www.zghlxwxcb.cn/news/detail-472486.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-472486.html
到了這里,關(guān)于《深入理解計算機系統(tǒng)(CSAPP)》第8章 異常控制流 - 學習筆記的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!