UNIX高級(jí)環(huán)境編程(8)進(jìn)程環(huán)境(PRocess Environment)- 進(jìn)程的啟動(dòng)和退出、內(nèi)存布局、環(huán)境變量列表在學(xué)習(xí)進(jìn)程控制相關(guān)知識(shí)之前,我們需要了解一個(gè)單進(jìn)程的運(yùn)行環(huán)境。
本章我們將了解一下的內(nèi)容:
- 程序運(yùn)行時(shí),main函數(shù)是如何被調(diào)用的;
- 命令行參數(shù)是如何被傳入到程序中的;
- 一個(gè)典型的內(nèi)存布局是怎樣的;
- 如何分配內(nèi)存;
- 程序如何使用環(huán)境變量;
- 程序終止的各種方式;
- 跳轉(zhuǎn)(longjmp和setjmp)函數(shù)的工作方式,以及如何和棧交互;
- 進(jìn)程的資源限制
?
1 main函數(shù)main函數(shù)聲明:
int main (int argc, char *argv[]);
參數(shù)說(shuō)明:
- argc:命令行參數(shù)個(gè)數(shù)
- argv:指向參數(shù)列表數(shù)組的指針
main函數(shù)啟動(dòng)前:
- C程序由內(nèi)核執(zhí)行,通過(guò)系統(tǒng)調(diào)用exec;
- main函數(shù)調(diào)用前,執(zhí)行指定的啟動(dòng)路徑(start-up routine);
- 可執(zhí)行文件認(rèn)為此地址為程序的啟動(dòng)地址,該地址由鏈接器指定;
- 啟動(dòng)路徑從內(nèi)核獲取參數(shù)列表和環(huán)境變量,使得main函數(shù)可以在稍后被調(diào)用時(shí)可以獲取這些變量。
?
2 進(jìn)程終止一共有8中終止進(jìn)程的方式,5種正常終止和3種異常終止。
5種正常終止:
- 從main函數(shù)返回;
- 調(diào)用exit;
- 調(diào)用_exit或_Exit;
- 最后一個(gè)線程返回;
- 最后一個(gè)線程調(diào)用pthread_exit。
3種異常終止:
- 調(diào)用abort;
- 接收到一個(gè)信號(hào);
- 最后一個(gè)線程應(yīng)答或者一個(gè)接收到一個(gè)退出請(qǐng)求
啟動(dòng)地址(start-up routine)同樣也是main函數(shù)的返回地址。
要獲取該地址,可以通過(guò)以下的方式:
exit (main(argc, argv));
?
退出函數(shù)函數(shù)聲明:
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
函數(shù)細(xì)節(jié):
- _exit和_Exit立刻返回到內(nèi)核;
- exit函數(shù)返回內(nèi)核前會(huì)進(jìn)行一些清理環(huán)境工作;
返回一個(gè)整數(shù)和調(diào)用exit函數(shù),并傳入該整數(shù)的作用是相同的:
exit(0);
return 0;
?
atexit函數(shù)函數(shù)聲明
#include <stdlib.h>
int atexit(void (*func)(void));
函數(shù)細(xì)節(jié)
- 每個(gè)進(jìn)程可以注冊(cè)32個(gè)函數(shù),這些函數(shù)可以在主函數(shù)調(diào)用exit時(shí)自動(dòng)被調(diào)用
- 通過(guò)atexit注冊(cè)的退出時(shí)處理函數(shù)稱(chēng)為退出句柄(exit handlers)
- 這些退出句柄的調(diào)用順序?yàn)樽?cè)時(shí)的相反順序
- exit函數(shù)第一次調(diào)用退出句柄時(shí),會(huì)關(guān)閉所有打開(kāi)的流
- 如果主程序調(diào)用了exec系列函數(shù),則所有注冊(cè)的退出句柄都會(huì)被清空
?
程序啟動(dòng)和終止流程圖?
Example:#include "apue.h"
?
static void my_exit1(void);
static void my_exit2(void);
?
int
main(void)
{
? ? if (atexit(my_exit2) != 0)
? ? ? ? err_sys("can't register my_exit2");
?
? ? if (atexit(my_exit1) != 0)
? ? ? ? err_sys("can't register my_exit1");
? ? if (atexit(my_exit1) != 0)
? ? ? ? err_sys("can't register my_exit1");
?
? ? printf("main is done/n");
? ? return(0);
}
?
static void
my_exit1(void)
{
? ? printf("first exit handler/n");
}
?
static void
my_exit2(void)
{
? ? printf("second exit handler/n");
}
?執(zhí)行結(jié)果:

