在这里插入图片描述

◆ 博主名称: 小此方-CSDN博客
大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


概要&序論

  Hello大家好,我是此方。本文将带大家由浅入深地拆解 Linux 系统级编程中的核心基石:环境变量。我们将从 main 函数隐藏的命令行参数切入,进而深度剖析环境变量的底层指针数组结构、三大核心获取手段 。好,我们开始。

零、 概念补充:命令行参数

0.1 重新认识 main 函数

0.1.1main函数不是程序的开始

  main函数不是开始,也许你的老师经常说“main函数是代码的开始。”但是这是对于程序员而言的。vim code.s我们打开汇编代码:
在这里插入图片描述
  Linux上是这个start,Windows上是_crt_start,这个函数内部调用main函数的逻辑如下

_start
{
    int ret = 0;
    int arg_count=0;
    arg_count=3;
    if(arg_count == 0)
        ret = main();
    else if(arg_count==2)
        ret = main(argc, argv);
    else
        ret =main(argc, argv, env);
}

  扫描我们写的main函数,通过词法分析和语法分析得到这个count值是多少 然后调用不同的main函数。

0.1.2main函数也有参数

  在初学 C/C++ 编程时,我们接触最多的 main 函数通常是不带任何参数的。然而在 Linux 系统级编程中,真正的 main 函数其实是可以携带参数的。最常见的声明方式如下:

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

  这两个参数在程序启动时由操作系统自动传递。其中,argc是一个整型变量,用来记录命令行参数的个数;而 argv则是一个指向字符指针数组的指针,用来存储具体的命令行参数字符串。
  有种写法要知道一下:

int main(int argc, char *argv[], char *env[]){
    (void)argc;//为什么这么写: 应用一下变量,取消告警,
    //你设置了但是没有用可能会发出警告
    (void)argv;
    (void)env;
}

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

  当我们在终端键入一个诸如 ls -a -b -c 的命令并按下回车时,终端解释器 bash 会首先拿到这一串完整的输入字符串。随后,bash 会以空格作为分隔符,将这个长字符串打散成数个子字符串。
  这些被切分好的字符串会被填入一张表中。以输入 ./code a b c 为例,系统内部构建的指针数组结构如图所示:

char *argv[]:
+---+    +----------------+
| 0 | -->| "./code"       |
+---+    +----------------+
| 1 | -->| "a"            |
+---+    +----------------+
| 2 | -->| "b"            |
+---+    +----------------+
| 3 | -->| "c"            |
+---+    +----------------+
| 4 | -->| NULL           |
+---+    +----------------+

  bash 在切分字符串并构建该数组时,会在数组的末尾强制追加一个 NULL 指针作为结束标记。此时,参数的总个数 argc 的值为 4。

通过这种设计,进程便能清晰地知道自己拥有这样一张命令行参数表。(这很重要,我们后面会总结)

0.3 命令行参数的应用演示

  为什么要大费周章地把命令行参数传给 main 函数呢?其根本目的在于实现同一个程序的不同子功能。在 Linux 中,我们经常使用的 ls -als -l 等,本质上都是通过解析命令行参数表来实现的。
  以下是一段 C++ 的演示代码,展示了如何通过不同的选项让程序支持不同的子功能:

#include <iostream>
#include <string>
using namespace std;

// 演示:如何遍历打印所有的命令行参数
void Test01(int _argc, char* _argv[])
{
    for (int i = 0; i < _argc; i++)
    {
        cout << _argv[i] << endl;
    }
}

// 演示:不同的功能是怎么通过命令行参数选项实现的
void Test02(int _argc, char* _argv[])
{
    if (_argc != 2)
    {
        cout << "输入错误!请重新输入" << endl;
    }
    else
    {
        string cmd = _argv[1];
        if (cmd == "a")
            cout << "执行一号功能" << endl;
        if (cmd == "b")
            cout << "执行二号功能" << endl;
        if (cmd == "c")
            cout << "执行三号功能" << endl;
    }
}

int main(int argc, char *argv[])
{
    // Test01(argc, argv); // 取消注释可查看参数遍历结果
    Test02(argc, argv);
    return 0;
}

