7.1引言

將介紹進(jìn)程控制原語,在此之前需先了解進(jìn)程的環(huán)境。本章 中將學(xué)習(xí):當(dāng)程序執(zhí)行時(shí),其main函數(shù)是如何被調(diào)用的;命令行參數(shù)是 如何傳遞給新程序的;典型的存儲空間布局是什么樣式;如何分配另外 的存儲空間;進(jìn)程如何使用環(huán)境變量;進(jìn)程的各種不同終止方式等。另 外,還將說明longjmp和setjmp函數(shù)以及它們與棧的交互作用。本章結(jié)束 之前,還將查看進(jìn)程的資源限制。
7.2 main函數(shù)
C程序總是從main函數(shù)開始執(zhí)行。main函數(shù)的原型是:
int main(int argc, char *argv[]); 其中,argc是命令行參數(shù)的數(shù)目,argv是指向參數(shù)的各個(gè)指針?biāo)鶚?gòu)成的數(shù)組。7.4 節(jié)將對命令行參數(shù)進(jìn)行說明。 當(dāng)內(nèi)核執(zhí)行C程序時(shí)(使用一個(gè)exec函數(shù),8.10節(jié)將說明exec函數(shù)),在調(diào)用main前先調(diào)用一個(gè)特殊的啟動例程??蓤?zhí)行程序文件將此 啟動例程指定為程序的起始地址——這是由連接編輯器設(shè)置的,而連接 編輯器則由C編譯器調(diào)用。啟動例程從內(nèi)核取得命令行參數(shù)和環(huán)境變量 值,然后為按上述方式調(diào)用main函數(shù)做好安排。
7.3 進(jìn)程終止
有8種方式使進(jìn)程終止(termination),其中 5種為正常終止,它們 是:
- (1)從main返回;
- (2)調(diào)用exit;
- (3)調(diào)用_exit或_Exit;
- (4)最后一個(gè)線程從其啟動例程返回(11.5節(jié));
- (5)從最后一個(gè)線程調(diào)用pthread_exit(11.5節(jié))。
異常終止有3種方式,它們是:
- (6)調(diào)用abort(10.17節(jié));
- (7)接到一個(gè)信號(10.2節(jié));
- (8)最后一個(gè)線程對取消請求做出響應(yīng)(11.5節(jié)和12.7節(jié))。
在第11章和第12章討論線程之前,我們暫不考慮專門針對線程的3
種終止方式。
上節(jié)提及的啟動例程是這樣編寫的,使得從main返回后立即調(diào)用 exit函數(shù)。如果將啟動例程以C代碼形式表示(實(shí)際上該例程常常用匯編 語言編寫),則它調(diào)用main函數(shù)的形式可能是:
exit(main(argc, argv));
- 1.退出函數(shù)
3個(gè)函數(shù)用于正常終止一個(gè)程序:_exit和_Exit立即進(jìn)入內(nèi)核,exit則 先執(zhí)行一些清理處理,然后返回內(nèi)核。
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
由于歷史原因,exit 函數(shù)總是執(zhí)行一個(gè)標(biāo)準(zhǔn) I/O 庫的清理關(guān)閉操 作:對于所有打開流調(diào)用fclose函數(shù)。回憶5.5節(jié),這造成輸出緩沖中的 所有數(shù)據(jù)都被沖洗(寫到文件上)。
3個(gè)退出函數(shù)都帶一個(gè)整型參數(shù),稱為終止?fàn)顟B(tài)(或退出狀態(tài),exit status)。大多 數(shù)UNIX系統(tǒng)shell都提供檢查進(jìn)程終止?fàn)顟B(tài)的方法。如果 (a)調(diào)用這些函數(shù)時(shí)不帶終止?fàn)顟B(tài),或(b)main執(zhí)行了一個(gè)無返回值 的return語句,或(c)main沒有聲明返回類型為整型,則該進(jìn)程的終止 狀態(tài)是未定義的。但是,若main的返回類型是整型,并且main執(zhí)行到最 后一條語句時(shí)返回(隱式返回),那么該進(jìn)程的終止?fàn)顟B(tài)是0。
這種處理是ISO C標(biāo)準(zhǔn)1999版引入的。歷史上,若main函數(shù)終止時(shí) 沒有顯式使用return語句或調(diào)用exit函數(shù),那么進(jìn)程終止?fàn)顟B(tài)是未定義 的。
main函數(shù)返回一個(gè)整型值與用該值調(diào)用exit是等價(jià)的。于是在main 函數(shù)中
exit(0);
等價(jià)于
return(0);
#include <stdio.h>
int main() {
printf("hello world\n");
}

