【Linux 系统篇(十八)】进程(六) :环境变量


大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇


Linux 系统篇(十八) ------ 环境变量

文章目录

  • [Linux 系统篇(十八) ------ 环境变量](#Linux 系统篇(十八) —— 环境变量)
    • 一、命令行参数
      • [1.1 重新认识 main 函数](#1.1 重新认识 main 函数)
        • [1.1.1 main 函数不是程序的开始](#1.1.1 main 函数不是程序的开始)
        • [1.1.2 main 函数的参数](#1.1.2 main 函数的参数)
      • [1.2 命令行参数的切分与表结构](#1.2 命令行参数的切分与表结构)
      • [1.3 命令行参数的应用演示](#1.3 命令行参数的应用演示)
    • 二、环境变量初识:概念与常见变量
      • [2.1 环境变量的概念](#2.1 环境变量的概念)
      • [2.2 常见环境变量](#2.2 常见环境变量)
      • [2.3 查看环境变量的方法](#2.3 查看环境变量的方法)
        • [2.3.1 echo:查看单个变量](#2.3.1 echo:查看单个变量)
        • [2.3.2 env:查看所有环境变量](#2.3.2 env:查看所有环境变量)
        • [2.3.3 set:查看所有变量(包括本地变量和环境变量)](#2.3.3 set:查看所有变量(包括本地变量和环境变量))
        • [2.3.4 export:不带参数时也可查看环境变量列表](#2.3.4 export:不带参数时也可查看环境变量列表)
      • [2.4 本地变量与环境变量的区别与转换](#2.4 本地变量与环境变量的区别与转换)
        • [2.4.1 定义本地变量](#2.4.1 定义本地变量)
        • [2.4.2 导出为环境变量](#2.4.2 导出为环境变量)
        • [2.4.3 删除变量](#2.4.3 删除变量)
      • [2.5 实验:为什么执行自己的程序必须加 `./`?](#2.5 实验:为什么执行自己的程序必须加 ./?)
    • 三、环境变量的底层组织
      • [3.1 环境变量表的结构](#3.1 环境变量表的结构)
      • [3.2 环境变量的内存级修改与磁盘级修改](#3.2 环境变量的内存级修改与磁盘级修改)
        • [3.2.1 内存级修改(临时修改)](#3.2.1 内存级修改(临时修改))
        • [3.2.2 磁盘级修改(永久修改)](#3.2.2 磁盘级修改(永久修改))
      • [3.3 环境变量的全局属性与继承](#3.3 环境变量的全局属性与继承)
    • 四、在代码中获取和操作环境变量
      • [4.1 通过 main 函数第三个参数获取](#4.1 通过 main 函数第三个参数获取)
      • [4.2 通过 getenv 函数获取指定变量](#4.2 通过 getenv 函数获取指定变量)
      • [4.3 通过全局变量 environ 获取](#4.3 通过全局变量 environ 获取)
      • [4.4 通过子进程继承获取环境变量](#4.4 通过子进程继承获取环境变量)
      • [4.5 设置环境变量的函数:setenv 和 putenv](#4.5 设置环境变量的函数:setenv 和 putenv)
    • 五、其他重要环境变量与实用技巧
      • [5.1 所有环境变量罗列与解释](#5.1 所有环境变量罗列与解释)
      • [5.2 USER 与 LOGNAME 的区别](#5.2 USER 与 LOGNAME 的区别)
      • [5.3 历史命令与 HISTSIZE](#5.3 历史命令与 HISTSIZE)
    • 六、总结与面试要点
      • [6.1 环境变量的基本概念](#6.1 环境变量的基本概念)
      • [6.2 操作环境变量的命令](#6.2 操作环境变量的命令)
      • [6.3 为什么执行自己的程序要加 `./`?](#6.3 为什么执行自己的程序要加 ./?)
      • [6.4 环境变量的组织方式](#6.4 环境变量的组织方式)
      • [6.5 在代码中获取环境变量的方法](#6.5 在代码中获取环境变量的方法)
      • [6.6 本地变量和环境变量的区别](#6.6 本地变量和环境变量的区别)
      • [6.7 如何让环境变量永久生效?](#6.7 如何让环境变量永久生效?)
      • [6.8 环境变量的全局性如何理解?](#6.8 环境变量的全局性如何理解?)
    • 结语:


一、命令行参数

1.1 重新认识 main 函数

1.1.1 main 函数不是程序的开始

  很多教材都说"main 是程序的入口",这句话对程序员来说没错,但从底层看,main 并不是程序启动后执行的第一行代码。

  我们可以查看汇编代码来验证。编写一个最简单的 C 程序:

c 复制代码
#include <stdio.h>
int main()
{
    printf("hello world!\n");
    return 0;
}

  编译后生成可执行文件,用 objdump -d 或 gdb 查看入口点,你会发现程序并不是从 main 开始的。在 Linux 上,程序的入口点是 _start,在 Windows 上则是 _crt_start(或类似)。这个入口函数由 C 运行时库(CRT)提供,它负责初始化进程环境、准备参数,然后才调用 main。

  _start 内部的大致逻辑如下(伪代码):

c 复制代码
void _start()
{
    int ret;
    int arg_count = /* 分析 main 函数签名,得到参数个数 */;
    
    if (arg_count == 0)
        ret = main();
    else if (arg_count == 2)
        ret = main(argc, argv);
    else
        ret = main(argc, argv, env);
    
    exit(ret);
}

  编译器在编译时会对 main 函数进行词法、语法分析,判断它声明了几个参数,然后生成不同的调用代码。因此,main 函数可以有 0 个、2 个或 3 个参数,分别对应无参、命令行参数、命令行参数+环境变量。

1.1.2 main 函数的参数

  在初学 C/C++ 时,我们大多使用无参的 main。但在 Linux 系统编程中,main 函数最多可以有三个参数:

c 复制代码
int main(int argc, char *argv[], char *env[]);
  • argc:命令行参数的个数(argument count)。
  • argv:字符串指针数组,保存各个参数(argument vector)。
  • env:环境变量字符串指针数组(environment vector)。

  其中 argc 和 argv 是最常见的,env 则与环境变量有关。这三个参数在程序启动时由操作系统(更准确说是父进程和内核)自动传递。

  有时我们在代码中声明了参数但未使用,编译器可能会警告。为了避免警告,可以显式地"使用"一下,比如:

c 复制代码
int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    (void)env;
    // 其他代码
    return 0;
}

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

  当我们在终端输入一条命令,比如:

bash 复制代码
ls -a -l /home 

  按下回车后,Shell(如 bash)会先接收这一整行字符串,然后以空格为分隔符 将其切分成多个子字符串。这些子字符串会被放入一个指针数组中,数组末尾追加一个 NULL 指针作为结束标记。

  以 ./code a b c 为例,系统内部构建的 argv 结构如下:

  此时 argc 的值是 4(包括程序名本身)。注意,argv[0] 始终是程序本身的路径(可能是相对路径或绝对路径),后面的才是用户传入的参数。

1.3 命令行参数的应用演示

  为什么要费这么大劲传递命令行参数?目的是让同一个程序通过不同的选项实现不同功能。比如 ls -a 和 ls -l 就是通过解析不同的选项来输出不同格式的目录信息。

  下面是一段 C 演示代码,展示如何遍历参数和根据选项执行不同逻辑:

cpp 复制代码
  1 #include<stdio.h>
  2 #include<string.h>
  3 
  4 int main(int argc,char *argv[])
  5 {
  6     //遍历打印所有的命令行参数
  7     //for(int i = 0;i < argc;i++)
  8     //{
  9     //  printf("argv[%d]: %s\n",i,argv[i]);
 10     //}
 11 
 12     if(argc != 2)
 13     {
 14         printf("Usage: %s [-a|-b|-c]\n",argv[0]);
 15         return 1;
 16     }
 17 
 18     const char* arg = argv[1];
 19 
 20     if(strcmp(arg,"-a")==0)
 21         printf("这是功能1\n");
 22     else if(strcmp(arg,"-b")==0)
 23         printf("这是功能2\n");
 24     else if(strcmp(arg,"-c")==0)
 25         printf("这是功能3\n");
 26     else
 27         printf("Usage: %s [-a|-b|-c]\n",argv[0]);
 28 
 29     return 0;
 30 }

编译运行:

  通过命令行参数,我们可以轻松实现类似 ls 这样的多选项程序。


二、环境变量初识:概念与常见变量

2.1 环境变量的概念

  环境变量(Environment Variables)是操作系统中用来指定运行环境的一些参数。它们以 名称=值 的形式存在,例如 PATH=/usr/bin:/bin。环境变量通常具有全局属性,可以被当前 Shell 及其启动的所有子进程继承。

  一个典型的例子:我们在编译 C/C++ 代码时,链接器需要找到动态库或静态库的位置,但我们通常不需要手动指定库路径,为什么?因为系统通过环境变量(如 LIBRARY_PATH、LD_LIBRARY_PATH 等)提供了默认搜索路径,编译器会自动在这些路径中查找所需的库文件。

2.2 常见环境变量

变量名 含义
PATH 指定可执行程序的搜索路径,多个路径用冒号 : 分隔
HOME 当前用户的主工作目录(登录后默认所在目录)
SHELL 当前使用的 Shell 类型,通常为 /bin/bash
USER 当前登录的用户名
LOGNAME 登录用户名(与 USER 基本相同,但某些情况下不同)
PWD 当前工作目录
OLDPWD 上一次所在的工作目录(用于 cd -)
HOSTNAME 主机名
TERM 终端类型
LANG 系统语言和编码
HISTSIZE 历史命令保存的最大条数
SSH_CONNECTION SSH 连接信息(客户端 IP、端口、服务端 IP、端口)

2.3 查看环境变量的方法

2.3.1 echo:查看单个变量
bash 复制代码
echo $HOME
echo $PATH

  注意 $ 用于引用变量的值。

2.3.2 env:查看所有环境变量
bash 复制代码
env

  输出会很长,可以配合 grep 过滤:

bash 复制代码
env | grep PATH
2.3.3 set:查看所有变量(包括本地变量和环境变量)
bash 复制代码
set

  set 会输出当前 Shell 中定义的所有变量,包括环境变量和本地变量。

2.3.4 export:不带参数时也可查看环境变量列表
bash 复制代码
export

  输出结果与 env 类似,但可能包含一些额外的变量(如 Shell 函数等)。

2.4 本地变量与环境变量的区别与转换

  在 Shell 中,变量分为本地变量 (local variable)和环境变量(environment variable)。

  • 本地变量 :仅在当前 Shell 进程中有效,子进程无法继承。例如在终端中执行 myvar=100,这个变量只在当前 Shell 中存在。
  • 环境变量 :当前 Shell 及其所有子进程都能访问。通过 export 命令可以将本地变量导出为环境变量。
2.4.1 定义本地变量
bash 复制代码
my_var="hello world"
i=1

注意:等号两边不能有空格;如果值包含空格,需要用双引号或单引号包裹。

2.4.2 导出为环境变量
bash 复制代码
# 先定义后导出
var1="ubuntu"
export var1

# 或者直接定义并导出
export var2="centos"
2.4.3 删除变量
bash 复制代码
unset var1
unset var2

  删除后变量就不存在了,可以通过 echo $var1 验证(输出为空)。

2.5 实验:为什么执行自己的程序必须加 ./?

  假设我们写了一个简单的程序 hello,编译后生成可执行文件。如果直接输入 hello,会提示 command not found,但输入 ./hello 就能运行。这是为什么呢?

  原因在于 PATH 环境变量。当我们输入一个命令时,Shell(bash)会在 PATH 变量指定的目录列表中从左到右搜索与该命令同名的可执行文件。如果找到,就执行;否则报错。

  查看 PATH 的内容:

  可以看到,系统默认只搜索这些目录。我们自己编译的程序通常放在当前目录或自定义目录,而这些目录并没有包含在 PATH 中,所以直接输入 hello 时 Shell 找不到它。

  解决办法有几种:

  1. 使用相对路径 ./hello 或绝对路径 /path/to/hello,显式告诉 Shell 程序的位置。
  2. 将程序所在目录添加到 PATH 中(临时或永久)。
  3. 将程序复制或链接到 PATH 中的某个目录(如 /usr/local/bin)。

实验:将程序路径加入 PATH

bash 复制代码
# 假设 hello 程序位于 /home/user/mybin 目录下
export PATH=$PATH:/home/user/mybin

  然后直接输入 hello 就能运行了。注意这种修改只在当前终端会话有效,重新打开终端后会失效(因为 PATH 是从配置文件加载的)。

实验:对比 root 和普通用户的 HOME

bash 复制代码
# root 用户
$ echo $HOME
/root

# 普通用户(如 student)
$ echo $HOME
/home/student

  HOME 变量决定了用户登录后默认进入的目录。执行 cd ~ 实际上就是切换到 $HOME 指定的目录。


三、环境变量的底层组织

3.1 环境变量表的结构

  每个进程在启动时都会收到一张环境变量表 (environment table)。这张表是一个字符指针数组,每个指针指向一个以 '\0' 结尾的字符串,字符串格式为 NAME=value。数组的末尾是一个 NULL 指针,作为结束标记。

3.2 环境变量的内存级修改与磁盘级修改

3.2.1 内存级修改(临时修改)

  在终端中直接修改变量的值,只影响当前 Shell 进程及其后续子进程,不会改变配置文件,因此重新打开终端后修改就会消失。

  例如,清空 PATH:

  可以看到,清空 PATH 后,连 ls 都找不到了(因为 Shell 不知道去哪个目录找 ls)。但注意,一些 Shell 内建命令(如 cd、echo、export)仍然可以使用,因为它们不是通过 PATH 查找的外部程序,而是 Shell 自身提供的。

  恢复 PATH 需要重新登录或手动设置。

  追加式修改更常见,例如:

bash 复制代码
export PATH=$PATH:/home/user/mybin

  这样就在原有 PATH 基础上添加了一个新目录,不会丢失原有内容。

3.2.2 磁盘级修改(永久修改)

  如果希望修改永久生效,需要修改配置文件。Linux 系统中,当用户登录时,Shell 会读取一些初始化脚本,设置环境变量。最重要的两个用户级配置文件是:

  • ~/.bash_profile(或 ~/.profile)
  • ~/.bashrc

  通常,~/.bash_profile 会调用 ~/.bashrc,而 ~/.bashrc 又会调用系统级的 /etc/bashrc。在这些文件中,我们可以使用 export 命令来设置环境变量。

  例如,在 ~/.bashrc 末尾添加:

bash 复制代码
export MY_CUSTOM_VAR="hello"
export PATH=$PATH:/home/user/mybin

  保存后,重新登录或执行 source ~/.bashrc 使修改立即生效。

  不建议随意修改系统级配置文件 ,尤其是 PATH,因为错误的修改可能导致系统命令无法使用。一般只在用户级配置文件中添加自定义路径。

3.3 环境变量的全局属性与继承

  环境变量之所以称为"全局",是因为它可以被子进程继承。当一个进程通过 fork() 创建子进程时,子进程会复制父进程的环境变量表。这意味着父进程设置的环境变量,子进程可以直接访问。

实验:验证环境变量的继承

  编写一个程序,打印自定义环境变量 MYENV 的值:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *env = getenv("MYENV");
    if (env)
        printf("MYENV = %s\n", env);
    else
        printf("MYENV not found\n");
    return 0;
}

  编译后,先直接运行:

  因为当前环境中没有这个变量。

  然后导出环境变量再运行:

  这说明子进程(我们的程序)继承了父进程(bash)的环境变量。

注意:本地变量不能被继承

  如果只是定义本地变量而不导出:

  因为 MYENV 只是 bash 的本地变量,不会传递给子进程。


四、在代码中获取和操作环境变量

4.1 通过 main 函数第三个参数获取

  main 函数的完全体可以有三个参数,第三个参数就是环境变量表:

c 复制代码
#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;
}

  这种方法可以遍历所有环境变量,但实际开发中很少使用,因为它要求修改 main 签名,而且不方便获取单个变量。

4.2 通过 getenv 函数获取指定变量

  C 标准库提供了 getenv 函数,用于获取指定名称的环境变量:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char *path = getenv("PATH");
    if (path)
        printf("PATH = %s\n", path);
    else
        printf("PATH not found\n");
    return 0;
}

  getenv 返回指向环境变量值的指针,如果变量不存在则返回 NULL。注意:不要修改返回的字符串,也不要 free,因为它指向的是进程环境表中的内存。

4.3 通过全局变量 environ 获取

  C 库提供了一个全局二级指针 environ,它指向环境变量表的首地址。使用前需要手动声明:

c 复制代码
#include <stdio.h>

extern char **environ;

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

  这种方式与 main 第三个参数类似,但不需要修改 main 签名。

4.4 通过子进程继承获取环境变量

  前三种方法都是在当前进程内读取环境变量。而环境变量的全局性体现在子进程自动继承父进程的环境变量表。我们可以利用这一特性,在父进程中设置环境变量,然后 fork 创建子进程,子进程就能直接读取。

示例代码:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    // 父进程设置环境变量
    setenv("MY_TEST_VAR", "Hello_Linux_Process", 1);
    printf("[父进程] 已设置环境变量 MY_TEST_VAR\n");

    fflush(NULL); // 刷新缓冲区,防止子进程重复打印

    // 创建子进程
    pid_t id = fork();

    if (id == 0) {
        // ===== 子进程 =====
        printf("[子进程] 开始运行\n");

        char *val = getenv("MY_TEST_VAR");
        if (val != NULL)
            printf("[子进程] 继承成功,值为: %s\n", val);
        else
            printf("[子进程] 没找到这个变量\n");

    } else {
        // ===== 父进程 =====
        wait(NULL); // 等待子进程结束
        printf("[父进程] 子进程已结束\n");
    }

    return 0;
}

4.5 设置环境变量的函数:setenv 和 putenv

  除了在 Shell 中使用 export,在 C 代码中也可以设置环境变量,常用函数有:

  • int setenv(const char *name, const char *value, int overwrite);
    • 若 overwrite 非 0,则覆盖已存在的同名变量;否则不覆盖。
  • int putenv(char *string);
    • 参数形式为 "NAME=value",直接修改环境表,使用需谨慎(字符串必须保持有效)。

  这些函数会影响当前进程及其后续子进程,但不会影响父进程(如 Shell)。


五、其他重要环境变量与实用技巧

5.1 所有环境变量罗列与解释

  下面是一个典型 Linux 系统中 env 输出的变量列表,以及它们的含义(部分):

变量名 含义
SHELL 当前 Shell 解析器,如 /bin/bash
HISTCONTROL 历史命令控制,如 ignoredups 忽略连续重复命令
HISTSIZE 历史命令保存的最大条数,如 1000
HOSTNAME 主机名
PWD 当前工作目录
LOGNAME 登录用户名
XDG_SESSION_TYPE 会话类型,如 tty
HOME 用户主目录
LANG 系统语言编码,如 en_US.UTF-8
LS_COLORS ls 命令的颜色配置
SSH_CONNECTION SSH 连接信息
TERM 终端类型
USER 当前用户名
PATH 可执行程序搜索路径
MAIL 用户邮箱路径
OLDPWD 上一次所在目录
_ 上一个执行的命令或程序的路径

5.2 USER 与 LOGNAME 的区别

  大多数情况下,USER 和 LOGNAME 的值相同,都表示当前用户名。但它们在特定场景下可能不同:

  • LOGNAME 是用户登录时确定的用户名,通常不可更改。
  • USER 是当前进程的有效用户名,可以通过 su 或 sudo 切换用户而改变。

5.3 历史命令与 HISTSIZE

  Bash 会记录用户输入过的命令,以便通过上下键或 ctrl+r 快速查找。HISTSIZE 变量指定保存的历史命令最大条数。

常用操作:

  • history:查看历史命令列表。
  • history | wc -l:查看当前保存的历史命令数量。
  • !v:执行最近一条以 v 开头的命令。
  • ctrl+r:反向搜索历史命令。

  这些功能依赖于 Bash 的历史记录机制,而历史记录文件通常位于 ~/.bash_history(或由 HISTFILE 指定)。


六、总结与面试要点

6.1 环境变量的基本概念

  • 什么是环境变量?
    环境变量是操作系统中用来指定运行环境的一些参数,以 NAME=value 形式存储,具有全局属性,可被子进程继承。
  • 常见环境变量有哪些?
    PATH(命令搜索路径)、HOME(用户主目录)、SHELL(当前 Shell)、USER(当前用户)、PWD(当前目录)等。

6.2 操作环境变量的命令

  • echo $VAR:查看变量值。
  • export VAR=value:设置/导出环境变量。
  • unset VAR:删除变量。
  • env:查看所有环境变量。
  • set:查看所有变量(包括本地变量)。

6.3 为什么执行自己的程序要加 ./?

  因为当前目录通常不在 PATH 环境变量中,Shell 找不到程序。加 ./ 显式指定路径即可执行。也可以修改 PATH 加入当前目录。

6.4 环境变量的组织方式

  每个进程都有一张环境变量表,是一个字符指针数组,每个元素指向 NAME=value 字符串,以 NULL 结尾。

6.5 在代码中获取环境变量的方法

  1. main 函数第三个参数 char *env[]。
  2. getenv("NAME") 获取指定变量。
  3. 外部全局变量 extern char **environ。
  4. 子进程通过继承父进程环境变量获得。

6.6 本地变量和环境变量的区别

  本地变量仅在当前 Shell 进程中有效,不能被子进程继承;环境变量可以被所有子进程继承。使用 export 可将本地变量转为环境变量。

6.7 如何让环境变量永久生效?

  修改用户级配置文件 ~/.bashrc 或 ~/.bash_profile,添加 export 语句,然后重新登录或 source 文件。

6.8 环境变量的全局性如何理解?

  当一个进程创建子进程时(如 fork),子进程会复制父进程的环境变量表,因此环境变量可以传递给所有后代进程。但子进程对环境变量的修改不会影响父进程。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
BullSmall39 分钟前
规则+大模型混合评测:高效精准双保障
运维·自动化
网硕互联的小客服7 小时前
Linux服务器磁盘应该如何合理分区?
linux·运维·服务器
楚识科技7 小时前
合同比对全流程自动化:OCR识别+差异比对+法律风险字段抽取实战指南
运维·自动化·ocr
代码方舟7 小时前
零信任架构实战:基于天远人企关联构建自动化供应链金融网关
运维·人工智能·架构·自动化
lisanmengmeng7 小时前
nacos nrpe 监控机及被监控机部署
服务器·nacos·nagios·nrpe
贵沫末8 小时前
Ubuntu——常用软件安装
linux·运维·ubuntu
Doraemomo8 小时前
Linux内核驱动开发——中断与定时器
linux·运维·驱动开发
2301_808414389 小时前
Linux中动静态库的理解
linux·运维·服务器
分布式存储与RustFS9 小时前
模型 checkpoint 放对象存储:分片上传、断点续传与残留清理
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
M78佐菲9 小时前
ARM学习笔记(11)
linux·arm开发·笔记·嵌入式硬件·学习