在这里插入图片描述

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

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

文章目录



一、命令行参数

1.1 重新认识 main 函数

1.1.1 main 函数不是程序的开始

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

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

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

  编译后生成可执行文件,用 objdump -dgdb 查看入口点,你会发现程序并不是从 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)。

  其中 argcargv 是最常见的,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 -als -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_PATHLD_LIBRARY_PATH 等)提供了默认搜索路径,编译器会自动在这些路径中查找所需的库文件。

2.2 常见环境变量

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

  解决办法有几种:

  1. 使用相对路径 ./hello 或绝对路径 /path/to/hello,显式告诉 Shell 程序的位置。
  2. 将程序所在目录添加到 PATH 中(临时或永久)。
  3. 将程序复制或链接到 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 内建命令(如 cdechoexport)仍然可以使用,因为它们不是通过 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_COLORSls 命令的颜色配置
SSH_CONNECTIONSSH 连接信息
TERM终端类型
USER当前用户名
PATH可执行程序搜索路径
MAIL用户邮箱路径
OLDPWD上一次所在目录
_上一个执行的命令或程序的路径

5.2 USER 与 LOGNAME 的区别

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

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

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

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),子进程会复制父进程的环境变量表,因此环境变量可以传递给所有后代进程。但子进程对环境变量的修改不会影响父进程。


结语:

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

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

Logo

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

更多推荐