在这里插入图片描述

  通过这段代码可以直观地发现,./code 自身作为 _argv[0] 也是一个命令行参数。通过接收并判断 _argv[1] 传入的选项(如 abc),程序就能够走向不同的分支,从而获得不同的子功能。

一、 引入环境变量

1.1基本概念

1.1.1环境变量的概念

  • 环境变量(environment variables) —— 一般是指在操作系统中用来指定操作系统运行环境的一些参数。名称=内容形式
  • 如: 我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

1.1.2常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
  • SHELL : 当前 Shell,它的值通常是 /bin/bash

1.1.3怎么看环境变量

命令 作用
echo $VAR 快速精准查看某一个变量的值
env 观察所有环境变量,常配合 | grep 过滤
set 查所有本地变量
export 不带参数可查看所有环境变量列表

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

  在Linux Shell中,变量主要分为本地变量(普通自定义变量)和环境变量(全局变量)。理解它们的区别以及如何转换是掌握Shell编程的基础。

1.1.4.1核心概念对比
变量类型 作用域 特点
本地变量 仅限当前Shell进程 子进程无法继承该变量。
环境变量
(全局变量)
当前Shell进程及其所有子进程 全局有效,常用于传递系统配置。

本地变量是给Shell脚本局部变量用的。我们不讲Shell脚本。 (面试不怎么考)

算了我简单讲一下吧:
  bash如何使用本地变量?bash是一个交互式的命令行解释器。
  它也是一门解释性语言。这门解释性语言它也有自己的词法语法分析我们的shell脚本就是这种语言的一种应用:

[whb@bite-alicloud lesson15]$ i=0; while [ $i -le 10 ]; do echo $i; let i++; done
0
1
2
3
4
5
6
7
8
9
10
[whb@bite-alicloud lesson15]$

  Shell脚本也可以这样写: 我们用一个文件写下很多的命令,文件以#! /bin/bash开头,这就是shell脚本。我们有些配置文件就是用Shell脚本写的。

1: test.sh
1 #!/bin/bash
2 
3 touch file
4 mv file myfile
5 i=100
6 echo $i

在这里插入图片描述

1.1.4.2变量的操作指令

① 定义本地变量
直接使用 变量名=值 即可定义本地变量。
等号两边绝对不能有空格。如果值包含空格,需用双引号包裹。

my_var="hello world"
i=10

② export:导入为环境变量(全局变量)
使用 export 命令可以将一个本地变量提升为环境变量,或者在定义时直接声明为环境变量。

  • 先定义,后导出 ,或者直接导出。
var1="ubuntu"
export var1  
#或者:
export var2="centos"

在这里插入图片描述

③ unset:删除变量
使用 unset 命令可以清理不再需要的变量。无论是本地变量还是环境变量,都可以被删除。

# 删除刚才定义的环境变量
unset var2
# 验证是否删除(输出为空)
echo $var2

1.2 main 函数的第三个参数

  你觉得命令行参数只有两个吗?不,还有一个:main 函数的完整形态实际上可以包含三个参数最多三个,从父进程继承过来),其原型的完全体代码如下:

int main(int argc, char *argv[], char *env[]);

  这第三个参数 char *env[] 便是环境变量表。与命令行参数表类似,它也是一个字符指针数组,数组的每一个元素都指向一个形如 NAME=value环境变量字符串,并且同样以 NULL 指针作为整张表的结尾。