?
3 命令行參數(shù)Example:#include "apue.h"
?
int
main(int argc, char *argv[])
{
? ? int ? ? i;
?
? ? for (i = 0; i < argc; i++)? ? ? /* echo all command-line args */
? ? ? ? printf("argv[%d]: %s/n", i, argv[i]);
? ? exit(0);
}
執(zhí)行結(jié)果:
?
?
4 環(huán)境變量列表每個(gè)程序會(huì)接受一個(gè)環(huán)境變量列表,該列表是一個(gè)數(shù)組,由一個(gè)數(shù)組指針指向,該數(shù)組指針類(lèi)型為:
extern char **environ;
例如,如果環(huán)境變量里有5個(gè)字符串(C風(fēng)格字符串),如下圖所示:

5 C程序的內(nèi)存布局典型的C程序的內(nèi)存布局如下圖所示:

上圖說(shuō)明:
- 文本段(Text Segment),保存CPU將要執(zhí)行的機(jī)器指令。文本段是可共享的,所以某個(gè)程序多次執(zhí)行時(shí),對(duì)應(yīng)的文本段只需要在內(nèi)存中存有一份拷貝。文本段是只讀的(read-only),防止程序的指令被修改。
- 已初始化數(shù)據(jù)段(initialized data segment),保存程序中被初始化的全局變量(定義在任何函數(shù)之外)。例如:int maxcount = 99; 全局變量變量maxcount被保存在初始化數(shù)據(jù)段。
- 未初始化數(shù)據(jù)段(uninitialized data segment),也被稱(chēng)為BSS(block started by symbol),這個(gè)段中的數(shù)據(jù)在程序執(zhí)行之前被內(nèi)核初始化為0或者null。;例如定義一個(gè)全局變量(定義在任何函數(shù)之外),long sum[1000]; ?該變量保存在未初始化數(shù)據(jù)段中。
- 棧(Stack):存儲(chǔ)臨時(shí)變量,函數(shù)相關(guān)信息。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),返回地址、調(diào)用者相關(guān)信息(如寄存器信息)會(huì)被保存在棧中。該被調(diào)用的函數(shù)會(huì)在棧上分配一部分空間保存它的臨時(shí)變量。函數(shù)的遞歸調(diào)用也是應(yīng)用這個(gè)原理。每一次函數(shù)調(diào)用自己,都會(huì)保存當(dāng)前函數(shù)的信息,然后再棧上開(kāi)辟一個(gè)新的空間用于保存該次函數(shù)的信息,和以前的函數(shù)并沒(méi)有影響。
- 堆(Heap):動(dòng)態(tài)內(nèi)存分配位置。堆的位置位于未初始化數(shù)據(jù)段和棧的中間。
?
6 內(nèi)存分配(Memory Allocation)有三個(gè)函數(shù)可以用于內(nèi)存分配:
- malloc:分配指定字節(jié)數(shù)的內(nèi)存,未初始化。
- calloc:分配指定數(shù)目的對(duì)象大小的內(nèi)存,內(nèi)存初始化為0;
- realloc:增加或減小之前分配的內(nèi)存。移動(dòng)舊內(nèi)存的內(nèi)容到新的更大的內(nèi)存塊,多余的部分內(nèi)存未初始化。
函數(shù)聲明:
#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);
函數(shù)細(xì)節(jié):
- 三個(gè)函數(shù)返回的內(nèi)存指針一定是內(nèi)存對(duì)齊的,這樣可以用來(lái)保存于不同的對(duì)象;
- free函數(shù)用于釋放ptr指向的內(nèi)存,被分配的內(nèi)存放入內(nèi)存池中用于下次的內(nèi)存分配;
- realloc函數(shù)用于改變之前分配的內(nèi)存的大小。比如運(yùn)行時(shí)我們申請(qǐng)了一段內(nèi)存用于存儲(chǔ)512個(gè)元素的數(shù)組,后來(lái)發(fā)現(xiàn)內(nèi)存大小不夠,這時(shí)可以調(diào)用realloc。如果操作系統(tǒng)發(fā)現(xiàn)在當(dāng)前內(nèi)存的后面有足夠的內(nèi)存,則直接分配多余的內(nèi)存到當(dāng)前內(nèi)存中,然后返回傳入的指針(即直接擴(kuò)展內(nèi)存)。但是如果當(dāng)前內(nèi)存后面沒(méi)有足夠大小的空間,則系統(tǒng)重新分配一個(gè)足夠大的內(nèi)存,將舊內(nèi)存塊中得內(nèi)容拷貝到新內(nèi)存塊中,然后返回新內(nèi)存的地址。
- 內(nèi)存分配函數(shù)使用系統(tǒng)調(diào)用sbrk來(lái)實(shí)現(xiàn)。該系統(tǒng)調(diào)用的作用是擴(kuò)展進(jìn)程的堆。
- 一般實(shí)際分配的內(nèi)存塊都比請(qǐng)求的要大,多出來(lái)的部分用來(lái)存儲(chǔ)內(nèi)存塊大小、指向下一內(nèi)存塊的指針等信息。寫(xiě)覆蓋信息記錄區(qū)的錯(cuò)誤是非常隱蔽而且嚴(yán)重的。
?
7 環(huán)境變量(Environment Variable)環(huán)境變量的字符串形式:
name=value
?內(nèi)核不關(guān)注環(huán)境變量,各種應(yīng)用才會(huì)使用環(huán)境變量。
獲取環(huán)境變量值使用函數(shù)getenv。
#include <stdlib.h>
char* getenv(const char* name);
// Returns: pointer to value associated with name, NULL if not found
修改環(huán)境變量的函數(shù):
#include <stdlib.h>
int putenv(char* str);
int setenv(const char* name, const char* value, int rewrite);
int unsetenv(const char* name);
?函數(shù)細(xì)節(jié):
- 函數(shù)putenv傳入一個(gè)字符串,形式為name=value,加入到環(huán)境變量列表中。如果name已經(jīng)存在,先刪除舊的定義。
- 函數(shù)setenv傳入一個(gè)name和一個(gè)value,如果name已經(jīng)存在,則參數(shù)rewrite決定是否覆蓋舊的定義,如果rewrite為非零,則會(huì)覆蓋舊的定義。
- 函數(shù)unsetenv刪除name的定義,如果name不存在,也不報(bào)錯(cuò)。?
?修改環(huán)境變量列表的過(guò)程是一件很有趣的事情
從上面的C程序內(nèi)存布局圖中可以看到,環(huán)境變量列表(保存指向環(huán)境變量字符串的一組指針)保存在棧的上方內(nèi)存中。
在該內(nèi)存中,刪除一個(gè)字符串很簡(jiǎn)單。我們只需要找到該指針,刪除該指針和該指針指向的字符串。
但是增加或修改一個(gè)環(huán)境變量困難得多。因?yàn)榄h(huán)境變量列表所在的內(nèi)存往往在進(jìn)程的內(nèi)存空間頂部,下面是棧。所以該內(nèi)存空間無(wú)法被向上或者向下擴(kuò)展。
所以修改環(huán)境變量列表的過(guò)程如下所述:
- 如果我們修改一個(gè)已經(jīng)存在的name:
- 如果新的value的大小比已經(jīng)存在的value小或者相當(dāng),直接覆蓋舊的value;
- 如果新的value的大小比已經(jīng)存在的value大,則我們必須為新的value malloc一個(gè)新的內(nèi)存空間,拷貝新value到該內(nèi)存中,替換指向舊value的指針為指向新value的指針。
- 如果我們新增一個(gè)環(huán)境變量:
- 首先我們需要調(diào)用malloc為字符串name=value分配空間,拷貝該字符串到目標(biāo)內(nèi)存中;
- 如果這是我們第一次添加環(huán)境變量,我們需要調(diào)用malloc分配一個(gè)新的空間,拷貝老的環(huán)境量列表到新的內(nèi)存中,并在列表后新增目標(biāo)環(huán)境變量。然后我們?cè)O(shè)置environ指向新的環(huán)境變量列表。
- 如果這不是我們第一次新增環(huán)境變量,則我們只需要realloc多分配一個(gè)環(huán)境變量的空間,新增的環(huán)境變量保存在列表尾部,列表最后仍然是一個(gè)null指針。
?
小結(jié)本篇介紹了進(jìn)程的啟動(dòng)和退出、內(nèi)存布局、環(huán)境變量列表和環(huán)境變量的修改。
下一篇將接著學(xué)習(xí)四個(gè)函數(shù)setjmp、longjmp、getrlimit和setrlimit。
?
?
參考資料:
《Advanced Programming in the UNIX Envinronment 3rd》