欢迎光临
我们一直在努力

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

在这里插入图片描述

大家好,欢迎来到 huangjin007_ 的博客

个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)

总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇


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

文章目录

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


一、命令行参数

1.1 重新认识 main 函数

1.1.1 main 函数不是程序的开始

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

  我们可以查看汇编代码来验证。编写一个最简单的 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 内部的大致逻辑如下(伪代码):

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 函数最多可以有三个参数:

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

  • argc:命令行参数的个数(argument count)。
  • argv:字符串指针数组,保存各个参数(argument vector)。
  • env:环境变量字符串指针数组(environment vector)。

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

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

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

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

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

ls -a -l /home

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

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

在这里插入图片描述

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

1.3 命令行参数的应用演示

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

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

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:查看单个变量

echo $HOME
echo $PATH

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

2.3.2 env:查看所有环境变量

env

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

env | grep PATH

2.3.3 set:查看所有变量(包括本地变量和环境变量)

set

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

2.3.4 export:不带参数时也可查看环境变量列表

export

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

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

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

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

my_var="hello world"
i=1

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

2.4.2 导出为环境变量

# 先定义后导出
var1="ubuntu"
export var1

# 或者直接定义并导出
export var2="centos"

2.4.3 删除变量

unset var1
unset var2

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

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

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

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

  查看 PATH 的内容:

在这里插入图片描述

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

  解决办法有几种:

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

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

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

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

    # 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 需要重新登录或手动设置。

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

    export PATH=$PATH:/home/user/mybin

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

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

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

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

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

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

    export MY_CUSTOM_VAR="hello"
    export PATH=$PATH:/home/user/mybin

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

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

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

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

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

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

    #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 函数的完全体可以有三个参数,第三个参数就是环境变量表:

    #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 函数,用于获取指定名称的环境变量:

    #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,它指向环境变量表的首地址。使用前需要手动声明:

    #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 创建子进程,子进程就能直接读取。

    示例代码:

    #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 在代码中获取环境变量的方法

  • main 函数第三个参数 char *env[]。
  • getenv("NAME") 获取指定变量。
  • 外部全局变量 extern char **environ。
  • 子进程通过继承父进程环境变量获得。
  • 6.6 本地变量和环境变量的区别

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

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

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

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

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


    结语:

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

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

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux 系统篇(十八)】进程(六) :环境变量
    分享到: 更多 (0)

    评论 抢沙发

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