在这里插入图片描述

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


前言:命令行参数

1.1 重新认识 main 函数

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

1.1.1 main 函数不是程序的开始

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

  1. 做程序运行前的初始化工作(栈、标准 IO、全局变量初始化)
  2. 解析命令行参数、解析环境变量表
  3. 调用我们写的main()函数
  4. 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 -lgcc -o test test.c-l-o test test.c全部都是命令行参数。


一、引入环境变量

1.1 基本概念

1.1.1 环境变量的概念

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

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

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

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

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

表格

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

# 打印单个环境变量
echo $PATH
echo $HOME
1.1.4 环境变量和本地变量的设置和导入

shell 里面有两种变量:

  1. shell 本地变量:只在当前 shell 生效,子进程看不到。
  2. 环境变量:通过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 类似:

  1. 此电脑 → 右键属性 → 高级系统设置 → 环境变量
  2. 分为用户环境变量(当前用户)、系统环境变量(全部用户)
  3. 修改后新开 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加载新程序,默认保留环境变量,这就是为什么gccsudo这些子进程可以拿到 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

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


全文总结

  1. 命令行参数:shell 把命令切割成字符串数组,传递给 main 函数的argc/argv,用来给程序传递启动参数。
  2. 环境变量:操作系统给进程的一组特殊全局变量,保存系统、用户信息,格式为KEY=VALUE
  3. shell 本地变量必须用export导出,才会变成环境变量被子进程继承。
  4. 4 种获取环境变量的方式:
    • main 第三个参数 envp(可移植差)
    • getenv()标准库函数(推荐)
    • 全局environ指针
    • fork 子进程继承父进程环境变量
  5. 继承原理:fork拷贝地址空间,环境变量表复制;exec默认保留环境变量。
  6. 为什么 gcc/g++、sudo 可以读取环境变量:它们都是 shell 的子进程,继承 shell 的环境变量。
  7. 环境变量可以做配置控制,但高权限程序要警惕环境变量带来的安全风险。
    在这里插入图片描述
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