-2 函數(shù)atexit
按照ISO C的規(guī)定,一個(gè)進(jìn)程可以登記多至32個(gè)函數(shù),這些函數(shù)將 由exit自動調(diào)用。我們稱這些函數(shù)為終止處理程序(exit handler),并調(diào) 用atexit函數(shù)來登記這些函數(shù)。
#include <stdlib.h>
int atexit(void (*func)(void));
返回值:若成功,返回0;若出錯(cuò),返回非0
其中,atexit 的參數(shù)是一個(gè)函數(shù)地址,當(dāng)調(diào)用此函數(shù)時(shí)無需向它傳
遞任何參數(shù),也不期望它返回一個(gè)值。exit調(diào)用這些函數(shù)的順序與它們 登記時(shí)候的順序相反。同一函數(shù)如若登記多次,也會被調(diào)用多次。
ISO C要求,系統(tǒng)至少應(yīng)支持32個(gè)終止處理程序,但實(shí)現(xiàn)經(jīng)常會提 供更多的支持(參見圖2-15)。為了確定一個(gè)給定的平臺支持的最大終
止處理程序數(shù),可以使用sysconf函數(shù)(如圖2-14所示)。
根據(jù)ISO C和POSIX.1,exit首先調(diào)用各終止處理程序,然后關(guān)閉 (通過fclose)所有打開流。POSIX.1擴(kuò)展了ISO C標(biāo)準(zhǔn),它說明,如若 程序調(diào)用exec函數(shù)族中的任一函數(shù),則將清除所有已安裝的終止處理程 序。圖7-2顯示了一個(gè)C程序是如何啟動的,以及它終止的各種方式。

注意,內(nèi)核使程序執(zhí)行的唯一方法是調(diào)用一個(gè)exec函數(shù)。進(jìn)程自愿 終止的唯一方法是顯式或隱式地(通過調(diào)用 exit)調(diào)用_exit 或_Exit。進(jìn) 程也可非自愿地由一個(gè)信號使其終止(圖 7-2中沒有顯示)。
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <string>
#include <stdlib.h>
static void my_exit1() {
std::cout << "my_exit" << std::endl;
}
static void my_exit2() {
std::cout << "my_exit2" << std::endl;
}
int main()
{
atexit(my_exit1);
atexit(my_exit2);
printf("main function is done\n");
return 0;
}

7.4 命令行參數(shù)
當(dāng)執(zhí)行一個(gè)程序時(shí),調(diào)用exec的進(jìn)程可將命令行參數(shù)傳遞給該新程 序。這是UNIX shell的一部分常規(guī)操作。在前幾章的很多實(shí)例中,我們 已經(jīng)看到了這一點(diǎn)。
所示的程序?qū)⑵渌忻钚袇?shù)都回顯到標(biāo)準(zhǔn)輸出上。注 意,通常的 echo程序不回顯第0個(gè)參數(shù)。
#include <iostream>
#include <stdio.h>
int main(int argc ,char * argv[]) {
int i = 0;
for (i = 0; i < argc; ++i ) {
printf("argv[%d] is : %s\n", argc, argv[i]);
}
return 0;
}

ISO C和POSIX.1都要求argv[argc]是一個(gè)空指針。這就使我們可以將 參數(shù)處理循環(huán)改寫為:
for (int i =0 ; argv[i]!= NULL; ++i)
7.5環(huán)境表
每個(gè)程序都接收到一張環(huán)境表。與參數(shù)表一樣,環(huán)境表也是一個(gè)字 符指針數(shù)組,其中每個(gè)指針包含一個(gè)以null結(jié)束的C字符串的地址。全局 變量environ則包含了該指針數(shù)組的地址:
extern char **environ;
例如,如果該環(huán)境包含5個(gè)字符串,那么它看起來如圖7-5中所示。 其中,每個(gè)字符串的結(jié)尾處都顯式地有一個(gè)null字節(jié)。我們稱environ為 環(huán)境指針(environment pointer),指針數(shù)組為環(huán)境表,其中各指針指向 的字符串為環(huán)境字符串。

按照慣例,環(huán)境由
name = value 這樣的字符串組成,如圖7-5中所示。大多數(shù)預(yù)定義名完全由大寫字母組成,但這只是一個(gè)慣例。
在歷史上,大多數(shù)UNIX系統(tǒng)支持main函數(shù)帶3個(gè)參數(shù),其中第3個(gè)
參數(shù)就是環(huán)境表地址:
int main(int argc, char *argv[], char *envp[]);
因?yàn)镮SO C規(guī)定main函數(shù)只有兩個(gè)參數(shù),而且第3個(gè)參數(shù)與全局變量 environ相比也沒有帶來更多益處,所以 POSIX.1 也規(guī)定應(yīng)使用 environ 而不使用第 3 個(gè)參數(shù)。通常用 getenv 和putenv函數(shù)(見7.9節(jié))來訪問特 定的環(huán)境變量,而不是用environ變量。但是,如果要查看整個(gè)環(huán)境,則 必須使用environ指針。
我們寫一個(gè)打印環(huán)境變量的代碼
#include <iostream>
#include <stdio.h>
extern char **environ;
int main() {
/*----------------------------------- display environ ----------------------------------------*/
for (int i = 0; environ[i] != NULL; ++i) {
printf("environ: %s\n", environ[i]);
}
}

7.6 C程序儲存空間布局
歷史沿襲至今,C程序一直由下列幾部分組成:

