複製鏈接
請複製以下鏈接發送給好友

進程環境

鎖定
程序執行時,main函數是如何被調用的,命令行參數是如何傳遞給程序的,典型的存儲空間佈局是什麼樣式,如何分配其他存儲空間,進程如何使用環境變量,進程的終止等等這些都是進程控制的基礎知識。
中文名
進程環境
外文名
process environment
基礎知識
參數傳遞、 存儲佈局空間等

進程環境main函數

我們知道C程序總是從main函數開始執行,main函數的原型如下:
int main(int argc, char *argv[]);
其中int是main函數的類型,雖然舊的編譯器使用void定義,或者不聲明main的類型也可以編譯,但是那是不好的做法,根據 ISO C和POSIX.1 的定義都應該將main顯式聲明為 int 類型的。argc是命令行參數的數目,argv是指向參數的各個指針構成的數組。與眾面嚮對象語言不同,C需要顯式的 argc 傳遞參數的個數,因為僅憑 argv 不能確定其大小。
當內核執行C程序時,在調用 main 前先調用了一個特殊的啓動例程,可執行程序文件將此例程指定為程序的起始地址–這是有連接編輯器設置的,而連接編輯器則由C編譯器調用,啓動例程從內核取得命令行參數和環境變量值,然後為按上述方式調用main函數做好安排 [1] 

進程環境進程終止

進程有多種退出運行的方式,最常用的是從main函數返回,或者main函數執行到結束。所有的進程終止的方式總結如下,其中前5種正常終止,後三種是異常中止:
從main返回
調用 exit()
調用 _exit() 或者 ——Exit()
最後一個線程從其啓動例程返回
最後一個線程調用 pthread_exit
調用 abort
接到一個信號
最後一個線程對取消請求作出響應
退出函數 exit 也是很常用的, _exit 和 _Exit 則不太常用,它們之間的區別是 exit 會先執行一些清理操作,比如對所有打開的文件調用 fclose 函數,刷新輸出緩衝等,然後在如內核,_exit 和 _Exit 則是立即進入內核的。還有一個區別是它們包含在不同的頭文件中,exit 和 _Exit 包含在中, _exit 包含在中(因為前兩者是ISO C説明的,而後者是POSIX.1説明的)。
這些終止函數都是用一個整型的狀態碼作為參數,稱為終止狀態(exit status)。C99 規定沒有顯示調用return而main執行到最後一個語句時返回,那麼進程的終止狀態是0,在之前的標準這種情況是為定義,所以返回值可能是隨機的。我們的應該以C99為標準 [2] 

進程環境atexit

按照 ISO C的規定,一個進程可以登記最多32個(具體實現可能更多)由exit自動調用的函數,這些函數稱為終止處理程序,調用atexit函數來登記這些函數。#include int atexit(void (*func)(void)); 還記得之前介紹的函數指針嗎,atexit函數的參數的類型就是一個函數指針(函數地址),其返回值和參數都是 void 。注意:exit調用這些登記了的函數的順序與它們登記的順序相反,同一函數若登記多次,則會被調用多次。
一個C程序的啓動和終止流程:
可以看出內核使程序執行的唯一方法是調用一個exec函數。

進程環境命令行參數

命令行參數其實我們之前已經使用過了,基本瞭解了,對於Java和Go中的命令行參數方式也有所瞭解:Java通過一個String數組獲取命令行參數,而Go則通過設置 flag 可以非常方便地獲取特定的參數。
C的命令行參數保存字啊 main函數的第二個參數,char **argv或者 (char *argv[])中,通過第一個參數 argc 獲得參數的個數。如果要想Go那樣獲取特定形式的參數則需要自己對 argv 數組進行一些處理。
在ISO C 和 POSIX.1 中,都要求 argv[argc] 是一個空指針(這由C啓動例程保證),所以對argv的遍歷也可以不借助 argc 的值。
for (int i=0; iargv[i] != NULL; i++) { ...}

進程環境環境變量

每個程序都自動接受(獲得)一張環境表,環境表也是一個字符指針數組(字符串數組),全局變量environ包含了該指針數組的地址。定義為:extern char **environ;。要想在代碼中使用這個數組,需要前面的聲明,否則 environ 是一個非定義的符號。
按照慣例,環境由name=value這樣形式的字符串組成,大多數預定義名完全由大寫字母組成,但是不保證全部是這樣。
ISO C定義了一些函數對環境變量進行讀寫相關的操作:
#include char *getenv(const char *name);int putenv(char *str);// rewrite非0則覆蓋已存在的定義,0則不刪除現有定義int setenv(const char *name, const char *value, int rewrite); int unsetenv(const char *name);

