第七章: 进程环境
本章主要讲:
- 当程序执行时, main 函数是如何被调用的;
- 命令行参数如何传递给程序;
- 典型的存储空间布局;
- 如何分配额外存储空间;
- 进程如何使用环境变量;
- 进程的各种不同终止方式;
- 共享库的简介
- longjmp 和 setjmp 以及它与栈的交互;
- 进程的资源限制
程序启动: main 函数的调用过程
C 程序总是从 main 函数开始执行. main 函数的原型是:
int main(int argc, char* argv[]);
其中 argc 是命令行参数个数, argv 是指向参数的各个指针构成的数组.
内核执行 C 程序时的流程如下所示:
程序的退出方式
共有 8 种退出方式:
- 从
main
函数返回 - 主动调用
exit
- 主动调用
_exit
或_Exit
- 从最后一个线程返回
- 在最后一个线程中调用
pthread_exit
- 调用
abort
的异常退出 - 收到一个信号(signal)
- 最后一个线程对取消请求进行响应
在程序中, 可以使用 atexit
注册退出前需要执行的函数.
命令行参数的传递
命令行参数是通过启动例程传递给进程的, 另外其父进程也可传递命令行参数(比如 shell).
ISO C 和 POSIX.1 还保证了 argv[argc]
肯定是 null
指针.
环境变量
每个进程都被传入当前的环境变量列表, 它也是一个字符串指针数组, 每个指针指向的是 null 结尾的 C 字符串. 这个数组被保存在一个全局变量中, 它的声明如下:
extern char **environ;
它里面指向的每个字符串都按 key=value
的形式给出, 比如 HOME=/home/username\0
.
不过如果要读写某个环境变量的值, 一般的做法是调用 getenv
和 putenv
函数完成, 而非直接使用 environ
全局变量. 但如果要遍历所有的环境变量, 就必须用到 environ
.
C 进程的典型内存布局
由于历史原因, C 进程内存由如下部分构成:
- Text segment: 即文本段, 包含编译后的机器指令. 并且它是只读的, 且可共享, 这样频繁运行程序的文本段可以只保留一份副本在内存中(比如文本编辑器, shell, 编译器等).
- Initialized data segment: 即已初始化数据段, 通常也简称为
data segment
. 包含程序中已进行初始化的变量, 比如在任何函数外声明的变量(全局的或文件内全局的)int max = 99;
, max 变量就存放在 data segment 中. - Uninitialized data segment: 即未初始化数据段, 通常简写为
bss
. 在其中的变量(仅全局的或文件内部全局的变量)由内核初始化为其默认值(0 或 null). 比如全局变量 sumlong sum[1000];
就被保存在 bss 中. - Stack: 即栈空间, 包含任何自动变量(本地变量)以及函数调用栈信息. 当函数 B 被函数 A 调用后, 栈上会保存 B 返回到 A 时需要的 A 函数的上下文信息, 比如寄存器信息等, 而后栈上会分配 B 所需的空间用于存放它的本地变量.
- Heap: 即堆空间, 其中存放动态分配内存的变量. 由于历史原因, 在进程地址空间中堆位于栈和 bss 中间.
典型的内存布局如下所示:
进程虚拟内存地址空间中, 栈是从高地址往低地址扩展的, 而堆是由低地址往高地址扩展. 而堆和栈中间的进程虚拟地址空间是非常巨大的.
实际上进程地址空间中还有一些其他的区域, 比如符号表, debug 信息, 动态库专有的链接表等. 这些额外的段区域并不会中进程运行时加载到内存中.
另外, 进程的 bss 中变量内容不会保存到程序文件中(仅由一些符号构成), 当进程启动时, 内核负责将 bss 中的数据初始化为 0 或 null.
可以使用 size
命令查看某个程序文件的空间组成情况, 比如 size /bin/sh
的输出如下所示:
$ size /bin/sh
text data bss dec hex filename
1313960 47744 44992 1406696 1576e8 /bin/sh
其中 dec 和 hex 是前面三列数据的 8 进制和 16 进制总和, 并非单独的段. 还可以发现, bss 的确占用了空间.
共享库
待续...