- 代碼段:程序的所有指令會存放在這個(gè)區(qū)域,這是已經(jīng)編譯后的機(jī)器碼。
這是由CPU執(zhí)行的機(jī)器指令部分。通常,正文段是可共 享的,所以即使是頻繁執(zhí)行的程序(如文本編輯器、C編譯器和shell 等)在存儲器中也只需有一個(gè)副本,另外,正文段常常是只讀的,以防 止程序由于意外而修改其指令。
字面量池是程序初始化時(shí)的一些字符串字面量,在程序中用于顯示文字
全局?jǐn)?shù)據(jù)段:通常將此段稱為數(shù)據(jù)段,程序初始化時(shí)的常量和全局/靜態(tài)的變量。C/C++ 用global/static聲明的變量都存放在這個(gè)區(qū)域,對所有函數(shù)公開可見。(static或extern 已經(jīng)初始化的全局變量都在這里)
它包含了程序中需明確 地賦初值的變量。例如, C程序中任何函數(shù)之外的聲明:
int maxcount = 99;
?未初始化數(shù)據(jù)段。通常將此段稱為bss段,這一名稱來源于早期匯 編程序一個(gè)操作符,意思是“由符號開始的塊”(block started by symbol),在程序開始執(zhí)行之前,內(nèi)核將此段中的數(shù)據(jù)初始化為0或空 指針。函數(shù)外的聲明:
long sum[1000];
使此變量存放在非初始化數(shù)據(jù)段中。
堆: 通常在堆中進(jìn)行動態(tài)存儲分配。由于歷史上形成的慣例,堆位于未初始化數(shù)據(jù)段和棧之間。這里保存的數(shù)據(jù)只是為了臨時(shí)存儲一些值而創(chuàng)建的,而我們可能在程序運(yùn)行過程中可能會回收此內(nèi)存。因?yàn)槲覀冊诔绦驁?zhí)行期間不需要很長時(shí)間,所以使用C中的new或malloc這類內(nèi)存分配程序來為我們所需的特定數(shù)據(jù)類型提供新的空間,并且隨著我們要求越來越多的動態(tài)數(shù)據(jù)空間而該區(qū)域不斷擴(kuò)大,并且在內(nèi)存中逐漸增長到更高的地址。
-
棧:
自動變量以及每次函數(shù)調(diào)用時(shí)所需保存的信息都存放在此段 中。每次函數(shù)調(diào)用時(shí),其返回地址以及調(diào)用者的環(huán)境信息(如某些機(jī)器 寄存器的值)都存放在棧中。然后,最近被調(diào)用的函數(shù)在棧上為其自動 和臨時(shí)變量分配存儲空間。通過以這種方式使用棧,C遞歸函數(shù)可以工 作。遞歸函數(shù)每次調(diào)用自身時(shí),就用一個(gè)新的棧幀,因此一次函數(shù)調(diào)用 實(shí)例中的變量集不會影響另一次函數(shù)調(diào)用實(shí)例中的變量。當(dāng)我們執(zhí)行這些過程調(diào)用時(shí),堆的基本特性是LIFO,存儲著該程序“上下文”,它將從內(nèi)存的高層地址開始,然后向另一個(gè)方向向下擴(kuò)展。上下文其實(shí)就是程序中各個(gè)函數(shù)之間調(diào)用的先后順序。
7.7 共享庫
即動態(tài)庫,linux系統(tǒng)上的.so文件
現(xiàn)在,大多數(shù)UNIX系統(tǒng)支持共享庫。Arnold[1986]說明了System V 上共享庫的一個(gè)早期實(shí)現(xiàn),Gingell等[1987]則說明了SunOS上的另一個(gè)實(shí) 現(xiàn)。共享庫使得可執(zhí)行文件中不再需要包含公用的庫函數(shù),而只需在所 有進(jìn)程都可引用的存儲區(qū)中保存這種庫例程的一個(gè)副本。程序第一次執(zhí) 行或者第一次調(diào)用某個(gè)庫函數(shù)時(shí),用動態(tài)鏈接方法將程序與共享庫函數(shù) 相鏈接。這減少了每個(gè)可執(zhí)行文件的長度,但增加了一些運(yùn)行時(shí)間開 銷。
這種時(shí)間開銷發(fā)生在該程序第一次被執(zhí)行時(shí),或者每個(gè)共享庫函數(shù) 第一次被調(diào)用時(shí)。共享庫的另一個(gè)優(yōu)點(diǎn)是可以用庫函數(shù)的新版本代替老 版本而無需對使用該庫的程序重新連接編輯(假定參數(shù)的數(shù)目和類型都 沒有發(fā)生改變)。
在不同的系統(tǒng)中,程序可能使用不同的方法說明是否要使用共享 庫。比較典型的有 cc(1)和ld(1)命令的選項(xiàng)。作為長度方面發(fā)生變化的例 子,先用無共享庫方式創(chuàng)建下列可執(zhí)行文件(典型的hello.c程序):
$ gcc -static hello1.c 阻止gcc 使用共享庫
7.8 儲存空間分配

