Skip to main content

第七章: 进程环境

本章主要讲:

  1. 当程序执行时, main 函数是如何被调用的;
  2. 命令行参数如何传递给程序;
  3. 典型的存储空间布局;
  4. 如何分配额外存储空间;
  5. 进程如何使用环境变量;
  6. 进程的各种不同终止方式;
  7. 共享库的简介
  8. longjmp 和 setjmp 以及它与栈的交互;
  9. 进程的资源限制

程序启动: main 函数的调用过程

C 程序总是从 main 函数开始执行. main 函数的原型是:

int main(int argc, char* argv[]);

其中 argc 是命令行参数个数, argv 是指向参数的各个指针构成的数组.

内核执行 C 程序时的流程如下所示:

程序的退出方式

共有 8 种退出方式:

  1. main 函数返回
  2. 主动调用 exit
  3. 主动调用 _exit_Exit
  4. 从最后一个线程返回
  5. 在最后一个线程中调用 pthread_exit
  6. 调用 abort 的异常退出
  7. 收到一个信号(signal)
  8. 最后一个线程对取消请求进行响应

在程序中, 可以使用 atexit 注册退出前需要执行的函数.

命令行参数的传递

命令行参数是通过启动例程传递给进程的, 另外其父进程也可传递命令行参数(比如 shell).

ISO C 和 POSIX.1 还保证了 argv[argc] 肯定是 null 指针.

环境变量

每个进程都被传入当前的环境变量列表, 它也是一个字符串指针数组, 每个指针指向的是 null 结尾的 C 字符串. 这个数组被保存在一个全局变量中, 它的声明如下:

extern char **environ;

它里面指向的每个字符串都按 key=value 的形式给出, 比如 HOME=/home/username\0.

不过如果要读写某个环境变量的值, 一般的做法是调用 getenvputenv 函数完成, 而非直接使用 environ 全局变量. 但如果要遍历所有的环境变量, 就必须用到 environ.

C 进程的典型内存布局

由于历史原因, C 进程内存由如下部分构成:

  • Text segment: 即文本段, 包含编译后的机器指令. 并且它是只读的, 且可共享, 这样频繁运行程序的文本段可以只保留一份副本在内存中(比如文本编辑器, shell, 编译器等).
  • Initialized data segment: 即已初始化数据段, 通常也简称为 data segment. 包含程序中已进行初始化的变量, 比如在任何函数外声明的变量(全局的或文件内全局的) int max = 99;, max 变量就存放在 data segment 中.
  • Uninitialized data segment: 即未初始化数据段, 通常简写为 bss. 在其中的变量(仅全局的或文件内部全局的变量)由内核初始化为其默认值(0 或 null). 比如全局变量 sum long 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 的确占用了空间.

共享库

待续...