Unix 環境高級編程:進程環境

原文連接:blog,轉載註明來源便可。
本文代碼:Githubhtml

本文結構

進程的環境 
    ├── 執行程序:main 函數
    ├── 終止進程
    ├── 命令行參數
    ├── 進程的環境表
    ├── 進程的內存分佈
    ├── 進程間的共享庫
    ├── 內存分配
    ├── 環境變量
    ├── setjmp 與 longjmp 函數
    └── getrlimit 與 setrlimit 函數

執行程序:main 函數

定義

main() 是 C 程序的主函數,是程序執行的入口,Golang 與之相似,C99 標準對 main 的 2 種正確的定義:linux

// 無參數
int main(void) {
    return 0;
}

// 有參數
int main(int argc, char *argv[]) {
    return 0;
}

// main 還可有第三個參數 char *ecvp[],用於存儲運行環境的環境變量

爲提升程序的可移植性,避免使用下邊對 main 定義的形式:git

// 沒有歸入 C 的標準,大部分編譯器是不容許這麼寫的
void main() {
    // ...
}

參數值

// argc 即 argument counts,命令行中參數的個數,程序會在運行時自動統計
// argv 即 argument values,使用空格分隔,包含指向命令行參數值的指針。其中 argv[0] 爲程序名
int main(int argc, char *argv[]) {
    printf("argc: %d\n", argc);

    int i;
    for (i = 0; i < argc; i++) {             // 循環輸出所有的命令行參數
        printf("argv[%d]: %s\n", i, argv[i]);    // 命令行參數的類型全是字符串,只能使用 %s 輸出
    }
}

在程序中就能檢查並獲取運行參數了(a.out 至關於 Windows 上的 a.exe):程序員

image-20180419154529101

返回值

return 0; 返回給操做系統程序的執行狀態爲 0,表示正常退出,返回其餘值則認爲程序發生了錯誤。如:github

/* demo.c */
int main(void) {
    return 233.7;    // 返回值會被強制轉換,截斷爲整型 233
}

在 Unix 上使用 $? 來驗證程序的退出狀態:shell

image-20180419153037368

說明:在 shell 中執行 cc demo.c && ./a.outa.out 做爲 shell 的子進程運行,在退出時將 0 返回給了 shell,故能使用 echo $? 查看其退出狀態。數組

終止進程

終止的 8 種方式

5 種正常終止

main() 中 return
exit()
_exit() 或 _Exit()
多線程程序中,最後一個線程從其 main() 中 return
多線程程序中,從最後一個線程調用 pthread_exit()
// ISOC 標準
#include<stdlib.h>
void exit(int status);    // 正常結束當前正在執行的進程,將 status 返回給父進程,關閉進程打開的文件
            // 關閉文件(I/O流)以前,會調用 fclose 將緩衝區數據寫回該文件(I/O流 )
void _Exit(int status);    // 馬上結束當前進程,返回 status 並關閉打開的文件,可是不處理緩衝區


// POSIX 標準
#include<unistd.h>
void _exit(int status); // 同 _Exit() // 遵循的標準不同,在不一樣的頭文件中

3 種異常終止

abort()
接到一個信號
多線程的程序中,最後一個線程對取消請求做出響應        // 多線程部分在第 12 章

退出狀態

狀態範圍

// 退出狀態碼爲 0 ~ 255,超出後則返回 n % 256
int main(void) {
    exit(257);        // 返回 1
}

狀態不肯定的三種狀況

// 調用上邊三個函數時無 status 參數
int main(void) {
    exit();        // c99 標準的編譯器會直接報錯:too few arguments to function call
}


// main 沒有聲明返回值是整型
float main(void) {
    exit(233);        // 第一次返回 233,以後均返回 0
}

// main 執行了無返回值的 return 語句
void main(void) {
    return ;        // 第一次返回 176,以後均返回 0
}

atexit() 函數

#include<stdlib.h>
int atexit(void (*func) (void));    // 註冊的函數參數和返回值均爲 void

atexit() 註冊的函數稱爲 exit handler,在進程退出時會被 exit() 自動調用後再清理緩衝區。有 2 個特色:多線程

  • 先註冊的後調用:相似於 Golang 的 defer 語句,handlers 的調用也是棧順序的
  • 屢次註冊同一函數,依舊會被執行屢次

特性:

int main(void) {

    if (atexit(myExit2) != 0) {
        err_sys("can't register myExit2");      // 終止函數先註冊後調用
    }
    if (atexit(myExit1) != 0) {
        err_sys("can't register myExit1");
    }
    if (atexit(myExit1) != 0) {                 // 終止程序登記一次就會被調用一次
        err_sys("can't register myExit1");
    }

    printf("main is done\n");
    return 0;
}


static void myExit1(void) {
    printf("first exit handler\n");
}

static void myExit2(void) {
    printf("second exit handler\n");
}

運行:函數

image-20180420101734467

限制:

書上說一個進程使用 atexit() 最多註冊 32 個清理函數,但後來的操做系統有所差別,使用 sysconf 查看這個限制值:學習