ISO C說明了3個(gè)用于存儲空間動態(tài)分配的函數(shù)。
- (1)malloc,分配指定字節(jié)數(shù)的存儲區(qū)。此存儲區(qū)中的初始值不確 定。
- (2)calloc,為指定數(shù)量指定長度的對象分配存儲空間。該空間中 的每一位(bit)都初始化為0。
- (3)realloc,增加或減少以前分配區(qū)的長度。當(dāng)增加長度時(shí),可能 需將以前分配區(qū)的內(nèi)容移到另一個(gè)足夠大的區(qū)域,以便在尾端提供增加的存儲區(qū),而新增 區(qū)域內(nèi)的初始值則不確定。
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
3個(gè)函數(shù)返回值:若成功,返回非空指針;若出錯(cuò),返回NULL
void free(void *ptr);
這3個(gè)分配函數(shù)所返回的指針一定是適當(dāng)對齊的,使其可用于任何 數(shù)據(jù)對象。例如,在一個(gè)特定的系統(tǒng)上,如果最苛刻的對齊要求是, double必須在8的倍數(shù)地址單元處開始,那么這3個(gè)函數(shù)返回的指針都應(yīng) 這樣對齊。
因?yàn)檫@ 3 個(gè) alloc 函數(shù)都返回通用指針 void *,所以如果在程序中包 括了#include<stdlib.h>(以獲得函數(shù)原型),那么當(dāng)我們將這些函數(shù)返 回的指針賦予一個(gè)不同類型的指針時(shí),就不需要顯式地執(zhí)行強(qiáng)制類型轉(zhuǎn)換。未聲明函數(shù)的默認(rèn)返回值為int,所以使用沒有正確函數(shù)聲明的強(qiáng)制 類型轉(zhuǎn)換可能會隱藏系統(tǒng)錯(cuò)誤,因?yàn)閕nt類型的長度與函數(shù)返回類型值的 長度不同(本例中是指針)。
函數(shù)free 釋放ptr指向的存儲空間。被釋放的空間通常被送入可用存 儲區(qū)池,以后,可在調(diào)用上述3個(gè)分配函數(shù)時(shí)再分配。
realloc函數(shù)使我們可以增、減以前分配的存儲區(qū)的長度(最常見的 用法是增加該區(qū))。例如,如果先為一個(gè)數(shù)組分配存儲空間,該數(shù)組長 度為 512,然后在運(yùn)行時(shí)填充它,但運(yùn)行一段時(shí)間后發(fā)現(xiàn)該數(shù)組原先的 長度不夠用,此時(shí)就可調(diào)用 realloc 擴(kuò)充相應(yīng)存儲空間。如果在該存儲區(qū) 后有足夠的空間可供擴(kuò)充,則可在原存儲區(qū)位置上向高地址方向擴(kuò)充, 無需移動任何原先的內(nèi)容,并返回與傳給它相同的指針值。如果在原存 儲區(qū)后沒有足夠的空間,則 realloc 分配另一個(gè)足夠大的存儲區(qū),將現(xiàn)存 的512個(gè)元素?cái)?shù)組的內(nèi)容復(fù)制到新分配的存儲區(qū)。然后,釋放原存儲 區(qū),返回新分配區(qū)的指針。因?yàn)檫@種存儲區(qū)可能會移動位置,所以不應(yīng) 當(dāng)使任何指針指在該區(qū)中。
注意,realloc的最后一個(gè)參數(shù)是存儲區(qū)的新長度,不是新、舊存儲 區(qū)長度之差。作為一個(gè)特例,若ptr是一個(gè)空指針,則realloc的功能與 malloc相同,用于分配一個(gè)指定長度為newsize的存儲區(qū)。
這些函數(shù)的早期版本允許調(diào)用realloc分配自上次malloc、realloc 或calloc調(diào)用以來所釋放的塊。這種技巧可回溯到 V7,它利用 malloc 的搜索策略,實(shí)現(xiàn)存儲器緊縮。Solaris仍支持這一功能,而很多其他 平臺則不支持。這種功能不被贊同,不應(yīng)再使用。
這些分配例程通常用sbrk(2)系統(tǒng)調(diào)用實(shí)現(xiàn)。該系統(tǒng)調(diào)用擴(kuò)充(或縮 小)進(jìn)程的堆
雖然sbrk可以擴(kuò)充或縮小進(jìn)程的存儲空間,但是大多數(shù)malloc和free 的實(shí)現(xiàn)都不減小進(jìn)程的存儲空間。釋放的空間可供以后再分配,但將它 們保持在malloc池中而不返回給內(nèi)核。
大多數(shù)實(shí)現(xiàn)所分配的存儲空間比所要求的要稍大一些,額外的空間 用來記錄管理信息——分配塊的長度、指向下一個(gè)分配塊的指針等。這 就意味著,如果超過一個(gè)已分配區(qū)的尾端或者在已分配區(qū)起始位置之前 進(jìn)行寫操作,則會改寫另一塊的管理記錄信息。這種類型的錯(cuò)誤是災(zāi)難 性的,但是因?yàn)檫@種錯(cuò)誤不會很快就暴露出來,所以也就很難發(fā)現(xiàn)。
在動態(tài)分配的緩沖區(qū)前或后進(jìn)行寫操作,破壞的可能不僅僅是該區(qū) 的管理記錄信息。在動態(tài)分配的緩沖區(qū)前后的存儲空間很可能用于其他 動態(tài)分配的對象。這些對象與破壞它們的代碼可能無關(guān),這造成尋求信 息破壞的源頭更加困難。
其他可能產(chǎn)生的致命性的錯(cuò)誤是:釋放一個(gè)已經(jīng)釋放了的塊;調(diào)用 free時(shí)所用的指針不是3個(gè)alloc函數(shù)的返回值等。如若一個(gè)進(jìn)程調(diào)用 malloc函數(shù),但卻忘記調(diào)用free函數(shù),那么該進(jìn)程占用的存儲空間就會連 續(xù)增加,這被稱為泄漏(leakage)。如果不調(diào)用free函數(shù)釋放不再使用 的空間,那么進(jìn)程地址空間長度就會慢慢增加,直至不再有空閑空間。 此時(shí),由于過度的換頁開銷,會造成性能下降。
因?yàn)榇鎯臻g分配出錯(cuò)很難跟蹤,所以某些系統(tǒng)提供了這些函數(shù)的 另一種實(shí)現(xiàn)版本。每次調(diào)用這3個(gè)分配函數(shù)中的任意一個(gè)或free時(shí),它們 都進(jìn)行附加的檢錯(cuò)。在調(diào)用連接編輯器時(shí)指定一個(gè)專用庫,在程序中就 可使用這種版本的函數(shù)。此外還有公共可用的資源,在對其進(jìn)行編譯時(shí) 使用一個(gè)特殊標(biāo)志就會使附加的運(yùn)行時(shí)檢查生效。
有很多可替代malloc和free的函數(shù)。某些系統(tǒng)已經(jīng)提供替代存儲空間 分配函數(shù)的庫。另一些系統(tǒng)只提供標(biāo)準(zhǔn)的存儲空間分配程序。如果需 要,軟件開發(fā)者可以下載替代函數(shù)。下面討論某些替代函數(shù)和庫。
1.libmalloc
基于SVR4的UNIX系統(tǒng),如Solaries,包含了libmalloc庫,它提供了 一套與ISO C存儲空間分配函數(shù)相匹配的接口。libmalloc庫包括mallopt函 數(shù),它使進(jìn)程可以設(shè)置一些變量,并用它們來控制存儲空間分配程序的 操作。還可使用另一個(gè)名為mallinfo的函數(shù),以對存儲空間分配程序的 操作進(jìn)行統(tǒng)計(jì)。2.vmalloc
Vo[1996]說明一種存儲空間分配程序,它允許進(jìn)程對于不同的存儲 區(qū)使用不同的技術(shù)。除了一些vmalloc特有的函數(shù)外,該庫也提供了ISO C存儲空間分配函數(shù)的仿真器。3.quick-fit
歷史上所使用的標(biāo)準(zhǔn) malloc 算法是最佳適配或首次適配存儲分配策 略。quick-fit(快速適配)算法比上述兩種算法快,但可能使用較多存 儲空間。Weinstock和Wulf[1988]對該算法進(jìn)行了描述,該算法基于將存 儲空間分裂成各種長度的緩沖區(qū),并將未使用的緩沖區(qū)按其長度組成不 同的空閑區(qū)列表?,F(xiàn)在許多分配程序都基于快速適配。4.jemalloc
jemalloc函數(shù)實(shí)現(xiàn)是FreeBSD 8.0中的默認(rèn)存儲空間分配程序,它是 庫函數(shù)malloc族在FreeBSD中的實(shí)現(xiàn)。它的設(shè)計(jì)具有良好的可擴(kuò)展性,可 用于多處理器系統(tǒng)中使用多線程的應(yīng)用程序。Evans[2006]說明了具體實(shí) 現(xiàn)及其性能評估。5.TCMalloc
TCMalloc函數(shù)用于替代malloc函數(shù)族以提供高性能、高擴(kuò)展性和高 存儲效率。從高速緩存中分配緩沖區(qū)以及釋放緩沖區(qū)到高速緩存中時(shí),
它使用線程-本地高速緩存來避免鎖開銷。它還有內(nèi)置的堆檢查程序和 堆分析程序幫助調(diào)試和分析動態(tài)存儲的使用。TCMalloc庫是開源可用 的,是Google-perftools工具中的一個(gè)。Ghemawat和Menage[2005]對此做 了簡單介紹。6.函數(shù)alloca
還有一個(gè)函數(shù)也值得一提,這就是alloca。它的調(diào)用序列與malloc相 同,但是它是在當(dāng)前函數(shù)的棧幀上分配存儲空間,而不是在堆中。其優(yōu) 點(diǎn)是:當(dāng)函數(shù)返回時(shí),自動釋放它所使用的棧幀,所以不必再為釋放空 間而費(fèi)心。其缺點(diǎn)是:alloca 函數(shù)增加了棧幀的長度,而某些系統(tǒng)在函 數(shù)已被調(diào)用后不能增加棧幀長度,于是也就不能支持alloca函數(shù)。盡管如 此,很多軟件包還是使用alloca函數(shù),也有很多系統(tǒng)實(shí)現(xiàn)了該函數(shù)。
本書中討論的4個(gè)平臺都提供了alloca函數(shù)。
7.9環(huán)境變量
如同前述,環(huán)境字符串的形式是:
name=value
UNIX內(nèi)核并不查看這些字符串,它們的解釋完全取決于各個(gè)應(yīng)用
程序。例如,shell使用了大量的環(huán)境變量。其中某一些在登錄時(shí)自動設(shè) 置(如HOME、USER等),有些則由用戶設(shè)置。我們通常在一個(gè)shell 啟動文件中設(shè)置環(huán)境變量以控制shell的動作。例如,若設(shè)置了環(huán)境變量 MAILPATH,則它告訴Bourne shell、GNU Bourne-again shell和Korn shell 到哪里去查看郵件。
ISO C定義了一個(gè)函數(shù)getenv,可以用其取環(huán)境變量值,但是該標(biāo)準(zhǔn) 又稱環(huán)境的內(nèi)容是由實(shí)現(xiàn)定義的。
#include <stdlib.h>
char *getenv(const char *name);
返回值:指向與name關(guān)聯(lián)的value的指針;若未找到,返回NULL 注意,此函數(shù)返回一個(gè)指針,它指向name=value字符串中的value。
我們應(yīng)當(dāng)使用getenv從環(huán)境中取一個(gè)指定環(huán)境變量的值,而不是直接訪 問environ。
Single UNIX Specification中的POSIX.1定義了某些環(huán)境變量。如果支 持XSI擴(kuò)展,那么其中也包含了另外一些環(huán)境變量定義。圖7-7列出了由 Single UNIX Specification定義的環(huán)境變量,并指明本書討論的4種實(shí)現(xiàn)對 它們的支持情況。由POSIX.1定義的各環(huán)境變量標(biāo)記為?,否則為XSI擴(kuò) 展。本書討論的4種UNIX實(shí)現(xiàn)使用了很多依賴于實(shí)現(xiàn)的環(huán)境變量。注 意,ISO C沒有定義任何環(huán)境變量。

