《Unix 环境高级编程》 第七章读书笔记,转载请注明来源。
本文代码:Github
本文结构 1 2 3 4 5 6 7 8 9 10 11 进程的环境 ├── 执行程序:main 函数 ├── 终止进程 ├── 命令行参数 ├── 进程的环境表 ├── 进程的内存分布 ├── 进程间的共享库 ├── 内存分配 ├── 环境变量 ├── setjmp 与 longjmp 函数 └── getrlimit 与 setrlimit 函数
执行程序:main 函数 定义 main()
是 C 程序的主函数,是程序执行的入口,Golang 与之类似,C99 标准对 main 的 2 种正确的定义:
1 2 3 4 5 6 7 8 9 10 11 int main (void ) { return 0 ; } int main (int argc, char *argv[]) { return 0 ; }
为提高程序的可移植性,避免使用下边对 main 定义的形式:
参数值 1 2 3 4 5 6 7 8 9 10 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]); } }
在程序中就能检查并获取运行参数了(a.out 相当于 Windows 上的 a.exe):
返回值 return 0;
返回给操作系统程序的执行状态为 0,表示正常退出,返回其他值则认为程序发生了错误。如:
1 2 3 4 int main (void ) { return 233.7 ; }
在 Unix 上使用 $?
来验证程序的退出状态:
说明:在 shell 中执行 cc demo.c && ./a.out
后 a.out
作为 shell 的子进程运行,在退出时将 0 返回给了 shell,故能使用 echo $?
查看其退出状态。
终止进程 终止的 8 种方式 5 种正常终止 1 2 3 4 5 main() 中 return exit() _exit() 或 _Exit() 多线程程序中,最后一个线程从其 main() 中 return 多线程程序中,从最后一个线程调用 pthread_exit()
1 2 3 4 5 6 7 8 9 10 #include <stdlib.h> void exit (int status) ; void _Exit(int status); #include <unistd.h> void _exit(int status);
3 种异常终止 1 2 3 abort() 接到一个信号 多线程的程序中,最后一个线程对取消请求作出响应 // 多线程部分在第 12 章
退出状态 状态范围 1 2 3 4 int main(void) { exit(257 ); }
状态不确定的三种情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main (void ) { exit (); } float main (void ) { exit (233 ); } void main (void ) { return ; }
atexit()
函数1 2 #include <stdlib.h> int atexit (void (*func) (void )) ;
atexit()
注册的函数称为 exit handler,在进程退出时会被 exit()
自动调用后再清理缓冲区。有 2 个特点:
先注册的后调用:类似于 Golang 的 defer 语句,handlers 的调用也是栈顺序的
多次注册同一函数,依旧会被执行多次
特性: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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
查看这个限制值:
1 2 3 4 5 6 #include <stdio.h> #include <unistd.h> int main (void ) { printf ("%ld" , sysconf(_SC_ATEXIT_MAX)); }
差距太大了,于是我在 macOS 上使用 for 循环验证了一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 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:
C 程序的启动与终止流程:
内核执行程序的唯一办法是调用 exec
进程主动退出的唯一办法是调用 exit()
、_Exit()
、_exit()
命令行参数 在 main() 函数的参数值已讨论,不过遍历命令行参数的结束条件还有另一种方式:
1 2 3 4 for (i = 0 ; argv[i] != NULL ; i++) { printf ("argv[%d]: %d\n" , i, argv[i]); }
进程的环境表 ecvp
参数环境参数是name=value 格式的字符串,能通过第三个参数 char *ecvp[]
接收到环境表:
1 2 3 4 5 int main (int argc, char *argv[], char *ecvp[]) { for (int i = 0 ; ecvp[i] != NULL ; i++) { printf ("ecvp[%d]: %s\n" , i, argv[i]); } }
输出的环境参数:
1 2 3 4 5 6 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
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #include <unistd.h> extern char **environ; int main (int argc, char *argv[], char *ecvp[]) { char **env = environ; while (*env != '\0' ) { printf ("%s\n" , *env); env++; } return 0 ; }
进程的内存分布
常驻内存:从进程开始到退出一直存在,使用常量地址访问。
静态区域 正文段
图中的 .text
内容:程序的机器指令
共享的:同时运行多个 shell,开始运行时都执行同一代码段;只读的:避免被篡改
只读数据段
图中 .rodata
内容:程序中不会修改的数据(常量),比如字符串
已初始化数据段
图中的 .data
内容:程序中初始化的数据,比如被赋初值的全局变量、静态变量
未初始化数据段
图中的 .bss
内容:程序中没有初始化的数据:比如仅声明,使用默认零值的变量
动态区域 堆
用于动态内存分配
一般由程序员手动分配 malloc 和 释放 free
栈
存放:临时数据:临时变量、调用函数时需要保存的数据等
函数在递归调用时,会新开一个栈来存储自身的变量集,所以变量互不影响
进程间的共享库
参考 阮一峰:编译器的工作过程
在 link 链接阶段,C 程序引用的库分为 2 种:
静态库 *.a、*.lib:外部函数库添加到可执行文件,体积大,但适用性更高
动态库(共享库)*.so、*.dll:外部函数库只在运行时动态引用,体积更小,但适用性更低
内存分配 动态内存分配函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdlib.h> void *malloc (size_t size) ; void *calloc (size_t nobj, size_t size) ;void *realloc (void *ptr, size_t newsize) ;
注意 返回值类型 返回值都是 void *
,不是没有返回值或返回空指针,而是返回通用指针,指向的类型未知,类似于 Go 中的 interface{} 类型。在使用时,需要将返回的 void *
进行强制类型转换,方便存储数据
内存释放 必须使用 void free (void* ptr);
来手动释放分配的内存,如果忘记释放的内存累计过多,进程将可能出现内存泄漏
对一块内存只能释放一次,调用多次 free()
将出错
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> int main () { int n = 2 ; int *buf1 = (int *) malloc (n); if (buf1 == NULL ) { exit (1 ); } for (int i = 0 ; i < n; i++) printf ("buf1[%d]: %d\n" , i, buf1[i]); int *buf2 = (int *) calloc (n, sizeof (int )); for (int i = 0 ; i < n; i++) printf ("buf2[%d]: %d\n" , i, buf2[i]); free (buf1); return 0 ; }
运行:
环境变量 查询环境变量的值 1 2 #include <stdlib.h> char *getenv (const char * name) ;
设置环境变量的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdlib.h> int putenv (char *str) ; int setenv (const char *name, const char *value, int rewrite) ;int unsetenv (const char *name) ;
参考前边环境表的 environ 全局变量,操作环境参数时更推荐使用上边的函数。
setjmp 与 longjmp 函数 函数内跳转 在 C 中使用 goto 在函数内部(栈中)跳转,可往前也可往后跳。不过为了提高代码的可维护性,应尽量少使用。除非你明确要使用它来跳出深层次的循环,那也不错。
函数间跳转 在 C 中使用 setjmp()
与 longjmp()
在函数之间(栈之间)跳转:
1 2 3 4 5 6 7 8 9 10 #include <setjmp.h> int setjmp (jmp_buf env) ; void longjmp (jmp_buf env, int val) ;
示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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(); } void myLongjmp () { printf ("准备开始序号为 1 的跳转\n" ); longjmp(saved_state, 1 ); }
效果:
内存泄漏 使用 Valgrind 做内存泄漏检测,结果显示有 4 字节的内存 lost,即是 16 行分配的内存没有释放:
在 setjmp()
和 longjmp()
之间分配的内存,在跳转后直接就废弃了,可能会因此发生内存溢出。
和 goto 一样,除非你知道自己在做什么,否则应尽量避免使用。
getrlimit 与 setrlimit 函数 系统对进程能调用的资源有限制,可使用 getrlimit()
来查看、setrlimit()
来修改
函数原型 1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/resource.h> struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; }; 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 前更新第八章笔记 :)