#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("%ld", sysconf(_SC_ATEXIT_MAX));    // 能註冊 2147483647 個
}

差距太大了,因而我在 macOS 上使用 for 循環驗證了一下:

int main(void) {
    for (int i = 0; i < 214748367; i++) {
        if (atexit(myExit) != 0) {        // 終止程序登記一次就會被調用一次
            err_sys("can't register myExit1");
        }
    }
    return 0;
}


static void myExit(void) {
    printf("exit handler\n");
}

程序能正確調用,只是內存佔用會飆升233:

image-20180420100952293

C 程序的啓動與終止流程:

  • 內核執行程序的惟一辦法是調用 exec
  • 進程主動退出的惟一辦法是調用 exit()_Exit()_exit()

    image-20180420095014936

命令行參數

在 main() 函數的參數值已討論,不過遍歷命令行參數的結束條件還有另外一種方式:

// for (i = 0; i < argc; i++) {         // 循環輸出所有的命令行參數
for (i = 0; argv[i] != NULL; i++) {     //  POSIX 和 ISO C 標準都要求 argv[argc] 是空指針
    printf("argv[%d]: %d\n", i, argv[i]);
}

進程的環境表

ecvp 參數

環境參數是name=value 格式的字符串,能經過第三個參數 char *ecvp[] 接收到環境表:

int main(int argc, char *argv[], char *ecvp[]) {
    for (int i = 0; ecvp[i] != NULL; i++) {     //  POSIX 要求 argv[argc] 是空指針
        printf("ecvp[%d]: %s\n", i, argv[i]);
    }
}

輸出的環境參數:

ecvp[8]: LANG=zh_CN.UTF-8
ecvp[9]: PWD=/Users/wuyin/C/apue
ecvp[10]: SHELL=/bin/zsh
...
ecvp[16]: HOME=/Users/wuyin
ecvp[18]: USER=wuyin

environ 全局變量

環境表與命令行參數表同樣,也是字符指針數組,指針指向各環境參數。其地址存儲在全局變量 environ 中:

#include <stdio.h>
#include <unistd.h>        

extern char **environ;    // 使用來自 unistd.h 的外部變量 environ,類型是指向指針的指針

int main(int argc, char *argv[], char *ecvp[]) {
    char **env = environ;

    while (*env != '\0') {    // 環境參數字符串以 NULL 結尾
        printf("%s\n", *env);
        env++;
    }
    return 0;
}

進程的內存分佈

image-20180420111106632

常駐內存:從進程開始到退出一直存在,使用常量地址訪問。

靜態區域

正文段

  • 圖中的 .text
  • 內容:程序的機器指令
  • 共享的:同時運行多個 shell,開始運行時都執行同一代碼段;只讀的:避免被篡改

只讀數據段

  • 圖中 .rodata
  • 內容:程序中不會修改的數據(常量),好比字符串

已初始化數據段

  • 圖中的 .data
  • 內容:程序中初始化的數據,好比被賦初值的全局變量、靜態變量

未初始化數據段

  • 圖中的 .bss
  • 內容:程序中沒有初始化的數據:好比僅聲明,使用默認零值的變量

動態區域

  • 用於動態內存分配
  • 通常由程序員手動分配 malloc 和 釋放 free

  • 存放:臨時數據:臨時變量、調用函數時須要保存的數據等
  • 函數在遞歸調用時,會新開一個棧來存儲自身的變量集,因此變量互不影響

進程間的共享庫

image-20180420152353111

參考 阮一峯:編譯器的工做過程

在 link 連接階段,C 程序引用的庫分爲 2 種:

靜態庫 *.a、*.lib:外部函數庫添加到可執行文件,體積大,但適用性更高

動態庫(共享庫)*.so、*.dll:外部函數庫只在運行時動態引用,體積更小,但適用性更低

內存分配

動態內存分配函數

#include<stdlib.h>    // 定義

// 分配 size 字節大小的內存區域
// 成功則返回內存地址,失敗返回 NULL
void *malloc(size_t size);    


// 分配 nobj 個長度爲 size 字節的內存區域,並將每個字節都初始化爲 0 
// 成功則返回內存地址,失敗返回 NULL
void *calloc(size_t nobj, size_t size);


// 爲 ptr 指向的空間分配新的 newsize 字節的內存
// ptr 必須指向動態分配的內存,即三個 *alloc() 函數的返回值
// ptr 爲 NULL:realloc() 與 malloc() 相同
// newsize 爲 0: ptr 指向的空間會被釋放,返回 NULL,相似 free()
//         更大: 存儲區域有足夠的空間可擴充,則擴充後直接返回 ptr
//          存儲區域沒有足夠的空間可擴充,複製 ptr 指向區域的數據到更大的內存空間
// 相似 Golang 中 slice 在 len 接近 cap 以後進行內存從新分配的操做
void *realloc(void *ptr, size_t newsize);

注意

返回值類型

