當內核執行C程序時(使用一個exec函數),在調用main前先調用一個特殊的啓動例程。可執行程序文件將此啓動例程指定爲程序的起始地址—這是由鏈接編輯器設置的,而鏈接編輯器由C編譯器調用。啓動例程從內核取得命令行參數和環境變量值,而後按上述方式調用main函數作好安排。shell
5種正常終止 數組
3種異常終止緩存
無論進程如何終止,最後都會執行內核中的同一段代碼。這段代碼爲相應進程關閉全部打開描述符,釋放他所使用的存儲器等。
對上述任意一種終止情形,咱們都但願終止進程可以通知其父進程它是如何終止的。對於e x i t和_ e x i t,這是依靠傳遞給它們的退出狀態( exit status)參數來實現的。在異常終止狀況,內核(不是進程自己)產生一個指示其異常終止緣由的終止狀態( termination status) 。在任意一種狀況下,該終止進程的父進程都能用 w a i t或w a i t p i d函數(在下一節說明)取得其終止狀態。(退出狀態是傳給exit/_exit的參數,或main返回值。在最後調用_exit時內核將其退出狀態轉爲終止狀態,若是子進程正常終止那父進程能夠獲取子進程的退出狀態)。
在說明fork函數時,子進程是在父進程調用fork後生成的。子進程將其終止狀態返回給父進程。可是若是父進程在子進程以前終止,該如何?回答是:對於父進程已經終止的全部進程,他們的父進程都改變爲init進程。咱們稱這些進程由init進程收養。其操做過程大概是:在一個進程終止時,內核諸葛檢查全部活動進程,以判斷他是不是正要終止進程的子進程,若是是,則該進程的父進程的ID更改成1(init進程的ID)。這種處理方法保證了每個進程都有一個父進程。
若是子進程在父進程以前終止,那麼父進程如何在作相應檢查時獲得子進程的終止狀態呢?若是子進程徹底消失了,父進程在最終準備好檢查子進程是否終止時是沒法得到他的終止狀態的。內核爲每一個終止子進程保存了必定量的信息。因此當終止進程的父進程調用wait或waitpid時,能夠獲得這些信息。這些信息至少包括進程ID、該進程的終止狀態以及該進程使用的CPU時間總量。內核能夠釋放終止進程所使用的全部存儲區,關閉其全部打開文件。在UNIX術語中,一切已經終止、可是其父進程還沒有對其進行善後處理的進程被稱爲殭屍進程。==若是一個長期運行的程序,它fork了不少子進程,那麼除非父進程取得子進程的終止狀態,否則子這些進程終止後就會變成殭屍進程。 編輯器
一個由init進程收養的進程最終終止時會發生什麼?他會不會變成一個殭屍進程,不會的,由於init被編寫成不管什麼時候只要有一個子進程終止,init就會調用一個wait函數取得其終止狀態。這樣也就防止了在系統中塞滿了殭屍進程。當說起「一個init的子進程」時,這指的是init直接產生的進程,也多是其父進程已經終止,由init收養的過程。函數
啓動例程是這樣編寫的,使得從main返回後當即調用exit函數,若是將啓動歷程以C代碼形式表示(此例程一般用匯編編寫),則它調用main函數的形式多是優化
exit(main(argc, argv));
exit和_exit函數用於正常終止一個程序:_exit當即進入內核,exit則先執行一些清除處理(包括調用執行各終止處理程序,關閉全部標準IO流等),而後進入內核。spa
#include <stdlib.h> void exit(int status); #include <unistd.h> void _exit(int statu);
因爲歷史緣由,exit函數老是執行一個標準IO庫的清除關閉操做:對全部打開的流調用fclose函數,這會形成緩存中的全部數據都被刷新(寫入到文件上)。命令行
按照ANSI C的規定,一個進程能夠登記多至32個的終止處理函數(exit handler),這些函數將由exit自動調用。可用atexit函數來註冊這些函數。線程
#include <stdlib.h> int atexit(void (*func)(void)); //返回值:成功爲0,出錯非0
atexit的參數是一個無參數而且無返回的函數的指針。exit以註冊這些函數的相反順序調用它們。若是一個函數被註冊屢次那會被調用屢次。 根據ANSI C和POSIX.1 exit首先調用各終止處理程序,而後按需屢次調用fclose。下圖顯示了一個C程序是如何啓動的,以及它終止的各類方式。指針
內核使程序執行的惟一方式是調用一個exec函數。
每一個程序都接收到一張環境表。與參數表同樣,環境表也是一個字符指針數組,其中每一個指針包含一個以null結束的字符串的地址。全局變量environ則包含了該指針數組的地址。 extern char **environ;
若是該環境包含五個字符串,那麼它們看起來以下圖:
其中每一個字符串的結束處都有一個null字符。咱們稱environ爲環境指針。指針數組爲環境表,其中各指針指向的字符串爲環境字符串。一般使用getenv和putenv函數來存取特定的環境變量,而不是用environ變量。可是若是要查看整個環境則必須使用environ指針。
int maxcount = 99;
使此變量以初值存放在初始化數據段中。long sum[1000]
使此變量存放在非初始化數據段中。 由編譯器自動分配釋放管理。局部變量及每次函數調用時返回地址、以及調用者的環境信息(例如某些機器寄存器)都存放在棧中。新被調用的函數在棧上爲其自動和臨時變量分配存儲空間。經過以這種方式使用棧,C函數能夠遞歸調用。遞歸函數每次調用自身時,就使用一個新的棧幀,所以一個函數調用實例中的變量集不會影響另外一個函數調用實例中的變量。
a.局部變量
b.函數調用時返回地址
c.調用者的環境信息(例如某些機器寄存器)
從圖中能夠看到未初始化數據段的內容並不存放在磁盤程序文件中。須要存放在磁盤程序文件中的段只有正文段和初始化數據段。size命令報告正文段、數據段、和bss段的長度:
$ size /bin/cc /bin/sh text data bss dec hex 81920 16384 664 98968 18298 /bin/cc 90112 16384 0 106496 1a000 /bin/sh //第4列和第5列分別以十進制和十六進制表示的總長度。
Linux中能夠藉助brk或mmap函數從用戶空間中申請連續內存。
共享庫使得可執行文件中再也不須要包含經常使用的庫函數,而只需在全部進程均可存取的存儲區中保存這種庫例程的一個副本。程序第一次執行的時候或第一次調用某個庫函數的時候,用動態連接方法將程序與共享庫函數相連接,這減小了每一個可執行文件的長度,但增長了一些運行時間開銷。另外一個優勢就是能夠用庫函數的新版原本替換老版本而無需對該庫的程序從新連接編譯。
不一樣的系統使用不一樣的方法說明程序是否須要使用共享庫。比較典型的有cc和ld命令的可選項。
ANSI C說明了三個存儲空間動態分配的函數
malloc。分配指定字節數的存儲區。此存儲區中的初始值不肯定。 (2) calloc。在內存中動態地分配nobj個長度爲size的連續空間。該空間中的每一位都初始化爲0。 (3) realloc。更改之前分配區的長度(增長或減小)。當增長長度時,可能須要將之前分配區的內容移到另外一個足夠大的區域,並且新增區域內的初始值不肯定。
#include <stdlib.h> void *malloc(size_t size); void *calloc(size_t nboj, size_t size); void *realloc(void *ptr, size_t newsize); //三個函數返回:成功返回爲非空指針,出錯爲NULL void free(void *ptr);
這三個分配函數返回的指針必定是適當對齊的,使其能夠用於任何數據對象。在一個特定的系統上,若是最苛刻的對齊要求是double,則對齊必須在8的倍數的地址單元處,那麼這三個函數返回的指針都應這樣對齊。 free函數釋放的空間一般被送入可用存儲區池,之後可在調用分配函數時再調用。 realloc若是在原存儲區後有足夠的空間可供擴充,則可在原存儲區位置上向高地址方向擴充。並返回傳給它的一樣的指針值。若是在原存儲區後沒有足夠的空間則realloc分配一個足夠大的存儲區,將現存的內容複製到新分配的存儲區中。由於這種存儲區會移動位置因此不該使任何指針指到該區。 realloc的最後一個參數是存儲區的newsize而不是新舊長度之差。若是ptr是空指針,則realloc功能與malloc相同。用於分配一個制定長度newsize的存儲區。
這些分配例程一般經過sbrk系統調用實現。該系統調用擴充或縮小進程的堆。
雖然sbrk能夠擴充或縮小一個進程的存儲空間,可是大多數malloc和free的實現都不減少進程的存儲空間而是將它們保存在malloc池中而不返回給內核。
大多數實現所分配的存儲空間比所要求的要大,額外的空間用來記錄管理信息--分配塊的長度,指向下一個分配塊的指針等等。這就意味着若是寫過一個已分配區的尾端,則會改寫後一塊的管理信息。將指向分配塊的指針向後移動可能也會改寫本塊的管理信息。
其餘可能出現的錯誤:釋放一個已經釋放了的塊;調用free所用的指針不是三個alloc函數的返回值等。
alloca函數是在當前函數的棧幀上分配存儲空間。優勢是:當函數返回時自動釋放它所使用的棧幀,缺點是:某些系統在函數已經被調用後不能增長棧幀長度,因而也就不能支持alloca函數。
ANSI C定義了一個函數getenv,能夠用其取環境變量值,可是該標準又稱環境的內容是由實現定義
#include <stdlib.h> char *getenv(const char *name); //返回值:指向與name關聯的value的指針,未找到則返回NULL
POSIX.1和XPG3定義了某些環境變量。下表列出了由這兩個標準定義並受到SVR4和4.3+BSD支持的環境變量。
除了取環境變量值,有時也須要設置環境變量,或者是改變現有變量的值,或者是增長新的環境變量。可是不是全部系統都支持這些操做。下表列出了不一樣的標準及實現支持的各類函數:
#include <stdlib.h> int putenv(const char *str); int setenv(const char *name, const char *value, int rewrite); //兩個函數返回:成功爲0,失敗非0. void unsetenv(const char *name);
這三個函數的操做是:
環境表和環境字符串典型的存放在進程存儲空間的頂部(棧之上)。刪除一個字符串很簡單--只要找到該指針,而後將全部後續指針都向下移一個位置。可是增長一個字符串或修改一個現存的字符串就比較困難。棧以上的空間由於已處於進程存儲空間的頂部因此沒法擴充,即沒法向上擴充也沒法向下擴充。
1.若是修改一個現存的name:
(a) 若是新value的長度少於或等於value的長度,則只要在原字符串所用空間中寫入新字符串。
(b) 若是新value的長度大於原長度,則必須調用malloc爲新字符串分配空間,而後將新字符寫入該空間中,而後使環境表中針對name的指針指向新分配區。
2.若是要增長一個新的name,則操做更爲複雜。首先調用malloc爲name=value分配空間而後將該字符串寫入該空間。而後:
(a) 若是這是第一次增長一個新name,則必須調用malloc爲新的指針表分配空間。將原來的環境表複製到新分配區。並將指向新name=value的指針存在該指針表的表尾,而後又將一個空指針存在其後。最後使environ指針指向新指針表。再看上一節中的內存分配圖,若是原來的環境表位於棧頂之上(這是常見狀況)那麼必須將此表移至堆中。可是此表中的大多數指針仍指向棧頂之上的個name=value字符串。
(b) 若是這不是第一次增長一個新name,則可知之前調用malloc在堆中爲環境表分匹配了空間,因此只要調用realloc,以分配比原來空間多一個指針的空間。而後將該指向新name=value字符串的指針存放在該表表尾,後面跟着一個空指針。
在C中不容許使用跳躍函數的goto語句。而執行這種跳轉功能的是非局部跳轉函數setjmp和longjmp。非局部表示這不是子啊一個函數內的普通的C語言goto語句,而是在棧上跳過若干調用棧,返回到當前函數調用路徑上的一個函數中。
#include <setjmp.h> int setjmp(jmp_buf env); //返回值:直接調用則爲0,若從longjmp返回則爲非0 void longjmp(jmp_buf env, int val);
在但願返回到的位置調用setjmp,由於咱們直接調用該函數因此其返回值爲0。setjmp的參數env是一個特殊類型jmp_buf。這一數據類型是某種形式的數組,其中存放在調用longjmp時能用恢復棧狀態的全部信息。通常,env變量是個全局變量,由於須要從另外一個函數中引用它。
當檢查到一個錯誤時,則調用longjmp函數,第一個參數就是在調用setjmp時所用的env,第二個val是個非0值,它成爲從setjmp處返回的值。使用第二個參數的緣由是對於一個setjmp能夠有多個longjmp。
執行main時,調用setjmp,它將所需的信息記入變量jmpbuffer中返回0。而後調用do_line,它又調用cm_add,假定在其中檢測到一個錯誤。在cmd_add中調用longjmp以前,棧的形式如圖所示
可是longjmp使棧回到執行main函數時的狀況,也就是拋棄了cmd_add和do_line的棧幀。調用longjmp形成main中setjmp的返回。可是,這一次的返回值是1(longjmp的第二個參數)。
在main函數中,自動變量和寄存器變量的狀態如何?當longjmp返回到main函數時,這些變量的值是否能恢復到之前調用setjmp時的值(即滾回原先值),或者這些變量的值保持爲調用do_line時的值(do_line調用cmd_add,cmd_add又調用longjmp)?大多數實現並不滾回這些自動變量和寄存器變量的值,而全部標準則說它們的值是不肯定的。若是有一個自動變量而又不想使其數值滾回能夠定義其爲具備volatile屬性。說明爲全局和靜態變量的值在執行longjmp時保持不變
#include <setjmp.h> static void f1(int, int, int); static void f2(void); static jmp_buf jmpbuffer; int main(void) { int count; register int val; volatile int sum; count 2; val = 3; sum = 4; if (setjmp(jmpbuffer) != 0) { printf("after longjmp: count = %d, val = %d, sum = %d\n", count, val, sum); exit(0); } count = 97; val = 98; sum = 99; f1(count, val, sum); } static void f1(int i, int j, int k) { printf("in f1():count = %d, val = %d, sum = %d\n", i, j, k); f2(); } static void f2(void) { longjmp(jmpbuffer, 1); }
若是以不帶優化和帶優化對此程序分別進行編譯,而後運行它們獲得的結果是不一樣的:
易失變量不受優化的影響,在longjmp以後的值,是它在調用f1時的值。存放在存儲器中的變量將具備longjmp時的值,而在CPU和浮點寄存器中的變量則恢復爲調用setjmp時的值。不進行優化時全部這三個變量都存放在存儲器中(會忽略val寄存器存儲優化)。而進行優化時,count和val都存放在寄存器中。sum因爲加了volatile限定符(該限定符修飾表示告訴編譯器不要對這個變量進行優化)因此不會放到寄存器中。
自動變量問題
一個open_data的函數,它打開了一個標準IO流,而後爲該流設置緩存
FILE *open_data(void) { FILE *fp; char databuf[BUFSIZ]; /* setvbuf設置的標準IO緩存 */ if ((fp = fopen(DATAFILE, "r")) == NULL) { return (NULL); } if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0) { return (NULL); } return (fp); }
當open_data返回時,它在棧上所使用的空間將由下一個被調用函數的棧幀使用。可是,標準IO函數仍然使用原先在棧上分配的存儲空間做爲流的緩存。這就產生了問題。爲了改正這個問題應該在全局空間靜態的(如static或extern),或者動態的爲數組分配空間(malloc在堆上分配)。
#include <sys/time.h> #inlcude <sys/resource.h> int getrlimit(int resource, struct rlimit *rlptr); int setrlimit(int resource, const struct rlimit *rlptr); //返回值:成功爲0,出錯非0. struct rlimit { rlimi_t rlim_cur; /* soft limit: current limit */ rlimi_t rlim_max; /* hard limit:maximum vlaue for rlim_cur */ };
每一個進程都有一組資源限制,其中一些能夠用getrlimit和setrlimit函數查詢和修改。
對這兩個函數的每一次調用都指定一個資源以及一下指向下列結構的指針。
在更改資源限制時,須遵循下列三條規則:
一個無限量的限制一般由常數RLIM_INFINITY指定。
這兩個函數的resource參數取下列值之一。並不是全部資源限制都受到SVR4和4.3+BSD的支持。
資源限制將影響到調用進程並由其子進程繼承。這就意味着爲了影響一個用戶的全部後續進程,需將資源限制設置構造在shell中。