原文連接:blog,轉載註明來源便可。
本文代碼:Githubhtml
進程的環境 ├── 執行程序:main 函數 ├── 終止進程 ├── 命令行參數 ├── 進程的環境表 ├── 進程的內存分佈 ├── 進程間的共享庫 ├── 內存分配 ├── 環境變量 ├── setjmp 與 longjmp 函數 └── getrlimit 與 setrlimit 函數
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):程序員
return 0;
返回給操做系統程序的執行狀態爲 0,表示正常退出,返回其餘值則認爲程序發生了錯誤。如:github
/* demo.c */ int main(void) { return 233.7; // 返回值會被強制轉換,截斷爲整型 233 }
在 Unix 上使用 $?
來驗證程序的退出狀態:shell
說明:在 shell 中執行 cc demo.c && ./a.out
後 a.out
做爲 shell 的子進程運行,在退出時將 0 返回給了 shell,故能使用 echo $?
查看其退出狀態。數組
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() // 遵循的標準不同,在不一樣的頭文件中
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 個特色:多線程
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"); }
運行:函數
書上說一個進程使用 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:
exec
exit()
、_Exit()
、_exit()
在 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; }
常駐內存:從進程開始到退出一直存在,使用常量地址訪問。
參考 阮一峯:編譯器的工做過程
在 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; }
運行:
#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 全局變量,操做環境參數時更推薦使用上邊的函數。
在 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); }
效果:
使用 Valgrind 作內存泄漏檢測,結果顯示有 4 字節的內存 lost,便是 16 行分配的內存沒有釋放:
在 setjmp()
和 longjmp()
之間分配的內存,在跳轉後直接就廢棄了,可能會所以發生內存溢出。
和 goto 同樣,除非你知道本身在作什麼,不然應儘可能避免使用。
系統對進程能調用的資源有限制,可以使用 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 前更新第八章筆記 :)