《UNIX环境高级编程》读书笔记08: 进程环境
作者:andylin02
学习章节:第7章 进程环境
关键词: 进程环境、main函数、进程终止、环境变量、内存布局、setjmp/longjmp、资源限制
一、引言:理解进程环境,掌握进程控制的“前奏”
在上一章,我们学习了系统数据文件和信息,了解了如何访问系统的各种配置数据。从本章开始,我们将逐步深入到进程的世界。
理解 UNIX 系统环境中 C 程序的环境是理解 UNIX 系统进程控制特性的先决条件。 本章正是为此打基础:当执行一个程序时,其 main 函数是如何被调用的?命令行参数是如何传送给执行程序的?典型的存储空间布局是什么样子?如何分配另外的存储空间?进程如何使用环境变量?
本章主要回答以下核心问题:
main函数是如何被调用的?- 进程终止的方式有哪几种?
- 如何获取和操作环境变量?
- C 程序的存储空间是如何布局的?
- 如何使用
setjmp和longjmp进行非局部跳转? - 如何获取和设置进程的资源限制?
📌 学习目标:理解进程启动、终止的完整流程,掌握进程的内存布局,能够使用环境变量和资源限制函数。为下一章“进程控制”打好理论基础。
二、核心内容精讲
2.1 main 函数与启动例程
C 程序总是从 main 函数开始执行。main 函数的原型为:int main(int argc, char *argv[]);。argc 是命令行参数的数目,argv 是指向参数的各个指针所构成的数组。ISO C 和 POSIX 都要求 argv[argc] 是一个空指针。
但 main 函数并非程序执行的真正入口点。
当内核执行 C 程序时(使用一个 exec 函数),在调用 main 前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器(链接器)设置的,而连接编辑器则由 C 编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用 main 函数做好安排。
// 启动例程的伪代码表示
exit(main(argc, argv));
在汇编层次,这个启动函数的名字通常是 _start。它负责从内核接收命令行参数和环境变量,设置好之后再调用 main 函数。
2.2 进程终止:八种方式
进程终止的方式有8种,其中5种为正常终止,3种为异常终止。
正常终止的5种方式:
- 从
main返回:相当于调用exit。 - 调用
exit:ISO C 定义的函数,会执行清理操作。 - 调用
_exit或_Exit:立即进入内核。 - 最后一个线程从其启动例程返回。
- 从最后一个线程调用
pthread_exit。
异常终止的3种方式:
- 调用
abort:产生SIGABRT信号。 - 接到一个信号:某些信号会导致进程终止。
- 最后一个线程对取消请求做出响应。
2.2.1 退出函数详解
三个用于正常终止程序的函数:
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
区别:
exit和_Exit由 ISO C 说明,_exit由 POSIX.1 说明。_exit和_Exit立即进入内核。exit先执行一些清理处理(如冲洗所有标准 I/O 流,执行终止处理程序),然后进入内核。
2.2.2 atexit:注册终止处理程序
atexit 函数可以登记函数,这些函数将由 exit 自动调用。
#include <stdlib.h>
int atexit(void (*func)(void));
特性:
- ISO C 规定,一个进程可以登记多至32个函数。
- 先登记的函数,后调用(即按照与登记顺序相反的顺序调用)。
- 同一函数登记多次,则会被调用多次。
示例:演示 atexit 的调用顺序。
#include "apue.h"
#include <stdlib.h>
static void hello(void) {
printf("hello\n");
}
static void byebye(void) {
printf("byebye\n");
}
int main(void) {
if (atexit(hello) != 0)
err_sys("atexit error");
if (atexit(hello) != 0) // 同一函数注册两次
err_sys("atexit error");
if (atexit(byebye) != 0)
err_sys("atexit error");
printf("main is done\n");
exit(0);
}
运行输出:
main is done
byebye
hello
hello
2.2.3 进程终止状态
exit、_exit、_Exit 都带一个整型参数,称为终止状态(或退出状态)。可以通过 shell 检查进程终止的状态。
特殊规则:
- 如果调用这些函数时不带终止状态,终止状态是未定义的。
- 如果
main执行了一个无返回值的return语句,终止状态是未定义的。 - 如果
main没有声明返回类型为整型,终止状态是未定义的。 - 若
main的返回类型是整型,并且main执行到最后一条语句时返回(隐式返回),则进程的终止状态是0。
2.3 命令行参数与环境表
2.3.1 命令行参数
当执行一个程序时,调用 exec 的进程可以将命令行参数传递给新程序。main 函数的 argc 和 argv 参数正是接收这些参数的。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; ++i) {
printf("%d: %s\n", i, argv[i]);
}
exit(0);
}
2.3.2 环境表
每个程序都会从父进程那里接收一张环境表。和参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址。全局变量 environ 则包含了该指针数组的地址。
// 环境表的结构
extern char **environ; // 环境指针
数据结构:
environ ──→ ┌──────────┐
│ 指针 ──→ "HOME=/home/user\0"
├──────────┤
│ 指针 ──→ "PATH=/bin:/usr/bin\0"
├──────────┤
│ ... │
├──────────┤
│ NULL │
└──────────┘
2.3.3 环境变量操作函数
为了可移植性,应使用标准函数操作环境变量,而不是直接操作 environ。
#include <stdlib.h>
char *getenv(const char *name); // 获取环境变量的值
int putenv(char *string); // 添加或修改环境变量
int setenv(const char *name, const char *value, int overwrite); // 设置环境变量
int unsetenv(const char *name); // 删除环境变量
| 函数 | 说明 |
|---|---|
getenv |
获取名为 name 的环境变量的值,返回指向该值的指针 |
putenv |
取形式为 "name=value" 的字符串,将其添加到环境表中。如果 name 已存在,则删除原定义 |
setenv |
将 name 设置为 value。如果 name 已存在,根据 overwrite 决定是否覆盖(非0覆盖,0不覆盖) |
unsetenv |
删除 name 的定义 |
💡 实践提示:直接修改
environ可能导致内存问题,推荐使用setenv和unsetenv等标准函数。
2.4 C 程序的存储空间布局
C 程序在内存中由多个段组成。从低地址到高地址的顺序如下:
| 段 | 说明 | 存储内容 | 特性 |
|---|---|---|---|
| 正文段(text) | CPU 执行的机器指令部分 | 代码、const 全局变量、const 静态变量、字符串字面值 |
可共享、只读 |
| 初始化数据段(data) | 已初始化的数据 | 程序中明确赋初值的全局变量和静态变量 | 从程序文件中读取初值 |
| 未初始化数据段(bss) | 未初始化的数据 | 未初始化的全局变量和静态变量 | 内核在程序执行前将其初始化为0或空指针 |
| 堆(heap) | 动态存储分配 | malloc、calloc、realloc 分配的内存 |
从低地址向高地址增长 |
| 栈(stack) | 自动变量和函数调用信息 | 局部变量、函数参数、返回地址、调用者环境信息 | 从高地址向低地址增长 |
| 命令行参数和环境变量 | 传递给程序的信息 | argv、environ |
位于栈顶之上 |
📌 重要规则:未初始化数据段(bss)的内容并不存放在磁盘程序文件中,只有正文段和初始化数据段需要存放在磁盘程序文件中。
2.5 共享库
共享库使得可执行文件不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存该库例程的一个副本。这样可显著减小可执行文件的长度,但增加了运行时的时间开销。
# 使用 size 命令查看程序各段长度
$ size /bin/bash
text data bss dec hex filename
916355 35848 23304 975507 ee293 /bin/bash
动态链接 vs 静态链接:
- 静态链接:可执行程序包含所有需要的函数,不依赖外部库,文件较大。****
- 动态链接:可执行程序依赖外部共享库,文件较小,运行时需要动态装入器加载共享库。****
2.6 动态内存分配
C 程序使用 malloc、calloc、realloc、free 等函数进行动态内存分配。
#include <stdlib.h>
void *malloc(size_t size); // 分配 size 字节,内容未初始化
void *calloc(size_t nobj, size_t size); // 分配 nobj * size 字节,内容初始化为0
void *realloc(void *ptr, size_t newsize); // 更改已分配区域的大小
void free(void *ptr); // 释放已分配的内存
alloca 函数:
#include <alloca.h>
void *alloca(size_t size); // 在栈上分配内存,函数返回时自动释放
alloca 的特殊之处在于它在调用者的栈帧上分配内存,因此不需要调用 free 来释放——当函数返回时,这块内存会自动释放。但它不能用于跨函数传递分配的内存。
2.7 非局部跳转:setjmp 和 longjmp
setjmp 和 longjmp 提供了在函数之间进行跳转的能力,常用于从深层嵌套的函数调用中直接返回,而无需逐级返回。这种机制常用于错误处理等场景。
#include <setjmp.h>
int setjmp(jmp_buf env); // 保存当前调用环境
void longjmp(jmp_buf env, int val); // 恢复保存的环境
工作原理
setjmp:在需要作为跳转目标的位置调用。它保存当前栈环境到env中。首次调用时返回0。longjmp:在另一个函数中调用。它恢复env保存的环境,使程序执行流程跳转到setjmp调用点。此时setjmp返回val参数的值,用于区分不同的跳转来源。
⚠️ 重要限制:
longjmp只能跳转到仍在调用栈上的setjmp。如果setjmp所在的函数已经返回,再调用longjmp将导致未定义行为(通常是段错误)。****
setjmp/longjmp 对自动变量、寄存器变量和全局变量的影响:
- 全局变量、静态变量:在
longjmp后保持调用longjmp时的值 - 自动变量、寄存器变量:如果变量值在调用
longjmp之前被修改,其值是否保持不变取决于编译器优化。声明为volatile可以确保其值不受longjmp影响。
示例:使用 setjmp/longjmp 实现错误恢复。
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
jmp_buf main_env;
void error_handler(int code) {
printf("Error occurred, code: %d\n", code);
longjmp(main_env, code);
}
int main(void) {
int ret;
if ((ret = setjmp(main_env)) != 0) {
printf("Recovered from error, ret = %d\n", ret);
// 这里可以进行错误恢复处理
exit(0);
}
printf("Normal execution path\n");
error_handler(1); // 触发跳转
printf("This line will not be executed\n");
return 0;
}
2.8 进程资源限制
每个进程都有一组资源限制,可以使用 getrlimit 和 setrlimit 函数查询和修改。
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlp);
int setrlimit(int resource, const struct rlimit *rlp);
struct rlimit 结构体包含两个成员:
rlim_cur:当前软限制rlim_max:硬限制
常用的资源类型:
| 资源 | 说明 |
|---|---|
RLIMIT_AS |
进程可用存储空间最大长度(字节) |
RLIMIT_CORE |
core 文件最大字节数 |
RLIMIT_CPU |
CPU 时间量(秒) |
RLIMIT_DATA |
数据段最大长度(字节) |
RLIMIT_FSIZE |
可创建的文件最大长度(字节) |
RLIMIT_NOFILE |
每个进程能打开的最多文件数 |
RLIMIT_NPROC |
每个实际用户 ID 可拥有的子进程数 |
RLIMIT_STACK |
栈的最大长度(字节) |
规则:
- 只有超级用户可以更改硬限制。
- 软限制可以设置为小于或等于硬限制的值。
- 软限制和硬限制都可以用
RLIM_INFINITY表示“无穷大”。
示例:查询和设置进程资源限制。
#include "apue.h"
#include <sys/resource.h>
#include <errno.h>
int main(void) {
struct rlimit rl;
// 获取栈大小的限制
if (getrlimit(RLIMIT_STACK, &rl) < 0)
err_sys("getrlimit error");
printf("RLIMIT_STACK: soft=%ld, hard=%ld\n",
(long)rl.rlim_cur, (long)rl.rlim_max);
// 获取能打开的文件数限制
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_sys("getrlimit error");
printf("RLIMIT_NOFILE: soft=%ld, hard=%ld\n",
(long)rl.rlim_cur, (long)rl.rlim_max);
return 0;
}
三、完整源代码示例
3.1 打印所有环境变量
#include "apue.h"
#include <stdlib.h>
extern char **environ; // 环境指针
int main(void) {
char **env = environ;
while (*env != NULL) {
printf("%s\n", *env);
env++;
}
return 0;
}
3.2 获取特定环境变量的值
#include "apue.h"
#include <stdlib.h>
int main(int argc, char *argv[]) {
char *path = getenv("PATH");
if (path == NULL)
printf("PATH environment variable not found\n");
else
printf("PATH = %s\n", path);
return 0;
}
3.3 演示 setjmp/longjmp 对变量的影响
#include <setjmp.h>
#include <stdio.h>
static jmp_buf env;
static int global = 0;
void do_jump(void) {
int local = 2;
register int reg = 3;
static int stat = 4;
volatile int vol = 5;
global = 10;
local = 20;
reg = 30;
stat = 40;
vol = 50;
longjmp(env, 1);
}
int main(void) {
int local = 0;
register int reg = 0;
static int stat = 0;
volatile int vol = 0;
if (setjmp(env) == 0) {
printf("Initial call\n");
do_jump();
} else {
printf("After longjmp\n");
printf("global=%d, local=%d, reg=%d, stat=%d, vol=%d\n",
global, local, reg, stat, vol);
}
return 0;
}
四、本章知识点速查表
| 知识点 | 核心内容 |
|---|---|
| main 函数 | C 程序入口,原型 int main(int argc, char *argv[]),argv[argc] 为 NULL |
| 启动例程 | _start,在 main 之前调用,负责取得参数并调用 main |
| 进程终止 | 8种方式:5种正常(main返回、exit、_exit/_Exit等),3种异常(abort、信号等) |
| 退出函数 | exit(有清理)、_exit/_Exit(无清理) |
atexit |
注册终止处理程序,先注册后调用,最多32个 |
| 环境表 | extern char **environ,每个字符串为 name=value 形式 |
| 环境变量函数 | getenv、putenv、setenv、unsetenv |
| 内存布局 | 正文段 → data → bss → 堆 → 栈 → 命令行参数和环境变量 |
| 共享库 | 动态链接 vs 静态链接 |
| 动态内存分配 | malloc、calloc、realloc、free、alloca |
setjmp/longjmp |
非局部跳转,用于错误恢复,不能跳转到已返回的函数 |
| 资源限制 | getrlimit、setrlimit,软限制 ≤ 硬限制 |
五、动手实践建议
-
查看进程内存布局:
cat /proc/self/maps # Linux 下查看当前进程的内存映射 -
使用
size命令查看程序各段大小:size /bin/bash -
查看共享库依赖:
ldd /bin/ls -
使用
ulimit查看 Shell 的资源限制:ulimit -a
六、常见易混淆点
Q1:exit 和 _exit 有什么区别?
A:exit 会执行标准 I/O 库的清理关闭操作(调用 fclose 冲洗所有输出流),并调用 atexit 注册的终止处理程序,然后进入内核。_exit 和 _Exit 则立即进入内核,不执行任何清理。****
Q2:atexit 注册的函数调用顺序是怎样的?
A:按照与注册顺序相反的顺序调用。先注册的函数后调用,后注册的函数先调用。
Q3:setjmp 和 longjmp 有什么限制?
A:longjmp 只能跳转到仍在调用栈上的 setjmp。如果 setjmp 所在的函数已经返回,再调用 longjmp 会导致未定义行为。****
Q4:alloca 和 malloc 有什么区别?
A:alloca 在栈上分配内存,函数返回时自动释放,不需要调用 free。malloc 在堆上分配内存,必须显式调用 free 释放。alloca 不能用于跨函数传递分配的内存。
Q5:volatile 在 setjmp/longjmp 中起什么作用?
A:setjmp/longjmp 会影响局部变量的值。声明为 volatile 的变量会确保其值在 longjmp 后保持不变(不会被优化器还原)。对于全局变量和静态变量,不需要 volatile,它们的值也会在 longjmp 后保持。
七、学习心得
第7章是全书的进程控制前奏,虽然概念较多,但都是理解后续章节的必要基础。重点掌握以下几点:
main函数的真正入口点是启动例程_start,它负责接收命令行参数和环境变量后调用main。atexit注册的终止处理程序调用顺序是“先注册后调用”。setjmp/longjmp用于实现非局部跳转,是 C 语言中模拟异常处理机制的方式。编写相关代码时需要注意volatile的使用。- 动态内存分配时,务必检查返回值(是否为 NULL),并养成及时释放内存的习惯。
- 理解资源限制,编写健壮的程序应能查询并适应系统的资源限制,而不是假设“无限大”。
理解本章后,你已经为下一章“进程控制”做好了充分准备。后续章节将深入进程的创建、执行和终止。
八、下一篇预告
下一篇将进入第8章“进程控制”,内容包括:
- 进程标识符(PID)
fork、vfork函数exit函数族wait和waitpid函数exec函数族- 解释器文件(
#!) system函数- 进程会计
- 用户标识
- 进程时间
敬请期待!
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析! ❤️ 如果觉得有用,请给个赞支持一下作者!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)