17、进程环境:main()函数的参数、环境变量与程序退出
各位同学,今天我们来聊聊进程环境。说白了,就是一个C程序跑起来之后,它周围到底有些什么东西。main函数是怎么被调用的?那些环境变量到底藏在哪?程序退出时又发生了什么?这些看似基础的问题,我在实际项目中踩过不少坑,今天一并讲清楚。
17.1 main()函数的参数——谁在调用它?
我们天天写 int main(int argc, char *argv[]),但你有没有想过,这两个参数是谁传进来的?
答案是——内核 + C运行时库(CRT)。
当你执行 ./a.out hello world 时,shell会先fork一个子进程,然后execve系统调用把程序加载进内存。内核会把命令行字符串拆好,放到进程的栈上。然后CRT的启动代码(比如 _start)会把这些参数整理好,再调用你的main函数。
我个人习惯在main函数开头先打印一下argc和argv,确认参数个数。这在调试时特别有用。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("参数个数: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
运行一下:
$ ./a.out hello world
参数个数: 3
argv[0] = ./a.out
argv[1] = hello
argv[2] = world
注意,argv[0]是程序名,不是第一个参数。这个细节我见过不少新手搞混。
17.2 环境变量——进程的“全局配置”
环境变量是什么?说白了就是一组键值对,每个进程都有一份拷贝。你想想看,为什么你写的程序能知道当前用户是谁?为什么能知道PATH路径?都是环境变量的功劳。
环境变量存储在进程地址空间的高地址区域,紧挨着main函数的参数。每个进程启动时,都会从父进程那里继承一份环境变量表。
17.2.1 获取环境变量:getenv()
这个函数最简单,给个名字,返回值。我在项目中经常用它来读取配置文件路径。
#include <stdlib.h>
#include <stdio.h>
int main()
{
char *home = getenv("HOME");
if (home) {
printf("用户主目录: %s\n", home);
}
char *path = getenv("PATH");
if (path) {
printf("PATH: %s\n", path);
}
return 0;
}
运行结果:
用户主目录: /home/zhangsan
PATH: /usr/local/bin:/usr/bin:/bin
17.2.2 设置环境变量:setenv() 与 putenv()
这两个函数都能改环境变量,但行为有区别。我当年在做一个嵌入式网关项目时,就因为搞混了它们,导致内存泄漏查了一整天。
| 函数 | 原型 | 行为 | 内存管理 |
|---|---|---|---|
| setenv() | int setenv(const char *name, const char *value, int overwrite); |
设置或修改环境变量 | 内部会复制字符串,安全 |
| putenv() | int putenv(char *string); |
直接插入字符串指针 | 不复制,必须传可修改的内存 |
看个例子:
#include <stdlib.h>
#include <stdio.h>
int main()
{
// setenv 用法:安全,推荐
setenv("MY_VAR", "hello", 1);
printf("MY_VAR = %s\n", getenv("MY_VAR"));
// putenv 用法:注意!必须传可修改的字符串
char buf[64];
snprintf(buf, sizeof(buf), "MY_VAR2=world");
putenv(buf);
printf("MY_VAR2 = %s\n", getenv("MY_VAR2"));
return 0;
}
putenv("KEY=VALUE"); —— 字符串常量是只读的,在某些系统上会直接崩溃。建议优先用setenv()。
17.3 程序退出——exit() 与 _exit() 的区别
程序结束的方式有很多种:main函数return、调用exit()、调用_exit()、被信号杀死……但exit()和_exit()有什么区别?
简单说:exit() 会做清理工作,_exit() 直接走人。
exit() 是C标准库的函数,它会:
- 调用所有通过
atexit()注册的退出处理函数 - 刷新并关闭所有标准I/O流(stdin、stdout、stderr等)
- 调用
_exit()进入内核
而 _exit() 是系统调用,直接通知内核终止进程,不做任何清理。
看个对比:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void cleanup()
{
printf("清理函数被调用\n");
}
int main()
{
atexit(cleanup);
printf("程序即将退出\n");
// 试试换成 _exit(0) 看看区别
exit(0);
// _exit(0);
}
用exit()时,你会看到"清理函数被调用"的输出。用_exit()时,不会输出这句话,而且printf的缓冲区可能都没来得及刷新。
17.4 知识体系总览
下面这张图把本章的核心内容串起来了,你可以看到main函数参数、环境变量、程序退出这三者之间的关系:
17.5 避坑指南
最后,我把自己这些年踩过的坑总结一下:
- 不要修改getenv()返回的字符串——它指向环境变量表内部,改了可能影响其他代码。
- putenv()传的字符串必须是可修改的——传字符串常量?等着段错误吧。
- fork后子进程用_exit()——避免刷新父进程的I/O缓冲区,造成数据重复输出。
- atexit()注册的函数在exit()时按注册顺序的逆序执行——如果你依赖清理顺序,要注意。
- main函数return 0; 等价于 exit(0);——但return非零值不一定等价于exit(非零),因为编译器可能做优化。
嗯,今天就讲到这里。进程环境这部分内容虽然基础,但理解透彻了,后面讲进程控制、信号处理时你会觉得轻松很多。
公众号:蓝海资料掘金营,微信deep3321