返回值都是 void * ,不是沒有返回值或返回空指針,而是返回通用指針,指向的類型未知,相似於 Go 中的 interface{} 類型。在使用時,須要將返回的 void * 進行強制類型轉換,方便存儲數據

內存釋放

必須使用 void free (void* ptr); 來手動釋放分配的內存,若是忘記釋放的內存累計過多,進程將可能出現內存泄漏

對一塊內存只能釋放一次,調用屢次 free() 將出錯

使用示例

#include <stdio.h>
#include <stdlib.h>

int main() {
    int n = 2;
    int *buf1 = (int *) malloc(n);    // 強制將 void* 轉換爲 int*
    if (buf1 == NULL) {           // 檢查是否分配成功
        exit(1);
    }
    for (int i = 0; i < n; i++)
        printf("buf1[%d]: %d\n", i, buf1[i]);    // malloc 分配的空間值的值是未知的

    int *buf2 = (int *) calloc(n, sizeof(int));    // calloc 分配的空間有默認值 0 
    for (int i = 0; i < n; i++)
        printf("buf2[%d]: %d\n", i, buf2[i]);

    free(buf1);
    // free(buf);  
    // malloc: *** error for object 0x7fd9b4d000e0: pointer being freed was not allocated
    // 沒有 free(buf2) 則可能發生內存泄漏
    return 0;
}

運行:

image-20180420163216349

環境變量

查詢環境變量的值

#include<stdlib.h>
char *getenv(const char* name);        // 環境變量 name 存在則返回值的指針,不存在則返回 NULL

設置環境變量的值

#include<stdlib.h>

// str 是 name=value 格式的參數,用於新增或覆蓋 name
// 執行成功返回 0,失敗返回 -1
int putenv(char *str);    


// 設置 name 環境變量的值爲 value
// 若 rewrite == 0 則新增或不覆蓋
//            != 0 則新增或覆蓋
int setenv(const char *name, const char *value, int rewrite);


// 刪除 name 環境變量,不存在也不會報錯
int unsetenv(const char *name);

參考前邊環境表的 environ 全局變量,操做環境參數時更推薦使用上邊的函數。

setjmp 與 longjmp 函數

函數內跳轉

在 C 中使用 goto 在函數內部(棧中)跳轉,可往前也可日後跳。不過爲了提升代碼的可維護性,應儘可能少使用。除非你明確要使用它來跳出深層次的循環,那也不錯。

函數間跳轉

在 C 中使用 setjmp()longjmp() 在函數之間(棧之間)跳轉:

#include<setjmp.h>    // 定義

// env 參數的類型是 jmp_buf,用於標識當前進程狀態的錨點
int setjmp(jmp_buf env);            


// 使用與 setjmp 對應的 env 參數,調用時直接跳轉到 env 處
// 一個 setjmp 可對應多個 longjmp,使用 val 標識是從哪裏回退的
// val 便是 setjmp 的返回值
void longjmp(jmp_buf env, int val);

示例

#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>

jmp_buf saved_state;

void myLongjmp();

int main(void) {
    printf("設置 setjmp 的錨點\n");
    int ret_code = setjmp(saved_state);
    if (ret_code == 1) {
        printf("結束序號爲 1 的跳轉\n");
        return 0;
    }
    int *flag = (int *) calloc(1, sizeof(int));
    myLongjmp();    // 直接跳轉到第 11 行
}

void myLongjmp() {
    printf("準備開始序號爲 1 的跳轉\n");
    longjmp(saved_state, 1);
}

效果:

image-20180420190004608

內存泄漏

使用 Valgrind 作內存泄漏檢測,結果顯示有 4 字節的內存 lost,便是 16 行分配的內存沒有釋放:image-20180420193100439

setjmp()longjmp() 之間分配的內存,在跳轉後直接就廢棄了,可能會所以發生內存溢出。

和 goto 同樣,除非你知道本身在作什麼,不然應儘可能避免使用。

getrlimit 與 setrlimit 函數

系統對進程能調用的資源有限制,可以使用 getrlimit() 來查看、setrlimit() 來修改

函數原型

#include<sys/resource.h>    // 定義

// resource 是標識資源類型的常量
// rlimit 的定義
struct rlimit {
    rlim_t    rlim_cur; // current (soft) limit    // 當前軟連接的限制值
    rlim_t    rlim_max; // maximum value for rlim_cur    // 硬連接的限制值
};

// 修改爲功返回 0,失敗返回非 0
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, struct rlimit *rlptr);

可修改的資源值

參數值 參數說明
RLIMIT_AS 進程可以使用的最大存儲
RLIMIT_NOFILE 進程可打開的最大文件數
RLIMIT_STACK 進程的棧的最大長度

更多請參考:手冊

總結

開始學習 APUE 就卡在了第三章,因而從第七章熟悉的 main() 開始學起,發現 C 和 Golang 真的有千絲萬縷的聯繫,好比 atexit()defer func(){},另外還有進程相關的內存分佈、內存分配都值得深刻學習,後邊依舊在本篇筆記中補充。

計劃下週 4.27 前更新第八章筆記 :)

相關文章
相關標籤/搜索