程序启动时,命令行参数和环境变量会一起被带进进程。一个负责告诉程序"这次怎么跑",一个负责告诉程序"在什么环境里跑"。

很多人第一次接触 argc、argv、envp 时,都会觉得它们只是 main() 里的几个参数;但把 shell 变量、环境变量、getenv()、environ、/proc/[pid]/cmdline、/proc/[pid]/environ 连起来看,就会发现这是一条很完整的数据链路。
这篇文章我们不绕弯子,直接从现象入手,顺着代码、shell、进程和 /proc 一层层拆开。
一、先看现象:命令行参数到底去哪了?
先看一个最直接的命令:
bash
./code -a -b -c
这几个参数不是程序自己生成的,而是 Shell 在创建新进程时,一起整理好交给新程序的。也就是说,命令行参数不是"程序跑起来后再去问 Shell",而是进程启动的时候就已经带进来了。
环境变量也是同理。程序启动时,父进程会把一份环境变量表一并交给子进程,所以新程序从一开始就能看到自己所处的运行环境。
二、main() 为什么能带参数?
很多人第一次看到下面这种写法时会有点疑惑:
c
int main(int argc, char *argv[], char *envp[])
其实它很自然。
| 参数 | 含义 |
|---|---|
argc |
命令行参数个数 |
argv |
命令行参数数组 |
envp |
环境变量数组 |
可以把它们理解成进程启动时自带的两份"清单":
argv记录这次启动传了什么参数envp记录这个进程继承了哪些环境变量
还有一个很容易忽略的小细节:
text
argv 的最后一个元素后面还有一个 NULL
envp 的最后一个元素后面也还有一个 NULL
所以我们遍历参数时通常会写成:
c
for(int i = 0; argv[i]; i++)
或者:
c
for(int i = 0; env[i]; i++)
不是因为"刚好只有这么多个",而是因为数组尾部用了 NULL 作为结束标记。
argv[0] 一般是程序名,后面的元素就是你在命令行里写的参数。比如:
bash
./code -a -b -c
通常会对应成:
| 下标 | 内容 |
|---|---|
argv[0] |
./code |
argv[1] |
-a |
argv[2] |
-b |
argv[3] |
-c |
此时 argc = 4。
三、用代码把它们打印出来
这组代码很适合做实验。下面这版代码可以直接拿来观察 argc、argv 和环境变量:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ;
int main(int argc, char *argv[], char *env[])
{
printf("argc = %d\n", argc);
for(int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("\n--- envp ---\n");
for(int i = 0; env[i]; i++)
{
printf("env[%d] = %s\n", i, env[i]);
}
printf("\n--- environ ---\n");
for(int i = 0; environ[i]; i++)
{
printf("environ[%d] = %s\n", i, environ[i]);
}
return 0;
}
编译运行:
bash
gcc code.c -o code
./code -a -b -c
输出里最关键的两类信息就是:
| 类型 | 说明 |
|---|---|
argv |
本次运行显式传入的命令行参数 |
env / environ |
当前进程继承到的环境变量 |
你可以粗略记成:
text
envp = main 入口传进来的环境变量表
environ = 全局变量形式的环境变量表
它们通常都指向同一批环境变量数据,所以遍历谁都能看到当前进程的环境。
四、环境变量到底是什么?
环境变量本质上就是一组 key=value 形式的字符串。
比如:
bash
PATH=/usr/local/bin:/usr/bin:/bin
USER=zdt
HOME=/home/zdt
它们会影响程序的行为,比如:
PATH决定命令去哪儿找HOME决定家目录位置USER表示当前用户
读取环境变量最常用的接口是 getenv():
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
const char *path = getenv("PATH");
if(path)
{
printf("PATH = %s\n", path);
}
return 0;
}
getenv() 的作用很直白:去当前进程的环境变量表里找指定名字对应的值。
如果需要修改当前进程的环境变量,还可以用:
c
setenv("NAME", "value", 1);
unsetenv("NAME");
五、shell 变量和环境变量不是一回事
这一点特别容易混。
bash
i=100
这只是 shell 的普通变量,只在当前 shell 里有效。
如果想让子进程也看到它,就要导出:
bash
export i=100
这时它才会进入环境变量表,子进程启动后才能继承到。
| 写法 | 作用 |
|---|---|
i=100 |
只是当前 shell 变量 |
export i=100 |
变成环境变量,子进程可继承 |
更精确地说:
text
shell 变量只属于当前 shell 进程
export 后的变量会进入环境变量表,随后被子进程继承