除了獲取環(huán)境變量值,有時(shí)也需要設(shè)置環(huán)境變量。我們可能希望改 變現(xiàn)有變量的值,或者是增加新的環(huán)境變量。(在下一章將會了解到, 我們能影響的只是當(dāng)前進(jìn)程及其后生成和調(diào)用的任何子進(jìn)程的環(huán)境,但 不能影響父進(jìn)程的環(huán)境,這通常是一個(gè)shell進(jìn)程。盡管如此,修改環(huán)境 表的能力仍然是很有用的。)遺憾的是,并不是所有系統(tǒng)都支持這種能 力。圖7-8列出了由不同的標(biāo)準(zhǔn)及實(shí)現(xiàn)支持的各種函數(shù)。

clearenv不是Single UNIX Specification的組成部分。它被用來刪除環(huán) 境表中的所有項(xiàng)。在圖7-8中,中間3個(gè)函數(shù)的原型是:
#include <stdlib.h>
int putenv(char *str);
函數(shù)返回值:若成功,返回0;若出錯(cuò),返回非0
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
兩個(gè)函數(shù)返回值:若成功,返回0;若出錯(cuò),返回?1 這3個(gè)函數(shù)的操作如下。
putenv取形式為name=value的字符串,將其放到環(huán)境表中。如果 name已經(jīng)存在,則先刪除其原來的定義。
setenv將name設(shè)置為value。如果在環(huán)境中name已經(jīng)存在,那么 (a)若rewrite非0,則首先刪除其現(xiàn)有的定義;(b)若rewrite為0,則 不刪除其現(xiàn)有定義(name不設(shè)置為新的value,而且也不出錯(cuò))。
unsetenv刪除name的定義。即使不存在這種定義也不算出錯(cuò)。
注意,putenv和setenv之間的差別。setenv必須分配存儲空間,以 便依據(jù)其參數(shù)創(chuàng)建name=value字符串。putenv可以自由地將傳遞給它的 參數(shù)字符串直接放到環(huán)境中。確實(shí),許多實(shí)現(xiàn)就是這么做的,因此,將 存放在棧中的字符串作為參數(shù)傳遞給putenv就會發(fā)生錯(cuò)誤,其原因是, 從當(dāng)前函數(shù)返回時(shí),其棧幀占用的存儲區(qū)可能將被重用。
我們寫一個(gè)獲取HOME目錄環(huán)境變量的小代碼
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
/*----------------------------------- test getenv ----------------------------------------*/
/* env name. */
char name[255];
memset(name, 0, 255);
char *p = name;
/* get env. */
p = getenv("HOME");
printf("env HOME: %s\n", p);
}

