進程環境

進程環境

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

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函式做好安排 。

進程終止

進程有多種退出運行的方式,最常用的是從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為標準 。

atexit

進程環境 進程環境

按照 ISO C的規定,一個進程可以登記最多32個(具體實現可能更多)由exit自動調用的函式,這些函式稱為終止處理程式,調用atexit函式來登記這些函式。#include <stdlib.h>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 <stdlib.h>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程式的記憶體布局如下圖所示:

進程環境 進程環境

上圖說明:

•文本段(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):動態記憶體分配位置。堆的位置位於未初始化數據段和棧的中間 。

存儲空間分布

ISO C說明了3個用於存儲空間動態分配的函式:

malloc分配指定位元組數的存儲區,存儲區的初始值不確定

calloc為指定數量,指定長度的對象分配存儲空間,該空間中,每一位都初始化為0

realloc增加或減少以前分配區的長度,當增加長度時,可能將以前分配的內容移到另一個足夠大的區域以便在尾端增加存儲區,新增的存儲區的初始值不確定

它們的函式聲明如下:

#include <stdlib.h>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的話在函式調用結束也不會自動釋放)。

相關詞條

熱門詞條

聯絡我們