Linux 命令行参数与环境变量:从 Shell 到 main() 的数据是怎么传进去的?

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

很多人第一次接触 argcargvenvp 时,都会觉得它们只是 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


三、用代码把它们打印出来

这组代码很适合做实验。下面这版代码可以直接拿来观察 argcargv 和环境变量:

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 先到 EDI
  • argv 先到 RSI
  • envp 先到 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 变量 不能,只有环境变量才行
envpenviron 完全不一样 本质上都是环境变量表
main() 只能有两个参数 标准写法可以带环境变量参数
/proc/[pid]/environ 是普通文本文件 里面是 \0 分隔,不是普通逐行文本

十二、这一节最后记住什么?

命令行参数和环境变量,其实就是进程启动时携带的两类"初始数据"。

  • argc/argv 负责描述"这次怎么跑"
  • envp/environ/getenv() 负责描述"在什么环境里跑"
  • /proc/[pid]/cmdline/proc/[pid]/environ 则把这些内容重新暴露给我们观察

一句话总结:

text 复制代码
命令行参数解决"这次怎么跑"
环境变量解决"在什么环境里跑"

一旦把这条线串起来,argvenvpgetenv()export/proc 就不再是零散知识点,而是同一套进程启动模型里的不同侧面。


相关推荐
瓶中怪6 分钟前
ROS2 机器人软件系统
linux·c++·python·ubuntu·vmware·ros2·机器人软件开发
iangyu7 分钟前
linux配置时间同步
linux·运维·服务器
天空'之城27 分钟前
Linux 系统编程 04:进程基础
linux·开发语言·进程基础
从零开始的代码生活_27 分钟前
NAT、代理服务与内网穿透详解
linux·服务器·网络·c++·http·智能路由器
灯厂码农1 小时前
C语言内存管理——内存对齐与共用体union
linux·服务器·c语言
charlie1145141911 小时前
Cinux: 加载第一个内核:从 bootloader 跳进 C++
linux·开发语言·c++·嵌入式
Tian_Hang2 小时前
eclipse ditto 学习笔记
运维·服务器·开发语言·javascript·3d
江畔柳前堤2 小时前
第13章:docker生产环境部署实战
运维·git·docker·容器·代码复审
爱喝水的鱼丶2 小时前
SAP-ABAP:接口 vs 抽象类:ABAP OOP两类扩展方式的差异与选型原则
运维·性能优化·sap·abap·erp·经验交流
iCxhust2 小时前
linux目录是否保存在硬盘 启动后读入解析的
linux·运维·服务器