[德] Michael Kerrisk
第6章 進(jìn)程
第24章 進(jìn)程的創(chuàng)建
第25章 進(jìn)程的終止
第26章 監(jiān)控子進(jìn)程
進(jìn)程
進(jìn)程和程序(Processes and Programs)
進(jìn)程是一個(gè)可執(zhí)行程序的實(shí)例(A process is an instance of an executing program).
程序(program)是包含了一系列信息的文件女嘲,這些信息描述了如何在運(yùn)行時(shí)創(chuàng)建一個(gè)進(jìn)程,所包含的信息有:
- 二進(jìn)制格式標(biāo)識(shí)(Binary format identification): 每個(gè)程序文件都包含用于描述可執(zhí)行文件格式的元信息(metainformation)。內(nèi)核利用此信息來(lái)解釋文件中的其他信息。大多數(shù)UNIX(包括Linux)采用Executable
and Linking Format (ELF). - 機(jī)器語(yǔ)言指令(Machine-language instructions): 對(duì)程序算法進(jìn)行編碼
- 程序入口地址(Program entry-point address):標(biāo)識(shí)程序開(kāi)始執(zhí)行時(shí)起始指令位置
- 數(shù)據(jù)(Data): 變量初始值和程序使用的字面常量(literal constant)
- 符號(hào)表及重定位表(Symbol and relocation tables): 描述程序中函數(shù)和變量的位置及名稱(chēng)蜕提。
- 共享庫(kù)和動(dòng)態(tài)鏈接信息(Shared-library and dynamic-linking information): 程序運(yùn)行需要的共享庫(kù)霎匈,以及加載共享庫(kù)的動(dòng)態(tài)鏈接器的路徑名
- 其他信息: 描述如何創(chuàng)建進(jìn)程
可以用一個(gè)程序創(chuàng)建多個(gè)進(jìn)程茴迁。
進(jìn)程是由內(nèi)核定義的抽象的實(shí)體夸盟,并為該實(shí)體分配用以執(zhí)行程序的各項(xiàng)系統(tǒng)資源秽荞。
進(jìn)程號(hào)和父進(jìn)程號(hào)
進(jìn)程號(hào)(PID)骤公,是用來(lái)唯一標(biāo)識(shí)系統(tǒng)中某個(gè)進(jìn)程的一個(gè)整數(shù)。對(duì)系統(tǒng)調(diào)用來(lái)說(shuō)扬跋,進(jìn)程號(hào)可以作為參數(shù)傳入阶捆,kill()系統(tǒng)調(diào)用;也可以作為返回值,比如getpid()系統(tǒng)調(diào)用洒试。
Linux內(nèi)核限制進(jìn)程號(hào)需要<=32767, 可以調(diào)整倍奢。
$ cat /proc/sys/kernel/pid_max
32768
每個(gè)進(jìn)程都有一個(gè)創(chuàng)建自己的父進(jìn)程,使用系統(tǒng)調(diào)用getppid()獲取父進(jìn)程的進(jìn)程號(hào)垒棋。
使用pstree
命令可以查看進(jìn)程樹(shù)卒煞。
進(jìn)程內(nèi)存布局(Memory Layout of a Process)
每個(gè)進(jìn)程所分配的內(nèi)存由很多部分組成,通常稱(chēng)為“段(segments)”叼架,或者“區(qū)(section)”:
- 文本段(text segment): 包含進(jìn)程運(yùn)行的機(jī)器語(yǔ)言指令畔裕。文本段具有只讀屬性,同時(shí)可共享乖订,使多個(gè)進(jìn)程使用同一份程序代碼拷貝柴钻。
- 初始化的數(shù)據(jù)段(initialized data segment): 包含顯示初始化的全局變量和靜態(tài)變量。
- 未初始化數(shù)據(jù)段(uninitialized data segment):也被稱(chēng)為BSS(block started by symbol)垢粮。 包含未進(jìn)行顯式初始化的全局變量和靜態(tài)變量贴届。程序啟動(dòng)之前系統(tǒng)將本段內(nèi)所有內(nèi)存初始化為0。
- 棧(stack): 動(dòng)態(tài)變化的segment蜡吧,由棧幀(stack frames)組成毫蚓。系統(tǒng)會(huì)為每個(gè)當(dāng)前調(diào)用的函數(shù)分配一個(gè)棧幀,其中存儲(chǔ)函數(shù)的局部變量昔善,實(shí)參和返回值元潘。
- 堆(heap): 在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的區(qū)域。堆頂端被稱(chēng)為program break君仆。
size
命令可顯示文本段翩概,初始化和未初始化數(shù)據(jù)段(bss)的大小
> $ man size
NAME
size - list section sizes and total size.
...
> $ size hello
text data bss dec hex filename
1230 548 4 1782 6f6 hello
虛擬內(nèi)存管理(Virtual Memory Management)
進(jìn)程的內(nèi)存布局存在與虛擬內(nèi)存(Virtual Memory)中。虛擬內(nèi)存的規(guī)劃之一就是將每個(gè)程序使用的內(nèi)存切割成小型的返咱、固定大小的“頁(yè)”(page)單元钥庇。相應(yīng)地,將RAM換成一系列與“頁(yè)”大小相同的頁(yè)幀咖摹。
為支持這一組織形式评姨,內(nèi)核為每個(gè)進(jìn)程維護(hù)一張頁(yè)表(page table),用于記錄每頁(yè)在進(jìn)程虛擬地址空間的位置萤晴。
虛擬內(nèi)存管理使進(jìn)程的虛擬地址空間與RAM物理地址空間隔離開(kāi)來(lái)吐句。
棧和棧幀(the stack and stack frame)
函數(shù)的調(diào)用和返回使棧的增長(zhǎng)和收縮呈線(xiàn)性。棧駐留在內(nèi)存的高端比你高向下增長(zhǎng)(朝堆的方向)店读。專(zhuān)用寄存器--棧指針(stack pointer)嗦枢,用于跟蹤當(dāng)前棧頂。每次調(diào)用函數(shù)和返回函數(shù)時(shí)屯断,都是在棧上新增和移去棧幀文虏。
棧幀(user stack), 一般指用戶(hù)棧侣诺,區(qū)分與內(nèi)核棧,包含一下信息:
- 函數(shù)實(shí)參和局部變量
- (函數(shù))調(diào)用的鏈接信息:每個(gè)函數(shù)都會(huì)用到一些CPU寄存器择葡,比如程序計(jì)數(shù)器紧武。比如調(diào)用另一個(gè)函數(shù)時(shí)剃氧,保存當(dāng)前寄存器狀態(tài)敏储,以便返回時(shí)恢復(fù)。
命令行參數(shù)(command-line argument)朋鞍,argc, argv
- int argc:命令行參數(shù)的個(gè)數(shù)
- char *argv[]: 指向命令行參數(shù)的指針數(shù)組已添,每一參數(shù)都是以空字符('\0')結(jié)尾的字符串.
程序可以通過(guò)/proc/PID/cmdline文件訪(fǎng)問(wèn)任一進(jìn)程的命令行參數(shù),每個(gè)參數(shù)都以空(NULL)字節(jié)終止滥酥。
argv和environ(環(huán)境變量)數(shù)組更舞,以及這些參數(shù)最初只想的字符串,都主流在進(jìn)程棧上的一個(gè)單一坎吻、連續(xù)的內(nèi)存區(qū)域缆蝉。
進(jìn)程的創(chuàng)建
創(chuàng)建新進(jìn)程: fork()系統(tǒng)調(diào)用
fork()創(chuàng)建一個(gè)新進(jìn)程(child),幾近于對(duì)調(diào)用進(jìn)程(parent)的翻版
#include <unistd.h>
pid_t fork(void);
In parent: returns process ID of child on success, or –1 on error;
in successfully created child: always returns 0
完成對(duì)其調(diào)用后將存在兩個(gè)進(jìn)程瘦真,每個(gè)進(jìn)程都會(huì)從fork()的返回處繼續(xù)執(zhí)行刊头,程序代碼可通過(guò)fork()的返回值來(lái)區(qū)分父子進(jìn)程。在父進(jìn)程中诸尽,fork()將返回新創(chuàng)建子進(jìn)程的進(jìn)程ID原杂,在子進(jìn)程中則返回0。
子進(jìn)程也可調(diào)用getpid(), getppid()分別獲得自身進(jìn)程以及父進(jìn)程的ID您机。
執(zhí)行fork()時(shí)候穿肄,子進(jìn)程會(huì)獲得父進(jìn)程所有文件描述符的副本,也即父子進(jìn)程共享打開(kāi)的文件及其屬性际看。
從概念上講咸产,fork()認(rèn)作對(duì)父進(jìn)程程序代碼段,數(shù)據(jù)段仲闽,堆棧的拷貝锐朴,實(shí)際上,子進(jìn)程一般會(huì)替換代碼段蔼囊,并重新初始化數(shù)據(jù)焚志,堆棧,全拷貝就造成了浪費(fèi)畏鼓。因此UNIX采用兩種技術(shù)來(lái)避免這種浪費(fèi):
- 內(nèi)核將每一進(jìn)程的代碼段標(biāo)記為只讀酱酬,父子進(jìn)程共享該代碼段。
- 對(duì)于數(shù)據(jù)段云矫,堆棧中各頁(yè)膳沽,內(nèi)核采用寫(xiě)時(shí)復(fù)制技術(shù)(copy-on-write)。即內(nèi)核會(huì)捕捉進(jìn)程中針對(duì)頁(yè)的修改企圖,并為將要修改的頁(yè)創(chuàng)建拷貝挑社。
fork()之后的競(jìng)爭(zhēng)條件(race condition)
競(jìng)爭(zhēng)表現(xiàn)在調(diào)用fork()后陨界,無(wú)法確定父、子進(jìn)程誰(shuí)將率先訪(fǎng)問(wèn)CPU痛阻。Linux在版本升級(jí)中菌瘪,多次調(diào)整默認(rèn)優(yōu)先的進(jìn)程。
由于會(huì)產(chǎn)生所謂“競(jìng)爭(zhēng)條件”的錯(cuò)誤阱当,不應(yīng)對(duì)fork()之后執(zhí)行父俏扩、子進(jìn)程的特定順序做任何假設(shè)。如若需要保證執(zhí)行順序弊添,需要采用同步技術(shù)录淡,包括信號(hào)量(semaphore)、文件鎖(file lock)以及進(jìn)程間經(jīng)由管道(pipe)的消息發(fā)送油坝。
進(jìn)程的終止
_exit()和exit()
進(jìn)程可能通過(guò)兩種方式終止:
- 異常(abnormal)終止:接受到終止信號(hào)(signal)嫉戚,可能產(chǎn)生核心轉(zhuǎn)儲(chǔ)(core dump)
- 進(jìn)程使用系統(tǒng)調(diào)用_exit()自主終止
#include <unistd.h>
void _exit(int status);
_exit()的status參數(shù)定義了進(jìn)程的終止?fàn)顟B(tài),父進(jìn)程可以調(diào)用wait()獲取該狀態(tài)澈圈。雖然定義為int類(lèi)型彬檀,但僅有低8位可以被父進(jìn)程使用。調(diào)用_exit()的程序總會(huì)成功終止极舔,即使從不返回凤覆。
一般使用庫(kù)函數(shù)exit()來(lái)終止進(jìn)程,它會(huì)在調(diào)用_exit()前執(zhí)行各種動(dòng)作:
- 調(diào)用退出處理程序(通過(guò)atexit()和on_exit()注冊(cè)的函數(shù))
- 刷新stdio流緩沖區(qū)
- 使用由status提供的值執(zhí)行_exit()系統(tǒng)調(diào)用
監(jiān)控子進(jìn)程
父進(jìn)程需要了解其某個(gè)子進(jìn)程何時(shí)改變了狀態(tài)拆魏,用于監(jiān)控子進(jìn)程有兩種方式:
- 系統(tǒng)調(diào)用wait()
- 信號(hào)SIGCHLD
等待子進(jìn)程
系統(tǒng)調(diào)用wait()
wait()等待進(jìn)程的任一子進(jìn)程終止盯桦,同時(shí)在參數(shù)status所指向的緩沖區(qū)中返回該子進(jìn)程的終止?fàn)顟B(tài)
#include <sys/wait.h>
pid_t wait(int *status);
Returns process ID of terminated child, or –1 on error
wait()執(zhí)行一下動(dòng)作:
- 如果調(diào)用之前還沒(méi)有子進(jìn)程終止,則一直阻塞渤刃,如果有拥峦,則立即返回
- 如果status非空,那么關(guān)于子進(jìn)程如何終止的信息則會(huì)通過(guò)status指向的整型變量返回
- 內(nèi)核將會(huì)為父進(jìn)程下所有子進(jìn)程的運(yùn)行總量追加進(jìn)程CPU時(shí)間以及資源使用數(shù)據(jù)
- 將終止子進(jìn)程的ID作為wait()的結(jié)果返回
系統(tǒng)調(diào)用waitpid()
wait()存在諸多限制卖子,而waitpid()則意在突破這些限制
- 無(wú)法指定某個(gè)特定的子進(jìn)程略号,只能按循序等待下一個(gè)子進(jìn)程終止
- 沒(méi)有進(jìn)程退出,則wait()總是阻塞
- wait()只能發(fā)現(xiàn)終止的子進(jìn)程洋闽,而對(duì)終止原因及恢復(fù)執(zhí)行情況無(wú)能為力
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
Returns process ID of child, 0 (see text), or –1 on error
等待狀態(tài)值
wait()和waitpid()返回的status值玄柠,可用來(lái)區(qū)分一下子進(jìn)程事件:
- 子進(jìn)程調(diào)用_exit()或exit()而終止,并指定一個(gè)整型值作為退出狀態(tài)
- 子進(jìn)程收到未處理信號(hào)終止
- 子進(jìn)程因?yàn)樾盘?hào)而暫停诫舅,并以WUNTRACED標(biāo)志調(diào)用waitpid()
- 子進(jìn)程因收到信號(hào)SIGCONT而恢復(fù)羽利,并以WCONTINUED標(biāo)志調(diào)用waitpid()
同時(shí)還有waitid(), wait3()和wait4()
孤兒進(jìn)程和僵尸進(jìn)程(Orphan and ZOmbie)
- 孤兒進(jìn)程:父進(jìn)程先于子進(jìn)程終止,此時(shí)init會(huì)接管該進(jìn)程刊懈,對(duì)getppid()的調(diào)用將返回1
- 僵尸進(jìn)程这弧,父進(jìn)程在wait()之前娃闲,子進(jìn)程就已終止,內(nèi)核會(huì)將子進(jìn)程轉(zhuǎn)為僵尸進(jìn)程匾浪,即釋放資源皇帮,但是保留該進(jìn)程在進(jìn)程表里的一條記錄,包含進(jìn)程id蛋辈,終止?fàn)顟B(tài)属拾,資源使用數(shù)據(jù)等信息。
父進(jìn)程應(yīng)執(zhí)行wait()方法梯浪,以確保系統(tǒng)中總是能夠清理那些死去的子進(jìn)程捌年。
SIGCHLD信號(hào)
無(wú)論一個(gè)子進(jìn)程何時(shí)終止瓢娜,系統(tǒng)都會(huì)向其父進(jìn)程發(fā)送SIGCHLD信號(hào)挂洛。
對(duì)該信號(hào)的默認(rèn)處理時(shí)將其忽略,可以通過(guò)設(shè)置信號(hào)處理程序signal()或sigaction()來(lái)捕獲眠砾,同時(shí)編寫(xiě)信號(hào)處理函數(shù)使用wait()來(lái)處理僵尸進(jìn)程虏劲。
原文鏈接
https://sun2y.me