我们输出一个结论: 一个进程内部有两张表:一张叫命令行参数表,一张叫环境变量表。(我们在后文中继续深入了解他们)
父进程把它的两张表传递给子进程。

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

  要想在 Linux 中运行一个可执行程序,核心的前提只有一条:系统必须先找到它

  当我们键入 ls 并回车时,系统之所以能立马响应,是因为 ls 对应的二进制可执行文件原本就存放在系统的 /usr/bin/ 目录下。(核心指令目录
在这里插入图片描述

  那么,问题又来了:“为什么是 /usr/bin/ ,而不是别的目录?
  系统内部定义了一个名为 PATH 的全局环境变量,它专门用来存储系统搜索可执行指令的默认路径集合。我们可以通过在终端输入 echo $PATH 来查看它的内容(后面这种用法,我们先用着):

[zbc@VM-0-9-opencloudos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin

  当我们在命令行输入一个指令时,命令行解释器 bash 就会顺着 PATH 中配置的路径,从左到右依次去对应的目录下遍历、搜寻同名的二进制文件。(对!查找工作也是bash完成的)因此可以找到。我们自己的程序所在的目录不在PATH里面,所以找不到。
  为了解决这个问题,我们必须通过 ./ 显式地告诉系统:请直接在当前的绝对/相对路径下查找并运行该程序。这就是为什么运行我们自己的程序时,必须要加上 ./code 的根本原因。

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

  我们上面讲的那什么?环境变量表。你肯定还是一头雾水。我们来集中研究一下:

2.1 环境变量表的组织形式

  当我们通过终端登录 Linux 系统时,系统会为我们启动一个 bash 进程。如果同时有 10 个用户登录,系统内就会存在 10 个独立的 bash 进程,各自维护一套环境变量表。环境变量表以NULL结尾(这个特性可以用来做代码的判断条件

char *env[]:
+---+    +------------------------------------+
| 0 | -->| "XDG_SESSION_ID=14400"             |
+---+    +------------------------------------+
| 1 | -->| "HOSTNAME=bite-alicloud"           |
+---+    +------------------------------------+
| 2 | -->| "TERM=xterm"                       |
+---+    +------------------------------------+
| 3 | -->| "SHELL=/bin/bash"                  |
+---+    +------------------------------------+
| ...| -->| ...                                |
+---+    +------------------------------------+
| n | -->| "PATH=/usr/local/bin:/usr/bin:..." |
+---+    +------------------------------------+
|n+1| -->| NULL                               |
+---+    +------------------------------------+

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

  想一想?我们能不能改环境变量? 比如,你可以在终端直接输入:

[zbc@VM-0-9-opencloudos ~]$ PATH=    # 强行把 PATH 清空
[zbc@VM-0-9-opencloudos ~]$ ls
-bash: ls: No such file or directory

  完啦!哈哈!这个时候再去执行一些命令,你会发现很多命令都不能用了(这种修改是覆盖式修改,你把原来的环境变量改没了,还能执行的叫内建命令,我们后面会讲)
  有一种追加式修改的方法:

[whb@bite-alicloud lesson15]$ PATH=$PATH:/home/whb/code/code/merge_class/lesson15
[whb@bite-alicloud lesson15]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/whb/.local/bin:/home/whb/bin:/home/whb/code/code/merge_class/lesson15
[whb@bite-alicloud lesson15]$

  这时候你只需要把终端关掉,重新连一下就恢复了
  那么,问题又来了:“如果我就是想增加一个永久性的环境变量,或者永久修改 PATH,该怎么办呢?

  先说结论:不建议改

  但是,为了搞懂底层的原理,我们引出配置文件。每次我们重新打开终端、成功登录 Linux 系统时,命令行解释器 bash 进程在被拉起的那一瞬间,会到当前用户的家目录(~)底下去读取几个隐藏的脚本文件。执行一遍,然后在内存中动态构建 出那张初始的环境变量表。我们知道bash也是一个进程,这个初始的环境变量表就是bash进程内部的环境变量表 。这里面最核心的两个就是:

~/.bash_profile
~/.bashrc

在这里插入图片描述

  不仅如此,~/.bashrc 还会进一步去调用系统级别的 /etc/bashrc。在这些文件里,早就写好了默认的环境变量导出指令(比如 export PATH=...)。(它就是个shell脚本,没什么了不起的
在这里插入图片描述

在这里插入图片描述
  .bash_profile调用.bashrc,,.bashrc调用系统的.bashrc
在这里插入图片描述

  设置完你得重新登录一下你的机器。

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

在这里插入图片描述
在这里插入图片描述

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

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

  用前面提到的 main 函数完全体参数。利用循环遍历该指针数组,直到遇到 NULL 为止:

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

没人会这么用

3.2 方法二:通过系统调用 API getenv定向获取

  方法一虽然能拿到全貌,但不能获取某个特定的环境变量,此时可以使用 C 标准库提供的 getenv 接口:

#include <stdio.h>
#include <stdlib.h>
int main(){
    char *user = getenv("USER");
    if (user != NULL)
        printf("Current user is: %s\n", user);
    return 0;
}

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

  如果不想修改 main 函数的参数声明,同时又想遍历完整的环境变量,C 语言提供了一个全局的外部指针 environ。它在 <unistd.h> 中声明(我们要手动进行 extern 声明

#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 方法四:子进程继承父进程获取环境变量

  前三种方法关注的是如何在当前进程中读取环境变量,而方法四探讨的是环境变量的来源与传递机制。在 Linux 系统中,环境变量具有全局属性,这种全局性是通过子进程默认会继承父进程的环境变量表来实现的。

关注两个结论:环境变量具有全局属性,环境变量可以被继承(包括原有的和倒给它的)。
如何理解【全局】这个字:所有的进程都能拿到bash的环境变量,如图:

在这里插入图片描述

3.4.1 核心原理

  当我们通过 fork() 系统调用创建子进程时,子进程会复制父进程的绝大部分数据,其中就包括环境变量表
  注意写时拷贝:子进程在刚被创建时,其 environ 指针指向的内存空间与父进程完全一致。

3.4.2 示例代码

  以下代码演示了父进程通过 putenvsetenv 自定义一个环境变量,然后通过 fork 创建子进程。子进程无需显式设置,便能直接继承并读取到该变量:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
    // 1. 父进程在自己的独立空间内设置一个自定义环境变量
    // setenv 参数:键, 值, 是否覆盖(1表示覆盖)
    setenv("MY_TEST_VAR", "Hello_Linux_Process", 1);
    printf("[父进程] 成功设置环境变量 MY_TEST_VAR\n");
    // 2. 创建子进程
    pid_t id = fork();
    
    if (id < 0) {
        perror("fork failed");
        return 1;
    } 
    else if (id == 0) {
        // 3. 子进程逻辑
        printf("[子进程] 开始尝试获取父进程留下的遗产...\n");
        
        // 子进程直接调用 getenv 获取父进程在 fork 前设置的变量
        char *val = getenv("MY_TEST_VAR");
        if (val != NULL) {
            printf("[子进程] 成功继承! 获取到的值为: %s\n", val);
        } else {
            printf("[子进程] 未找到该环境变量。\n");
        }
        exit(0);
    } 
    else {
        // 父进程等待子进程退出
        wait(NULL);
    }
    return 0;
}

四、其他环境变量

4.1所有环境变量罗列

  我给重点的加了红色(准确点说是:“常用”)

环境变量名 变量值 / 含义说明
SHELL /bin/bash (当前使用的 Shell 解析器)
HISTCONTROL ignoredups (忽略连续重复的命令历史记录)
HISTSIZE 1000 (历史命令保存的最大条数)
HOSTNAME VM-0-9-opencloudos (主机名)
PWD /home/zbc (当前工作目录)
LOGNAME zbc (登录的用户名)
XDG_SESSION_TYPE tty (当前会话类型为 TTY 终端)
MOTD_SHOWN pam (已通过 PAM 模块展示过每日欢迎信息)
HOME /home/zbc (当前用户的家目录)
LANG en_US.UTF-8 (系统的语言与编码环境)
LS_COLORS rs=0:di=01;34:ln=01;36:... (因长度过长,此处省略了具体的各种文件类型颜色配置文件代码)
SSH_CONNECTION 223.104.72.236 58438 10.1.0.9 22 (SSH 连接的客户端 IP、端口及服务端 IP、端口)
XDG_SESSION_CLASS user (会话类别为普通用户)
TERM xterm (终端仿真器类型)
LESSOPEN `
USER zbc (当前用户名)
GOPROXY https://mirrors.tencent.com/go,direct (Go 语言的腾讯云代理源设置)
SHLVL 1 (Shell 的嵌套层级)
XDG_SESSION_ID 15098 (当前 XDG 会话 ID)
XDG_RUNTIME_DIR /run/user/1001 (当前用户运行时的临时数据目录)
SSH_CLIENT 223.104.72.236 58438 22 (SSH 客户端的 IP、端口和服务器端口)
which_declare declare -f (内部变量,定义了 which 命令声明函数的方式)
PATH /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin (系统查找可执行命令的路径环境变量)
DBUS_SESSION_BUS_ADDRESS unix:path=/run/user/1001/bus (D-Bus 系统会话总线地址)
MAIL /var/spool/mail/zbc (当前用户的系统邮件信箱路径)
SSH_TTY /dev/pts/0 (当前 SSH 会话所绑定的虚拟终端伪设备)
BASH_FUNC_which%% () { ( alias; eval ${which_declare} ) | /usr/bin/which ... } (导出的 Bash 自定义 which 函数)
_ /usr/bin/env (上一个执行的命令或当前拉起进程的工具路径)
OLDPWD /home/student/project (上一次所在的工作目录路径,系统借此实现 cd - 运行时的快捷回跳)

4.1.1USER和LOGNAME的区别

  我用一种最省事的方式告诉你他们的区别:
  USER是我当前用户是谁。LOGNAME是我登录的时候是谁。这两个一般都是一致的,但是有一种情况不一致:
在这里插入图片描述
在这里插入图片描述

4.1.2历史命令与HISTSIZE

  bash可以记录我们的历史命令,于是我们就可以调用如下方式找到我们的历史命令

  • ctrl + r
  • ! v
  • 上下键。
  • history
  • history | wc -l 表示当前系统里保存的历史命令条数

  我来一个一个演示一下:

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

  在 Linux 环境编程中,我们可以通过获取进程的环境变量或用户身份标识,来实现特定用户的权限绑定。

4.2.1核心逻辑与安全防范

  编写此类程序时,仅依赖 getenv("USER") 是不够安全的。原因如下:

  1. 环境变量可伪造:任何用户(尤其是 root)都可以在终端通过 export USER=your_username 临时修改该变量,从而绕过简单的字符串检查。
  2. UID 的唯一性:在 Linux 系统中,用户的 UID(User ID)是硬性绑定的,root 用户的 UID 永远为 0。通过 getuid() 系统调用可以获取真实的当前用户 ID。

  因此,最稳固的方案是双重验证:既要匹配 USER 环境变量,又要排除 root 的 UID(或者直接限定为你的专属 UID)。

4.2.2.1示例代码

  以下是完整的 C++ 示例代码。请将代码中的 "your_username" 替换为你自己的 Linux 用户名。

#include <iostream>
#include <cstdlib>   // 包含 getenv
#include <unistd.h>  // 包含 getuid, geteuid
#include <string>

int main() {
    // 1. 定义允许执行该程序的专属用户名
    const std::string AUTHORIZED_USER = "your_username";

    // 2. 获取当前环境变量中的 USER 值
    const char* env_user = std::getenv("USER");
    std::string current_user = env_user ? env_user : "";

    // 3. 获取当前进程的实际用户 ID (UID)
    uid_t current_uid = getuid();

    // 4. 核心安全检查:
    //    - 检查环境变量 USER 是否匹配
    //    - 确保当前用户不是 root (root 的 UID 永远为 0)
    if (current_user == AUTHORIZED_USER && current_uid != 0) {
        // 正确的逻辑分支
        std::cout << "[验证通过] 欢迎回来," << AUTHORIZED_USER << "!" << std::endl;
        std::cout << "执行核心绝密逻辑... 正确答案是:42" << std::endl;
    } 
    else {
        // 其他人(包括 root)或环境变量被篡改时的分支
        std::cout << "[错误] 权限不足:该程序已被锁定,仅限指定用户执行。" << std::endl;
        // 故意输出误导性的错误信息或静默退出
        return 1;
    }
    return 0;
}

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

  除了系统自带的环境变量外,我们还可以通过自定义环境变量来控制程序的行为(例如作为一种简单的软件激活凭证或 Debug 开关)。

4.2.3.1示例代码

  这就是利用进程级的数据传递来控制子进程的一些控制逻辑

#include <iostream>
#include <cstdlib>  // 包含 getenv
#include <string>

int main() {
    // 1. 获取自定义的 FLAG 环境变量
    const char* env_flag = std::getenv("FLAG");
    std::string flag_val = env_flag ? env_flag : "";

    // 2. 控制逻辑:仅当 FLAG 显式设置为 "1" 时允许运行
    if (flag_val == "1") {
        std::cout << "[SUCCESS] FLAG 验证成功,程序开始正常运行!" << std::endl;
        // 核心业务逻辑...
    } else {
        std::cout << "[ERROR] 缺失关键环境变量或参数不正确,程序终止。" << std::endl;
        return 1;
    }

    return 0;
}

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