六、命令行参数和环境变量是怎么传进去的?
Shell 执行命令时,通常会先准备两份数据:
- 命令行参数数组
argv - 环境变量数组
envp
然后在创建新进程、调用 exec 族函数时,把这些内容交给新程序。
所以 main(argc, argv, envp) 并不是编译器"凭空送出来"的,而是程序入口和操作系统之间约定好的接口。
从进程角度看,这些数据都属于启动时的初始状态:
text
Shell 负责组织输入
exec 负责把输入交给新程序
main 负责接住这些输入
七、/proc 里也能看到它们
Linux 给我们提供了 /proc 来观察进程。
你可以通过下面两个文件看到命令行参数和环境变量:
bash
cat /proc/$$/cmdline
cat /proc/$$/environ
对应关系如下:
| 文件 | 内容 |
|---|---|
/proc/[pid]/cmdline |
进程启动时的命令行参数 |
/proc/[pid]/environ |
进程的环境变量 |
注意:这两个文件内部通常用 \0 分隔,所以直接 cat 可能看起来有点乱。更常见的写法是:
bash
cat /proc/$$/environ | tr '\0' '\n' | head
这样每个环境变量一行,会清楚很多。
如果想对比普通变量和环境变量,还可以试试:
bash
set | grep '^i='
env | grep '^i='
一般来说,普通变量只能在 set 里看到,env 里看到的是导出后的环境变量。
八、code.s 说明了什么?
code.s 可以帮我们从编译后视角理解 main() 的参数是怎么来的。
关键片段是:
asm
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movq %rdx, -40(%rbp)
这说明函数一开始,编译器已经把 main() 的参数保存进栈帧了。
在 Linux x86-64 下,main(argc, argv, envp) 的参数通常会先通过寄存器传进来,再被保存到当前函数栈里:
argc先到EDIargv先到RSIenvp先到RDX
所以你在 C 里写 argv[i],本质上就是顺着参数数组做地址偏移和解引用。
九、test.sh 也能说明问题
这个 test.sh 示例:
bash
#!/bin/bash
touch file
mv file myfile
i=100
echo $i
这里的 i=100 只是 shell 变量,所以只在脚本内部有效。
如果你想让后续子进程也能继承它,就要写成:
bash
export i=100
这样 i 才会进入环境变量表,后续启动的子进程才能看到。
如果改成下面这样,差别会更明显:
bash
#!/bin/bash
export i=100
./code
这时 ./code 里面就能通过 getenv("i") 或 environ 看到它。
十、做一个完整实验
可以用下面这个实验把整件事串起来:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ;
int main(int argc, char *argv[])
{
printf("argc = %d\n", argc);
for(int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("\nPATH = %s\n", getenv("PATH"));
printf("USER = %s\n", getenv("USER"));
printf("\n--- environ ---\n");
for(int i = 0; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
编译运行:
bash
gcc code.c -o code
export MYVAR=hello
./code -a -b -c
观察结果时重点看三件事:
| 观察点 | 你会看到什么 |
|---|---|
argc/argv |
命令行参数被传进来了 |
getenv() |
能拿到环境变量值 |
environ |
能遍历完整环境变量表 |
如果想让读者更容易看懂,可以固定一组参数来演示:
bash
./code -a -b -c
这样 argv[] 的拆分会特别清楚。
十一、最容易混淆的几个点
| 误区 | 正确理解 |
|---|---|
argv 是编译器生成的 |
不是,是 Shell / 内核在进程启动时传进来的 |
getenv() 能拿到普通 shell 变量 |
不能,只有环境变量才行 |
envp 和 environ 完全不一样 |
本质上都是环境变量表 |
main() 只能有两个参数 |
标准写法可以带环境变量参数 |
/proc/[pid]/environ 是普通文本文件 |
里面是 \0 分隔,不是普通逐行文本 |
十二、这一节最后记住什么?
命令行参数和环境变量,其实就是进程启动时携带的两类"初始数据"。
argc/argv负责描述"这次怎么跑"envp/environ/getenv()负责描述"在什么环境里跑"/proc/[pid]/cmdline和/proc/[pid]/environ则把这些内容重新暴露给我们观察
一句话总结:
text
命令行参数解决"这次怎么跑"
环境变量解决"在什么环境里跑"
一旦把这条线串起来,argv、envp、getenv()、export、/proc 就不再是零散知识点,而是同一套进程启动模型里的不同侧面。