這些函數(shù)在修改環(huán)境表時(shí)是如何進(jìn)行操作的呢?對這一問題進(jìn)行研 究、考察是非常有益的?;貞洺绦虻膬?nèi)存布局,其中,環(huán)境表(指向?qū)嶋H name=value字符串的指針數(shù)組)和環(huán)境字符串通常存放在進(jìn)程存儲空間 的頂部(棧之上)。刪除一個(gè)字符串很簡單——只要先在環(huán)境表中找到 該指針,然后將所有后續(xù)指針都向環(huán)境表首部順次移動一個(gè)位置。但是 增加一個(gè)字符串或修改一個(gè)現(xiàn)有的字符串就困難得多。環(huán)境表和環(huán)境字符串通常占用的是進(jìn)程地址空間的頂部,所以它不能再向高地址方向 (向上)擴(kuò)展:同時(shí)也不能移動在它之下的各棧幀,所以它也不能向低 地址方向(向下)擴(kuò)展。兩者組合使得該空間的長度不能再增加。
(1)如果修改一個(gè)現(xiàn)有的name:
a.如果新value的長度少于或等于現(xiàn)有value的長度,則只要將新字 符串復(fù)制到原字符串所用的空間中;
b.如果新value的長度大于原長度,則必須調(diào)用malloc為新字符串分 配空間,然后將新字符串復(fù)制到該空間中,接著使環(huán)境表中針對name的 指針指向新分配區(qū)。
(2)如果要增加一個(gè)新的name,則操作就更加復(fù)雜。首先,必須 調(diào)用malloc為name=value字符串分配空間,然后將該字符串復(fù)制到此空 間中。
a.如果這是第一次增加一個(gè)新name,則必須調(diào)用malloc為新的指針 表分配空間。接著,將原來的環(huán)境表復(fù)制到新分配區(qū),并將指向新 name=value字符串的指針存放在該指針表的表尾,然后又將一個(gè)空指針 存放在其后。最后使environ指向新指針表。再看一下圖7-6,如果原來 的環(huán)境表位于棧頂之上(這是一種常見情況),那么必須將此表移至堆 中。
但是,此表中的大多數(shù)指針仍指向棧頂之上的各name=value字符 串。b.如果這不是第一次增加一個(gè)新name,則可知以前已調(diào)用malloc在 堆中為環(huán)境表分配了空間,所以只要調(diào)用 realloc,以分配比原空間多存 放一個(gè)指針的空間。然后將指向新name=value字符串的指針存放在該表 表尾,后面跟著一個(gè)空指針。
7.10 setjmp 和 long jmp
在C中,goto語句是不能跨越函數(shù)的,而執(zhí)行這種類型跳轉(zhuǎn)功能的 是函數(shù)setjmp和longjmp。這兩個(gè)函數(shù)對于處理發(fā)生在很深層嵌套函數(shù)調(diào) 用中的出錯(cuò)情況是非常有用的。
非局部goto——setjmp和longjmp函 數(shù)。非局部指的是,這不是由普通的C語言goto語句在一個(gè)函數(shù)內(nèi)實(shí)施 的跳轉(zhuǎn),而是在棧上跳過若干調(diào)用幀,返回到當(dāng)前函數(shù)調(diào)用路徑上的某 一個(gè)函數(shù)中。
#include <setjmp.h>
int setjmp(jmp_buf env);
返回值:若直接調(diào)用,返回0;若從longjmp返回,則為非0
void longjmp(jmp_buf env, int val);
setjmp參數(shù)env的類 型是一個(gè)特殊類型jmp_buf。這一數(shù)據(jù)類型是某種形式的數(shù)組,其中存 放在調(diào)用 longjmp 時(shí)能用來恢復(fù)棧狀態(tài)的所有信息。因?yàn)樾柙诹硪粋€(gè)函 數(shù)中引用env變量,所以通常將env變量定義為全局變量。
要保證env不會因?yàn)闂袚Q改變,可以保存為static /extern 或在堆區(qū)建設(shè)
7.11 函數(shù)getrlimit 和setrlimit
每個(gè)進(jìn)程都有一組資源限制,其中一些可以用getrlimit和setrlimit函 數(shù)查詢和更改。
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
兩個(gè)函數(shù)返回值:若成功,返回0;若出錯(cuò),返回非0
這兩個(gè)函數(shù)在Single UNIX Specification的XSI擴(kuò)展中定義。進(jìn)程 的資源限制通常是在系統(tǒng)初始化時(shí)由0進(jìn)程建立的,然后由后續(xù)進(jìn)程繼 承。每種實(shí)現(xiàn)都可以用自己的方法對資源限制做出調(diào)整。
對這兩個(gè)函數(shù)的每一次調(diào)用都指定一個(gè)資源以及一個(gè)指向下列結(jié)構(gòu) 的指針。
struct rlimit {
rlim_t rlim_cur; /* soft limit: current limit */
rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */ };
在更改資源限制時(shí),須遵循下列3條規(guī)則。
(1)任何一個(gè)進(jìn)程都可將一個(gè)軟限制值更改為小于或等于其硬限 制值。
(2)任何一個(gè)進(jìn)程都可降低其硬限制值,但它必須大于或等于其 軟限制值。這種降低,對普通用戶而言是不可逆的。
(3)只有超級用戶進(jìn)程可以提高硬限制值。
常量RLIM_INFINITY指定了一個(gè)無限量的限制。
這兩個(gè)函數(shù)的 resource 參數(shù)取下列值之一。圖 7-15 顯示哪些資源限 制是由 Single UNIX Specification定義并由本書討論的4種UNIX系統(tǒng)實(shí)現(xiàn) 支持的。

