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 就不再是零散知识点,而是同一套进程启动模型里的不同侧面。


相关推荐
何中应1 小时前
Grafana面板没有数据问题排查
linux·grafana·prometheus
独隅1 小时前
IntelliJ IDEA 在 Linux 上的完整安装与使用指南
java·linux·intellij-idea
NaclarbCSDN1 小时前
我写了一个命令行书签管理器,然后抛弃了浏览器书签栏
linux·git·python·github
isyangli_blog1 小时前
基于 OpenDaylight 的 SDN 负载均衡应用
运维·负载均衡
ICT系统集成阿祥2 小时前
校园网络准入认证建设与运维经验
运维·网络·智慧校园·经验总结
颖火虫盟主2 小时前
Linux USB 探测→枚举→RNDIS 驱动匹配 全流程笔记
linux·运维·笔记
程序猿编码2 小时前
子域猎手:一款高性能DNS枚举工具的设计与实现
linux·c++·python·c·dns
Full Stack Developme2 小时前
Linux cd /abc 与 cd /abc/ 区别
linux·运维·服务器
想吃火锅10052 小时前
【leetcode】20.有效的括号js
linux·javascript·leetcode