進程環境C程序的存儲空間佈局

典型的C程序的內存佈局如圖1所示:
圖1 圖1
圖1説明:
  • 文本段(Text Segment),保存CPU將要執行的機器指令。文本段是可共享的,所以某個程序多次執行時,對應的文本段只需要在內存中存有一份拷貝。文本段是隻讀的(read-only),防止程序的指令被修改。
  • 已初始化數據段(initialized data segment),保存程序中被初始化的全局變量(定義在任何函數之外)。例如:int maxcount = 99; 全局變量變量maxcount被保存在初始化數據段。
  • 未初始化數據段(uninitialized data segment),也被稱為BSS(block started by symbol),這個段中的數據在程序執行之前被內核初始化為0或者null。;例如定義一個全局變量(定義在任何函數之外),long sum[1000]; 該變量保存在未初始化數據段中。
  • 棧(Stack):存儲臨時變量,函數相關信息。當一個函數被調用時,返回地址、調用者相關信息(如寄存器信息)會被保存在棧中。該被調用的函數會在棧上分配一部分空間保存它的臨時變量。函數的遞歸調用也是應用這個原理。每一次函數調用自己,都會保存當前函數的信息,然後再棧上開闢一個新的空間用於保存該次函數的信息,和以前的函數並沒有影響。
  • 堆(Heap):動態內存分配位置。堆的位置位於未初始化數據段和棧的中間 [3] 

進程環境存儲空間分佈

ISO C説明了3個用於存儲空間動態分配的函數:
malloc分配指定字節數的存儲區,存儲區的初始值不確定
calloc為指定數量,指定長度的對象分配存儲空間,該空間中,每一位都初始化為0
realloc增加或減少以前分配區的長度,當增加長度時,可能將以前分配的內容移到另一個足夠大的區域以便在尾端增加存儲區,新增的存儲區的初始值不確定
它們的函數聲明如下:
#include void *malloc(size_t size);void *calloc(size_t nobj, size_t size);void *realloc(void *ptr, size_t newsize);void free(void *ptr);
關於它們返回值的賦值有一個要注意的地方,參見這裏。要注意 realloc函數的第二個參數是存儲區的新長度,而不是新舊存儲區的長度之差。
這些存儲區分配函數通常用sbrk系統調用實現,該系統調用擴充(或縮小)進程的堆,雖然 sbrk 可以擴充或者縮小進程的存儲空間,但是大多數 malloc 和 free的實現都不減少進程的存儲空間,釋放的空間可供以後再分配,但是將它們保持在 malloc 池中,而不是返還給內核。
大多數動態分配函數的實現實際分配的空間比所請求的要大一些,額外的空間用於記錄管理信息,比如分配塊的長度,指向下一個分配塊的指針等。如果在超多分配去尾端或者在已分配區開始位置之前進行寫操作,會修改另一塊的管理記錄信息,這導致的錯誤是災難性的,可惡的是這中錯誤很難發現。在動態分配的緩衝區的前或後進行寫操作,破壞的可能不僅僅是該區的管理記錄信息,這些區域可能用於其它動態分配的對象,這些對象因此可能被破壞,而且很難追查到原因。
另一個導致致命錯誤的是:釋放一個已經釋放的塊,或者free的參數的指針不是由上面的函數分配的對象。對一個對象調用 free 後,這個指針的值實際上沒有改變,它仍然在作用域內,如果該指針指向的地址被重新分配了,對它再進行free就會導致預料之外的行為。
如果一個分配的區域沒有調用free則會導致進程佔用的存儲空間越來越大,導致泄漏(即時是在調用函數中分配的空間,沒有free的話在函數調用結束也不會自動釋放)。
參考資料
  • 1.    霍普克羅夫特 (John E.Hopcroft) , Rajeev Motwani , Jeffrey D.Ullman .自動機理論、語言和計算導論:機械工業出版社,2008
  • 2.    Alfred V. Aho , Monica S.Lam .編譯原理(第2版):機械工業出版社,2009
  • 3.    博韋 .深入理解LINUX內核:中國電力出版社,2008