RLIMIT_AS 進(jìn)程總的可用存儲空間的最大長度(字節(jié))。這影響 到 sbrk 函數(shù)(1.11節(jié))和mmap函數(shù)(14.8節(jié))。
RLIMIT_CORE core文件的最大字節(jié)數(shù),若其值為0則阻止創(chuàng)建core 文件。
RLIMIT_CPU CPU時(shí)間的最大量值(秒),當(dāng)超過此軟限制時(shí),向 該進(jìn)程發(fā)送SIGXCPU信號。
RLIMIT_DATA 數(shù)據(jù)段的最大字節(jié)長度。這是始化數(shù)據(jù)、非初始以及堆的總和。
RLIMIT_FSIZE 可以創(chuàng)建的文件的最大字節(jié)長度。當(dāng)超過此軟限制時(shí),則向該進(jìn)程發(fā)送SIGXFSZ信號。
RLIMIT_MEMLOCK 一個(gè)進(jìn)程使用mlock(2)能夠鎖定在存儲空間中
的最大字節(jié)長度。
RLIMIT_MSGQUEUE 進(jìn)程為POSIX消息隊(duì)列可分配的最大存儲字
節(jié)數(shù)。
RLIMIT_NICE 為了影響進(jìn)程的調(diào)度優(yōu)先級,nice值(8.16節(jié))可設(shè)
置的最大限制。
RLIMIT_NOFILE 每個(gè)進(jìn)程能打開的最多文件數(shù)。
RLIMIT_NPROC 每個(gè)實(shí)際用戶 ID 可擁有的最大子進(jìn)程數(shù)。更改此 限制將影響到sysconf函數(shù)在參數(shù)_SC_CHILD_MAX中返回的值(見2.5.4 節(jié))。
RLIMIT_SWAP 用戶可消耗的交換空間的最大字節(jié)數(shù)
RLIMIT_VMEM 這是RLIMIT_AS的同義詞。
資源限制影響到調(diào)用進(jìn)程并由其子進(jìn)程繼承。這就意味著,為了影
響一個(gè)用戶的所有后續(xù)進(jìn)程,需將資源限制的設(shè)置構(gòu)造在shell之中。確 實(shí),Bourne shell、GNU Bourne-again shell和Korn shell具有內(nèi)置的ulimit命令,C shell具有內(nèi)置limit命令。(umask和chdir函數(shù)也必須是shell內(nèi)置 的。)
打印由系統(tǒng)支持的所有資源當(dāng)前的軟限制和硬限制。 為了在各種實(shí)現(xiàn)上編譯該程序,我們已經(jīng)條件地包括了各種不同的資源 名。
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
rlimit my_rlimit;
int main() {
/*----------------------------------- get cpu rlimit ----------------------------------------*/
int ret = getrlimit(RLIMIT_CPU, &my_rlimit);
printf("cpu rlimit: (%llu) (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
/*----------------------------------- get nire rlimit ----------------------------------------*/
ret = getrlimit(RLIMIT_AS, &my_rlimit);
printf("AS rlimit: (%llu) (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
/*----------------------------------- get stack rlimit ----------------------------------------*/
ret = getrlimit(RLIMIT_STACK, &my_rlimit);
printf("stack rlimit: (%llu) (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
return 0;
}

也可以通過系統(tǒng)命令行來修改硬上限
- linux可以通過ulimit命令查看棧上限和設(shè)置上限
ulimit -a 查看進(jìn)程所有資源上限
ulimit -s xx 修改棧上限
7.12 小節(jié)
理解UNIX系統(tǒng)環(huán)境中C程序的環(huán)境是理解UNIX系統(tǒng)進(jìn)程控制特性 的先決條件。本章說明了一個(gè)進(jìn)程是如何啟動和終止的,如何向其傳遞 參數(shù)表和環(huán)境。雖然參數(shù)表和環(huán)境都不是由內(nèi)核進(jìn)行解釋的,但內(nèi)核起 到了從exec的調(diào)用者將這兩者傳遞給新進(jìn)程的作用。
本章也說明了C程序的典型存儲空間布局,以及一個(gè)進(jìn)程如何動態(tài) 地分配和釋放存儲空間。詳細(xì)地了解用于維護(hù)環(huán)境的一些函數(shù)是有意義 的,因?yàn)樗鼈兩婕按鎯臻g分配。本章也介紹了setjmp 和 longjmp 函 數(shù),它們提供了一種在進(jìn)程內(nèi)非局部轉(zhuǎn)移的方法。最后介紹了各種實(shí)現(xiàn) 提供的資源限制功能。