欢迎光临
我们一直在努力

Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解

在这里插入图片描述

上一篇我们讲完了 Linux O (1) 调度算法,搞懂了进程如何被调度上 CPU 运行。
但一个进程启动之后,除了自己的代码、数据,操作系统还会给它附带两份 “随身资料”:命令行参数 和 环境变量。
很多人天天敲命令,gcc、g++、sudo、su,却很少思考:
为什么 gcc 可以自动找到系统的库文件?为什么 shell 可以在 PATH 里面搜索命令?为什么子进程可以拿到父进程的环境信息?
本篇我们从 main 函数讲起,逐层拆解命令行参数、环境变量底层结构、4 种代码获取方式、继承原理,同时讲环境变量的安全边界。


前言:命令行参数

1.1 重新认识 main 函数

很多 C/C++ 初学者以为,main()就是程序的真正入口,其实不是。

1.1.1 main 函数不是程序的开始

真正的程序入口是_start函数,这个函数属于 C 运行时库(crt0)。
当操作系统把程序加载进内存,CPU 跳转到_start执行:

  • 做程序运行前的初始化工作(栈、标准 IO、全局变量初始化)
  • 解析命令行参数、解析环境变量表
  • 调用我们写的main()函数
  • main 返回后,_start调用exit()完成进程退出回收
  • main 只是我们写业务逻辑的入口,不是整个程序的底层入口。

    1.1.2 main 函数也有参数

    我们平时写的 main 有两种写法:

    // 写法1,不接收参数
    int main()

    // 写法2,接收命令行参数,argc + argv
    int main(int argc, char *argv[])

    • argc(argument count):命令行参数的个数
    • argv(argument vector):字符串指针数组,保存每一个命令行字符串,数组末尾以NULL标记结束。

    示例代码:

    // arg_demo.c
    #include <stdio.h>
    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]);
    }
    return 0;
    }

    编译运行:

    gcc arg_demo.c -o arg_demo
    ./arg_demo hello linux 123

    输出:

    argc = 4
    argv[0] = ./arg_demo
    argv[1] = hello
    argv[2] = linux
    argv[3] = 123

    注意:argv[0]永远是程序自身的名字。

    1.2 命令行参数的切分与表结构

    shell 在敲回车执行命令的时候,会做一件事:把你输入的整行字符串,按空格切分成一个个字符串,组装成argv数组。
    内存布局:

    argv[0]"./arg_demo"
    argv[1]"hello"
    argv[2]"linux"
    argv[3]"123"
    argv[4] → NULL

    这个数组,存放在进程用户地址空间的栈区高地址,和环境变量挨在一起。

    1.3 命令行参数的应用演示

    命令行参数最典型的用途:程序不用重新编译,启动的时候传参数改变行为。
    比如ls -l,gcc -o test test.c,-l、-o test test.c全部都是命令行参数。


    一、引入环境变量

    1.1 基本概念

    1.1.1 环境变量的概念

    环境变量就是操作系统提供给进程的一组特殊全局变量,用来记录系统和用户的运行信息。

    就像人出门会随身带证件、通讯录,进程启动的时候,操作系统也会给它附带一整套环境变量。
    里面记录的信息包括:

    • 当前是谁登录用户(USER)
    • 主机名字(HOSTNAME)
    • 命令去哪里找(PATH)
    • 动态库去哪里找(LD_LIBRARY_PATH)
    • 历史命令最多存多少条(HISTSIZE)

    重点:环境变量最大的特性:子进程会继承父进程的环境变量。
    这就解释了:为什么gcc、g++、sudo、su这些工具都可以读取环境变量。

    • gcc/g++ 继承 shell 的环境变量,读取LD_LIBRARY_PATH去寻找动态库,读取PATH寻找编译工具链。
    • su/sudo 继承 shell 的环境变量,可以读取系统信息,完成切换用户、查找命令等操作。
    1.1.2 常见环境变量

    表格

    变量名作用
    PATH 命令搜索路径,shell 在这里找可执行程序
    HOME 当前用户家目录
    USER 当前会话用户名
    LOGNAME 登录用户名
    HOSTNAME 主机名
    LD_LIBRARY_PATH 动态链接库搜索路径
    HISTSIZE shell 历史命令保存条数
    1.1.3 怎么看环境变量

    # 打印全部环境变量
    env
    printenv

    # 打印单个环境变量
    echo $PATH
    echo $HOME

    1.1.4 环境变量和本地变量的设置和导入

    shell 里面有两种变量:

  • shell 本地变量:只在当前 shell 生效,子进程看不到。
  • 环境变量:通过export导出,子进程可以继承。
  • 1.1.4.1 核心概念对比

    # 定义shell本地变量(子进程不可见)
    MY_VAR="hello world"

    # 导出为环境变量(子进程可以继承)
    export MY_VAR

    1.1.4.2 变量的操作指令

    export 变量名=# 设置环境变量
    unset 变量名 # 删除变量

    ⚠️注意:直接写MY_VAR=xxx,只是 shell 局部变量;不加 export,子进程拿不到这个变量。

    1.2 main 函数的第三个参数

    很多人不知道 main 函数还有第三个隐藏参数,用来接收环境变量表:

    int main(int argc, char *argv[], char *envp[])

    envp是字符串指针数组,每一个元素格式为KEY=VALUE,数组末尾以NULL结束。

    示例代码:

    // env_main.c
    #include <stdio.h>
    int main(int argc, char *argv[], char *envp[])
    {
    for(int i = 0; envp[i] != NULL; i++)
    {
    printf("%s\\n", envp[i]);
    }
    return 0;
    }

    编译运行,就会打印出全部环境变量。

    注意:这个第三个参数是 GCC 的扩展,标准 C 不推荐直接使用,标准推荐使用getenv()或者全局environ指针。

    1.3 为什么执行自己的程序必须加 ./

    ./myprogram

    为什么不能直接写myprogram?

    shell 执行命令的时候,会去PATH环境变量记录的目录里面挨个找可执行文件。
    当前目录.默认不在 PATH 里面,所以 shell 找不到你的程序,必须用./显式告诉 shell:在当前目录找这个程序。

    如果你把.加入 PATH,就可以直接写程序名运行,但这存在安全风险,不建议生产环境这么做。


    二、环境变量的底层表结构与溯源

    2.1 环境变量表的组织形式

    环境变量表本质就是char *envp[]指针数组:

    envp[0]"PATH=/usr/bin:/bin"
    envp[1]"HOME=/home/ubuntu"
    envp[2]"USER=ubuntu"
    envp[3] → NULL

    每一条字符串格式固定为键=值,数组末尾用NULL标记结束。
    这块内存位于进程地址空间的用户栈高地址,和命令行参数挨在一起。

    2.2 环境变量的内存级和磁盘级修改

    • 内存级修改(临时):shell 执行export,只对当前 shell 会话生效,关闭终端就消失。
    • 磁盘级修改(永久):修改配置文件,登录 shell 的时候自动加载环境变量。
      • 用户级别:~/.bashrc、~/.profile,只对当前用户生效
      • 系统级别:/etc/profile,对所有用户生效

    修改完磁盘配置文件,需要执行source ~/.bashrc,让配置立刻生效。

    2.3 怎么在 windows 下修改环境变量

    Windows 环境变量逻辑和 Linux 类似:

  • 此电脑 → 右键属性 → 高级系统设置 → 环境变量
  • 分为用户环境变量(当前用户)、系统环境变量(全部用户)
  • 修改后新开 cmd 窗口生效,旧窗口不会自动更新。

  • 三、获取环境变量的三种核心代码手段

    注意:main 第三个参数、getenv、environ、子进程继承,一共 4 种方式。

    3.1 方法一:通过 main 函数的第三个参数隐式获取

    就是上面写的char *envp[],直接遍历数组读取环境变量。
    缺点:标准 C 不支持,属于编译器扩展,可移植性差。

    3.2 方法二:通过系统调用 API getenv 获取(✅最推荐)

    C 标准库提供getenv(),输入环境变量 key,返回对应的 value 字符串。

    // getenv_demo.c
    #include <stdio.h>
    #include <stdlib.h>

    int main()
    {
    char *path = getenv("PATH");
    if(path != NULL)
    {
    printf("PATH = %s\\n", path);
    }
    char *user = getenv("USER");
    printf("USER = %s\\n", user);
    return 0;
    }

    编译运行:

    gcc getenv_demo.c -o getenv_demo
    ./getenv_demo

    优点:标准 C 库函数,跨平台,代码简洁。

    3.3 方法三:通过全局第三方指针environ显式获取

    内核给我们提供一个全局的二级指针environ,不需要 main 第三个参数,直接使用。

    需要手动声明 extern char **environ;

    // environ_demo.c
    #include <stdio.h>
    extern char **environ;

    int main()
    {
    for(int i = 0; environ[i] != NULL; i++)
    {
    printf("%s\\n", environ[i]);
    }
    return 0;
    }

    environ全局指针,直接指向进程的环境变量表的起始地址。

    3.4 方法四:子进程继承父进程获取环境变量

    3.4.1 核心原理

    当我们调用fork()创建子进程的时候:
    子进程会拷贝父进程的完整地址空间,环境变量表也完整复制一份。
    所以子进程天然拥有父进程全部环境变量。

    后续调用exec加载新程序,默认保留环境变量,这就是为什么gcc、sudo这些子进程可以拿到 shell 的环境变量。

    exec 也可以使用execle(),传入自定义的环境变量表,不继承父进程。

    3.4.2 示例代码

    // fork_env.c
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>

    int main()
    {
    pid_t pid = fork();
    if(pid == 0)
    {
    // 子进程,读取PATH
    printf("[子进程] PATH = %s\\n", getenv("PATH"));
    }
    else
    {
    printf("[父进程] PATH = %s\\n", getenv("PATH"));
    }
    return 0;
    }

    运行可以看到父子进程打印出一模一样的 PATH,证明继承生效。


    四、其他环境变量

    4.1 所有环境变量罗列

    执行env命令,就可以列出当前进程全部环境变量。

    4.1.1 USER 和 LOGNAME 的区别
    • LOGNAME:记录登录终端的原始用户。
    • USER:记录当前会话的用户。

    举例子:用 ubuntu 账号登录终端,然后执行sudo su root切换 root。
    此时LOGNAME仍然是 ubuntu,而USER变成 root。

    4.1.2 历史命令与 HISTSIZE

    HISTSIZE这个环境变量控制 shell 历史命令最多保存多少条。

    echo $HISTSIZE

    修改这个值,就可以调整 shell 历史命令的存储上限。

    4.2 环境绑定与权限控制用法

    环境变量可以用来给程序传递配置,不需要修改源代码,只需要设置环境变量就改变程序行为。但是也存在安全风险。

    4.2.1 核心逻辑与安全防范

    ⚠️安全坑点:如果程序是 root 权限运行,环境变量来自普通用户,恶意用户可以篡改环境变量,实现攻击,比如 PATH 劫持。
    所以高权限程序,很多会主动清空或者过滤环境变量。

    4.2.1.1 示例代码

    // secure_env.c
    #include <stdio.h>
    #include <stdlib.h>

    int main()
    {
    // 高权限程序,不要无条件信任环境变量
    char *debug = getenv("DEBUG");
    if(debug != NULL)
    {
    printf("DEBUG模式开启\\n");
    }
    return 0;
    }

    4.2.3 基于自定义环境变量的控制逻辑

    业务程序可以自定义环境变量,作为配置开关。

    4.2.3.1 示例代码

    // custom_env.c
    #include <stdio.h>
    #include <stdlib.h>

    int main()
    {
    char *mode = getenv("RUN_MODE");
    if(mode == NULL)
    {
    printf("默认生产模式\\n");
    }
    else if(strcmp(mode, "debug") == 0)
    {
    printf("调试模式\\n");
    }
    return 0;
    }

    运行测试:

    # 生产模式
    ./custom_env

    # 调试模式
    export RUN_MODE=debug
    ./custom_env

    不用改代码,只设置环境变量,就切换程序运行模式。


    全文总结

  • 命令行参数:shell 把命令切割成字符串数组,传递给 main 函数的argc/argv,用来给程序传递启动参数。
  • 环境变量:操作系统给进程的一组特殊全局变量,保存系统、用户信息,格式为KEY=VALUE。
  • shell 本地变量必须用export导出,才会变成环境变量被子进程继承。
  • 4 种获取环境变量的方式:
    • main 第三个参数 envp(可移植差)
    • getenv()标准库函数(推荐)
    • 全局environ指针
    • fork 子进程继承父进程环境变量
  • 继承原理:fork拷贝地址空间,环境变量表复制;exec默认保留环境变量。
  • 为什么 gcc/g++、sudo 可以读取环境变量:它们都是 shell 的子进程,继承 shell 的环境变量。
  • 环境变量可以做配置控制,但高权限程序要警惕环境变量带来的安全风险。
    在这里插入图片描述
  • 赞(0)
    未经允许不得转载:171主机测评 » Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址