对于Linux:环境变量的解析

开篇介绍:

hello 大家,这篇博客依旧是和我们的进程有关,但是呢,关联性并不是很大,却也是值得我们去了解和知道的,它就是系统中的环境变量,大家可能对它闻所未闻,但是它确实一个默默付出的贡献者,我们平时很多的看似轻松写意的操作,其实都是环境变量在背后帮我们省略了很多操作,所以,我们肯定得去了解一下它,知其然知其所以然。

那么话不多说,我们开始喽。

命令行参数:

在正式了解环境变量之前,我们得先来了解了解命令行参数这一个东西,其实它和我们平时使用的main函数有关,不知道大家好不好奇,main函数有参数吗?答案是有的。

命令行参数是程序运行时通过命令行输入的 "外部信息",专门用于向程序传递临时指令,而接收这些信息的核心就是 main 函数的参数。下面从基础到细节,一步步讲清楚:

一、什么是命令行参数?

简单说:当你在命令行(比如 Linux 的终端、Windows 的 CMD)中运行一个程序时,在程序名后面输入的内容,就是命令行参数。

比如:

  • 运行 ls -l /home 时,-l/home 是传给 ls 程序的命令行参数,我们前面说过,我们平时输入的命令其实也是相当于是一个程序,只不过是Linux系统里面已经有了的程序。
  • 运行 ./myprog 123 "hello" 时,123"hello" 是传给 myprog 程序的命令行参数。

这些参数会被程序接收,用来决定程序的具体行为(比如 ls -l 用 "详细列表" 显示内容,ls -a 显示隐藏文件)。

二、main 函数如何接收命令行参数?

main 函数可以带两个参数,专门用来接收命令行参数,标准写法是:

复制代码
int main(int argc, char *argv[])

这两个参数的名字 argcargv 是约定俗成的(可以改,但全世界程序员都这么用,建议别改)。

1. argc:记录参数的 "总个数"

argc 是 "argument count" 的缩写,意思是 "命令行参数的总数量"。关键规则:程序自己的名字(即你输入的 "程序名")也算一个参数。

举例:假设你编译了一个程序,生成的可执行文件叫 test,然后在命令行输入:./test abc 123

这里输入的内容被拆成了 3 部分:./test(程序名)、abc123 ------ 所以 argc 的值是 3

2. argv:存储每个参数的 "具体内容"

argv 是 "argument vector" 的缩写,它是一个 "字符串数组"(可以理解为 "装着多个字符串的容器"),每个元素都是一个参数的具体内容。

核心规则:

  • argv[0]:永远指向程序自己的名字(比如上面的 ./test);
  • argv[1]:指向第一个用户输入的参数(比如 abc);
  • argv[2]:指向第二个用户输入的参数(比如 123);
  • ... 以此类推,直到 argv[argc-1](最后一个参数)。

注意:argv 中的每个元素都是 "字符串类型"(char*),哪怕你输入的是数字(比如 123),在程序中也会被当作字符串处理。

大家可以这么理解,就是系统是生成了一张命令行参数列表。

我们平时所输入的命令,选项之类的,都会被系统存进命令行参数列表,然后执行对应选项的操作。

三、代码示例:直观感受命令行参数

写一个程序,把收到的命令行参数 "列出来",看完你就全懂了:

复制代码
#include <stdio.h>

// main函数带参数:argc(数量)和argv(内容)
int main(int argc, char *argv[]) {
    // 1. 打印参数总个数
    printf("参数总共有 %d 个:\n", argc);
    
    // 2. 循环打印每个参数的内容
    for (int i = 0; i < argc; i++) {
        printf("第 %d 个参数:%s(地址:%p)\n", i, argv[i], argv[i]);
    }
    
    return 0;
}
编译并运行:

假设编译后生成可执行文件 a.out,在命令行输入:./a.out hello 666 "world"

输出结果:
复制代码
参数总共有 4 个:
第 0 个参数:./a.out(地址:0x7ffd9a9b92a0)
第 1 个参数:hello(地址:0x7ffd9a9b92a8)
第 2 个参数:666(地址:0x7ffd9a9b92b0)
第 3 个参数:world(地址:0x7ffd9a9b92b4)
结果说明:
  • argc=4:因为参数包括 ./a.outhello666world,共 4 个;
  • argv[0] 是程序名 ./a.outargv[1]argv[3] 是用户输入的参数;
  • 每个参数都是字符串(比如 666 在这里是字符串 "666",不是数字);
  • 括号里的 "地址" 是每个字符串在内存中的位置(体现 argv 是指针数组的特性)。

四、细节

  1. 参数的分隔 :命令行中,参数之间用 "空格" 分隔。如果参数本身包含空格(比如 hello world),需要用引号(单引号或双引号)括起来,否则会被拆成两个参数。例:./a.out "hello world" 中,"hello world" 会被当作一个参数(argv[1]"hello world")。

  2. 无参数的情况 :如果运行程序时不带任何参数(比如 ./a.out),argc 的值是 1 (只有程序名自己),argv[0] 是程序名,argv[1] 及以后的元素不存在(访问会越界)。

  3. 参数的类型argv 中的所有参数都是字符串(char*)。如果需要数字(比如计算 123 + 456),必须用 atoi()(字符串转整数)、atof()(字符串转浮点数)等函数转换。例:int num = atoi(argv[1]);(将 argv[1] 的字符串转成整数)。

  4. argv 的结尾 :在大多数系统中,argv[argc] 的值是 NULL(空指针),可以用这个特性判断参数是否结束(比如 while (*argv != NULL) { ...; argv++; })。

五、为什么需要命令行参数?

它的核心作用是让程序更灵活:不用修改代码,只需改变运行时的参数,程序就能做不同的事,其实这个和if、else if等等有点像,本质上就是通过外部传入的参数去直接指定这个程序要做什么事,不用我们进入程序内部修改代码让它做特定的事情,再通俗易懂一点就是说,程序内部设置了多个选项,每个选项对应着不同的操作,我们可以在外面执行程序的同时指定要是哪个选项,这样子就能让程序进行指定的操作。

比如:

  • 一个 "文件备份" 程序,通过参数指定 "源文件" 和 "目标路径":./backup src.txt /home/backups
  • 一个 "图片处理" 程序,通过参数指定 "缩放比例":./img_resize pic.jpg 0.5(缩小到 50%)。
总结

命令行参数是程序运行时从命令行传入的 "外部指令",由 main 函数的 argc(参数数量)和 argv(参数内容)接收。其中:

  • argc 是整数,记录参数总个数(含程序名);
  • argv 是字符串数组,按顺序存储每个参数的具体内容(argv[0] 是程序名,后续是用户输入的参数)。

掌握命令行参数,是理解 "程序如何与外部交互" 的基础,也是学习环境变量、命令行工具的前提。

环境变量:

环境变量是操作系统里的 "全局参数卡片",就像贴在系统和所有程序都能看到的公告栏上的便利贴,上面记着各种关键信息 ------ 比如 "常用命令存放在哪个文件夹""你的个人目录在哪里""系统用什么语言显示"。系统执行命令、程序查找资源时,都会主动看这些 "便利贴",不然就像没带地图的旅行者,不知道该往哪走。

然后我在这里有必要说明一下,其实环境变量是属于系统的数据,即是shell的数据,也可以说是bash的数据,是最大的父进程的数据哦。

一、本地变量和环境变量:"私人日记" 与 "公共公告" 的区别

不管是本地变量还是环境变量,都是 "变量名 = 值" 的键值对,但它们的 "可用范围" 和 "存储地方" 完全不同,就像你写在私人日记里的内容和贴在小区公告栏上的通知:

1. 本地变量:只有当前终端能用,"外人" 看不到

本地变量是当前终端(bash,我们常用的命令行工具)的 "私人日记",只存在终端自己的内存里,没被写到 "环境变量表"(每个程序都有的一张记录环境变量的清单)里。这意味着:

  • 只有当前终端能直接用:在终端里输echo $变量名能看到它,但用env命令(专门看环境变量的工具)看不到;
  • 从终端启动的程序(比如ls、你自己写的程序,这些都是终端的 "子进程")完全读不到 ------ 就像你日记里的内容,别人拿不到。

比如在终端输student="张三"(定义本地变量):

  • set命令(能看终端所有变量,包括本地和环境变量)能看到student=张三
  • env命令看不到(因为没进环境变量表);
  • 就算写个程序读student,结果也是 "没找到"。
2. 环境变量:所有程序都能用,"子辈" 能继承

**环境变量是写进 "环境变量表" 的 "公共公告",每个程序(包括终端和它启动的子进程)都有一份这样的表。**这意味着:

  • 当前终端能用:终端可以直接读、改这些变量;
  • 子进程会自动 "抄" 一份:子进程启动时,会把父进程(终端)的环境变量表完整复制过去,所以所有子进程都能看到 ------ 就像公告栏上的通知,谁都能看,这个是我们下面会说的环境变量的特性:全局性。

比如在终端输export student="张三"(把本地变量升级为环境变量):

  • env命令能看到student=张三(已进环境变量表);
  • 不管启动什么程序(子进程),都能读到student的值是 "张三"。

一句话总结:本地变量是 "终端自己的小秘密,不给子进程看",存在终端的局部内存;环境变量是 "终端和所有子进程共享的公开信息",存在每个程序的环境变量表里。

二、环境变量存在哪里?终端怎么管理它?

我们操作的终端(bash)里有一张 "环境变量表",就像终端的 "专属公告板",所有环境变量都清清楚楚记在这里。

1. 环境变量表的样子:一串 "变量名 = 值" 的字符串

这张表本质是个字符串数组(类似 C 语言里的char**),每个元素都是 "变量名 = 值" 的格式(比如"PATH=/bin:/usr/bin""HOME=/home/lisi"),结尾用一个空指针(NULL)标记 "表结束"------ 终端遍历变量时,看到 NULL 就知道 "没更多了"。

最关键的是,这张表会被所有子进程 "完整复制":比如你运行ls或自己的程序时,子进程启动第一件事就是把终端的环境变量表 "抄" 一份,所以子进程能看到所有环境变量。

2. 终端怎么管理这张 "公告板"?靠两个命令:
  • 把本地变量变成环境变量 :先定义本地变量(比如score=100),它只在终端内存里;输export score,终端就会把"score=100"写到环境变量表,这时它就成了环境变量,子进程能读到。
  • 修改环境变量 :比如PATH已经在表中,输export PATH=$PATH:/新目录,终端会找到表中的PATH,把旧值加上新目录,替换成新内容。
  • 删除环境变量 :输unset 变量名,终端会从环境变量表中 "擦掉" 对应的内容(如果是环境变量);如果是本地变量,就从终端内存里删掉。

举个例子:

bash 复制代码
# 1. 定义本地变量(只在终端内存,不在环境变量表)
score=100  

# 2. 用export加入环境变量表(成为环境变量)
export score  
# 此时表中有"score=100",子进程能读到  

# 3. 子进程读取(假设read_score程序会打印score)
./read_score  # 输出:100  

# 4. 修改环境变量(更新表中的值)
export score=90  
./read_score  # 输出:90  

# 5. 用unset从表中删除
unset score  
./read_score  # 输出:(null)(子进程读不到了)

三、环境变量从哪来?一条 "代代相传" 的链

终端里的环境变量不是凭空出现的,而是从 "系统启动" 到 "你登录" 再到 "终端打开",一步步 "继承 + 添加" 来的,就像一条接力链:

1. 系统启动:最基础的 "源头变量"

Linux 启动时,第一个运行的程序是init(现代系统多是systemd,负责初始化系统),它会设置最核心的环境变量,作为整个系统的 "基础参数":

  • 比如PATH的初始值(包含/bin/sbin等,确保lscd这些基础命令能被找到);
  • 比如HOSTNAME(主机名,从/etc/hostname文件读,相当于电脑的 "网络昵称");
  • 比如TERM(默认终端类型,告诉程序 "当前终端支持哪些功能")。

这些变量是所有后续程序的 "源头"------ 因为系统里所有程序最终都由init启动或继承自它,所以都会带着这些基础变量,那么这些都是从根目录下获取的。

2. 你登录时:根据身份加 "用户信息"

当你通过终端、SSH 登录时,会启动 "登录进程"(比如本地登录用getty,远程登录用sshd)。登录进程会:

  • 先 "复制"init的环境变量(继承基础参数);
  • 再根据你的身份加用户相关变量:
    • USER:你的用户名(比如USER=lisi,从登录账号来);
    • HOME:你的个人主目录(比如HOME=/home/lisi,从/etc/passwd文件查,这个文件记着每个用户的主目录);
    • LOGNAME:和USER类似,有些程序习惯用它获取用户名。

这些就是在用户自己的家目录里进行获取。

3. 终端启动:读配置文件,加更多 "个性化变量"

登录成功后,系统会启动终端(bash),终端会:

  • 先 "复制" 登录进程的环境变量(继承init的基础变量 + 用户相关变量);
  • 再读一系列配置文件,添加或修改变量,最终形成你在终端里看到的环境变量。

这些配置文件分 "全局"(所有用户共用)和 "个人"(只有你能用):

配置文件类型 路径 作用(大白话) 举例(加的变量)
全局配置 /etc/profile,这个是在全局里面的,相当于是在根目录下的,所有用户都会有 所有用户登录时都会读,设系统级变量 PATH/usr/local/bin(用户装软件的地方)、LANG=zh_CN.UTF-8(默认中文)
全局配置 /etc/bashrc这个是在全局里面的,相当于是在根目录下的,所有用户都会有 所有用户启动终端时都会读,设终端全局行为 PS1(命令提示符格式,比如[lisi@mycomputer ~]$
个人配置 ~/.bash_profile,而这个则是在用户自己的家里,针对用户自己,并不会用到所有用户身上 只有你登录时读,会继承/etc/profile的内容 export MY_PROJECT=/home/lisi/code(你的项目目录)
个人配置 ~/.bashrc,而这个则是在用户自己的家里,针对用户自己,并不会用到所有用户身上 你每次启动终端时都会读,会继承/etc/bashrc的内容 export PATH=$PATH:~/.local/bin(你的个人脚本目录)

举个例子:PATH变量的 "成长史":

PATH是最常用的环境变量,它的 "一生" 完美体现了这条传承链:

  • init启动时,PATH初始值是/sbin:/bin:/usr/sbin:/usr/bin(确保系统命令能运行);
  • 登录进程继承后,不修改PATH(保持默认);
  • 终端启动时,先读/etc/profile,加/usr/local/bin(用户装的软件通常在这),PATH变成/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
  • 再读~/.bashrc,如果你之前加过~/.local/bin(自己写的脚本在这),最终PATH变成/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:/home/lisi/.local/bin------ 这就是你输echo $PATH看到的结果。

四、直观案例:为啥ls能直接运行,我的程序却要加路径?

这全是PATH环境变量在起作用,咱们动手试试就懂了:

步骤 1:ls能直接运行的原因

ls时,系统立刻列出文件,因为PATH的传承链里包含/bin目录,而ls程序正好在/bin里。系统执行ls时,会顺着PATH的目录一个个找,很快就找到/bin/ls并运行。

步骤 2:自己的程序为啥要加路径?

写个简单的 C 程序hello.c

复制代码
#include <stdio.h>
int main() {
    printf("环境变量测试:成功运行!\n");
    return 0;
}

编译生成hellogcc hello.c -o hello

直接输hello运行,会报错 "command not found"------ 因为hello在当前目录(比如/home/lisi),但这个目录不在PATH里,系统不知道去哪找。

加路径./hello运行,能正常输出 ------./表示 "当前目录",相当于手动告诉系统 "程序在这"。

步骤 3:让自己的程序像ls一样直接运行

把当前目录加入PATH就行:输export PATH=$PATH:./$PATH表示 "保留原来的目录,再加当前目录./")。

再输hello,直接输出结果 ------ 因为PATH现在包含当前目录,系统会自动在这找程序,当然,如果我们退出了之后再次进入系统,那么这个就会不行了,因为我们只是暂时的修改了PATH的值,并没有在系统获取到PATH的文件里去修改,所以重新进入系统就会消失掉。

五、常用环境变量:各自有啥用?

系统预设了很多环境变量,每个都有明确分工,就像不同功能的 "公告牌":

环境变量 作用(大白话) 查看示例(echo $变量名
PATH 命令的 "搜索目录清单":系统执行命令时,按顺序在这些目录里找程序。 输出:/usr/local/bin:/usr/bin:/binls/bingcc/usr/bin
HOME 你的 "个人主目录":登录后默认进的目录("家在哪")。 普通用户输出/home/你的用户名;root 用户输出/root
SHELL 当前用的 "命令解释器":终端用什么工具解析你的命令(默认是 bash)。 输出/bin/bash
USER 当前登录的 "用户名":记着 "谁在用系统"。 普通用户输出lisi;root 用户输出root
PWD 当前所在的 "目录路径":记着 "现在在哪个文件夹"(和pwd命令结果一样)。 /home/lisi/doc时,输出/home/lisi/doc
OLDPWD 上一次所在的 "目录路径":记着 "刚才在哪个文件夹",输cd -能直接回去。 /home切到/tmp后,输出/home
LANG 系统的 "语言和编码":决定中文、英文是否正常显示(乱码常和它有关)。 输出zh_CN.UTF-8(中文环境)或en_US.UTF-8(英文环境)
LD_LIBRARY_PATH 动态链接库的 "搜索路径":程序运行时,到这些目录找需要的.so库文件(比如libc.so)。 输出/usr/local/lib
TERM 终端的 "功能描述":告诉程序 "当前终端支持哪些功能"(比如是否支持彩色显示)。 输出xterm-256color(支持 256 色)
HOSTNAME 主机的 "网络名称":电脑在网络中的 "昵称"。 输出localhost或自定义名称(比如mycomputer

这些其实都是相当于一张张便利贴,当用户输入一些指令时,系统就会去借助这些便利贴执行更加方便的操作,本质上其实便利了用户自己,因为用户不用去指定的清清楚楚,让这些便利贴去存储,然后系统去从便利贴获取信息,比如我们的cd ~其实就是系统会去使用HOME这张便利贴,然后我们就可以进入家目录,不需要我们去cd 家目录。

六、怎么操作环境变量?像 "看公告、贴公告、撕公告" 一样简单

和环境变量打交道,就 3 个核心动作,用几个简单命令就能搞定:

1. 看单个环境变量:echo $变量名,要注意后面是要跟着$符号的哦

想知道某个变量的值,用echo $变量名$表示 "取变量的值"):

复制代码
echo $HOME   # 看个人主目录,输出/home/lisi
echo $USER   # 看当前用户名,输出lisi
echo $PATH   # 看命令搜索目录,输出一堆用:分隔的目录
2. 看所有环境变量:env

直接输env,会列出所有 "变量名 = 值" 的行:

复制代码
env  # 输出示例:
# PATH=/usr/local/bin:/usr/bin:/bin
# HOME=/home/lisi
# USER=lisi
# LANG=zh_CN.UTF-8
# ...
3. 新增 / 修改本地变量、显示所有变量:set(注意是本地变量,仅当前终端生效,子进程无法继承;若需升级为环境变量,需结合 export

本地变量是终端的 "私人变量",仅当前终端能用,子进程(如启动的程序)读不到。用 set 可以直接定义或修改本地变量,不需要加 export

  • 新增本地变量:比如定义一个本地变量 MY_LOCAL 记录个人爱好:

    复制代码
    set MY_LOCAL="打篮球"  # 新增本地变量MY_LOCAL
    echo $MY_LOCAL         # 验证,输出"打篮球"
  • 修改已有本地变量:比如更新 MY_LOCAL 的值:

    复制代码
    set MY_LOCAL="踢足球"  # 修改本地变量MY_LOCAL的值
    echo $MY_LOCAL         # 验证,输出"踢足球"

    🔔 注意:用 set 定义的变量默认是本地变量,只有当前终端能用;若想让它变成环境变量(子进程也能继承),需再执行 export MY_LOCAL

当然上面的这些其实不一定需要,我们不加set,直接变量名=内容也行,大家自行选择。

  • 显示所有变量(本地 + 环境变量)

    set 不带任何参数时,会列出当前 Shell 中所有的变量(包括本地变量、环境变量,以及 Shell 内置的变量)。

  • 示例:在终端输入 set,会输出一大段内容,比如你之前定义的 MY_LOCALMY_SCHOOL,以及系统内置的 PATHHOME 等变量都会被列出。

4. 新增 / 修改环境变量:export(注意是环境变量,而不是本地变量,本地变量不需要加export)
  • 新增:比如定义MY_SCHOOL记学校名称:

    复制代码
    export MY_SCHOOL="北京大学"  # 新增环境变量MY_SCHOOL
    echo $MY_SCHOOL             # 验证,输出"北京大学"
  • 修改已有变量:比如给PATH加个新目录(让该目录下的程序能直接运行)。假设你的程序都在/home/lisi/myprograms,输:

    复制代码
    export PATH=$PATH:/home/lisi/myprograms  # 保留原来的目录,加新目录

    之后这个目录里的程序(比如test),直接输test就能运行。

5. 删除环境变量:unset

想删掉某个变量(环境变量和本地变量都可以),用unset 变量名

复制代码
unset MY_SCHOOL  # 删除MY_SCHOOL
echo $MY_SCHOOL  # 输出空(变量没了)

七、程序怎么读环境变量?代码举例

方式 1:getenv 函数(按名字查单个变量)
代码示例
复制代码
#include <stdio.h>
#include <stdlib.h>  // 必须包含,getenv 声明在这里

int main() {
    // 1. 查找系统内置变量 PATH(命令搜索路径)
    char* path_val = getenv("PATH");
    if (path_val != NULL) {  // 必须判断,避免 NULL 指针错误
        printf("找到 PATH:%s\n", path_val);
    } else {
        printf("PATH 变量不存在(系统级变量几乎不会出现这种情况)\n");
    }

    // 2. 查找用户自定义变量 MY_SCHOOL(可能未设置)
    char* school_val = getenv("MY_SCHOOL");
    if (school_val != NULL) {
        printf("找到 MY_SCHOOL:%s\n", school_val);
    } else {
        printf("MY_SCHOOL 变量未设置(请先用 export MY_SCHOOL=xxx 定义)\n");
    }

    return 0;
}
编译运行
复制代码
# 先定义一个自定义环境变量(终端中执行)
export MY_SCHOOL="清华大学"

# 编译代码
gcc getenv_demo.c -o getenv_demo

# 运行程序
./getenv_demo
输出结果
复制代码
找到 PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
找到 MY_SCHOOL:清华大学
核心说明
  • getenv("变量名") 直接返回 "变量值" 的指针(即字符指针,即字符串),无需关心底层列表结构;
  • 必须判断返回值是否为 NULL(尤其是自定义变量,可能未设置)。
方式 2:main 函数的 envp 参数(遍历所有变量)
代码示例
复制代码
#include <stdio.h>
#include <string.h>  // 用于字符串拆分(strchr 函数)

// main 函数完整参数:argc(参数个数)、argv(命令行参数)、envp(环境变量列表)
int main(int argc, char* argv[], char* envp[]) {
    printf("===== 所有环境变量(共 %d 个)=====\n", argc);  // argc 是命令行参数个数,仅作区分

    // 循环遍历 envp 数组(直到遇到 NULL 结束)
    for (int i = 0; envp[i] != NULL; i++) {
        // 打印原始格式:"变量名=值"
        printf("第 %d 个:%s\n", i, envp[i]);

        // 拆分"变量名"和"值"(可选操作)
        char* equal_pos = strchr(envp[i], '=');  // 找第一个 '=' 的位置
        if (equal_pos != NULL) {
            // 临时将 '=' 换成字符串结束符,拆分出变量名
            *equal_pos = '\0';  // 此时 envp[i] 就是纯变量名
            char* var_name = envp[i];
            char* var_val = equal_pos + 1;  // 跳过 '=' 就是值
            printf("  → 变量名:%s,值:%s\n", var_name, var_val);
            *equal_pos = '=';  // 恢复原始字符串(避免影响后续使用)
        }
    }

    return 0;
}
编译运行
复制代码
gcc envp_demo.c -o envp_demo
./envp_demo
输出结果(截取部分)
复制代码
===== 所有环境变量(共 1 个)=====  # argc 是命令行参数个数,此处为 1(程序名本身)
第 0 个:PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
  → 变量名:PATH,值:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
第 1 个:HOME=/home/yourname
  → 变量名:HOME,值:/home/yourname
第 2 个:MY_SCHOOL=清华大学
  → 变量名:MY_SCHOOL,值:清华大学
...(省略其他变量)
核心说明
  • envpmain 函数的第三个参数,直接指向环境变量列表(数组);
  • 数组元素格式为 变量名=值,以 NULL 结尾,必须用循环遍历;
  • 可通过 strchr 拆分变量名和值(注意临时修改后要恢复,避免影响其他代码)。
方式 3:environ 全局变量(全局访问所有变量)

environ 是 Linux 系统中用于访问和管理进程环境变量的核心全局变量,以下从定义、结构、作用、使用场景等方面详细说明:

1. 定义与类型

environ 是一个全局变量,声明为:

复制代码
extern char **environ;
  • 类型:char **(二级字符指针),本质是指向 "环境变量字符串数组" 的指针
2. 存储结构

environ 指向的数组遵循以下规则:

  • 数组中的每个元素是 char* 类型,指向一个 **"变量名 = 值" 格式的环境变量字符串 **(例如 PATH=/usr/binHOME=/root);
  • 数组的最后一个元素是 NULL,作为环境变量列表的结束标记。

简单来说,environ 存储的是 "环境变量字符串数组的首地址",结构类似:

复制代码
environ --> [ "VAR1=val1", "VAR2=val2", ..., NULL ]
3. 核心作用
  • 访问所有环境变量 :提供了一种底层、直接 的方式遍历进程的所有环境变量(而 getenv 仅能获取单个变量);
  • 自定义环境变量传递 :在 execle 等函数中,可通过 environ 传递当前进程的环境变量,或自定义环境变量数组。
4. 使用场景示例
execle 中传递环境变量
复制代码
#include <unistd.h>
extern char **environ;

int main() {
    // 自定义环境变量数组(需以 NULL 结尾)
    char *my_env[] = {"MY_VAR=test", "OTHER_VAR=123", NULL};
    
    // execle 调用时,可选择传递 my_env 或 environ
    execle("/bin/echo", "echo", "Hello", NULL, my_env); 
    // 若传递 environ,则新进程继承当前进程的所有环境变量
    // execle("/bin/echo", "echo", "Hello", NULL, environ);
    
    return 0;
}
5. 注意事项
  • 进程独立性 :每个进程的 environ 是独立的副本,修改当前进程的 environ 不会影响其他进程;
  • 结构合法性 :若自定义环境变量数组,必须以 NULL 结尾,否则可能导致程序异常;
  • 头文件与声明environ 无需显式包含头文件(其定义在 libc 中),但使用时需通过 extern char **environ; 声明类型。
6. 与 getenv 的对比
特性 environ getenv
功能 访问所有环境变量,支持遍历 仅获取单个环境变量的值
灵活性 高(可修改、遍历、自定义传递) 低(仅查询)
使用场景 需批量处理环境变量或自定义传递 仅需查询单个变量的值
代码示例
复制代码
#include <stdio.h>
#include <string.h>

// 声明全局环境变量表(系统定义,必须加 extern)
extern char** environ;

// 自定义函数:用 environ 遍历环境变量(展示全局访问能力)
void print_all_env() {
    printf("\n===== 自定义函数中遍历环境变量 =====");
    for (int i = 0; environ[i] != NULL; i++) {
        printf("\n第 %d 个:%s", i, environ[i]);
    }
}

int main() {
    // 主函数中用 environ 遍历
    printf("===== 主函数中遍历环境变量 =====");
    for (int i = 0; environ[i] != NULL; i++) {
        // 只打印包含 "PATH" 的变量(筛选示例)
        if (strstr(environ[i], "PATH") != NULL) {
            printf("\n包含 PATH 的变量:%s", environ[i]);
        }
    }

    // 调用自定义函数,展示 environ 全局可用
    print_all_env();

    return 0;
}
编译运行
复制代码
gcc environ_demo.c -o environ_demo
./environ_demo
输出结果(截取部分)
复制代码
===== 主函数中遍历环境变量 =====
包含 PATH 的变量:PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
包含 PATH 的变量:LD_LIBRARY_PATH=/usr/local/lib

===== 自定义函数中遍历环境变量 =====
第 0 个:PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
第 1 个:HOME=/home/yourname
第 2 个:MY_SCHOOL=清华大学
...(省略其他变量)
核心说明
  • environ 是全局变量,需用 extern char **environ 声明后才能使用,因为它不在任何一个头文件里
  • 作用和 envp 完全一致(指向同一个环境变量表),但可在程序任何地方访问(包括自定义函数);
  • 适合需要在多个函数中处理环境变量的场景(无需传递参数)。
总结对比
方式 代码特点 最适合的场景
getenv 一行代码查单个变量,需判断 NULL 只需要某个特定变量(如 HOMEPATH
envp 依赖 main 参数,循环遍历数组 仅在 main 函数中遍历所有变量
environ 全局变量,可在任意函数中使用 多个函数需要访问环境变量列表

通过代码示例能明显看出:getenv 专注 "精准查找",envpenviron 专注 "批量遍历",根据实际需求选择即可。

八、环境变量的 "继承性":父进程的变量,子进程能直接用

环境变量最核心的特性是 "父传子"------ 父进程(比如终端)的环境变量表,会被所有子进程(比如从终端启动的程序)完整复制。咱们用实验验证:

步骤 1:定义本地变量(不导出)

在终端输:

复制代码
lesson="环境变量教程"  # 定义本地变量,仅当前终端能用

set命令能看到lesson=环境变量教程;用env命令看不到(没进环境变量表)。

步骤 2:子进程读不到本地变量

写个程序read_lesson.c

复制代码
#include <stdio.h>
#include <stdlib.h>
int main() {
    printf("lesson的值:%s\n", getenv("lesson"));
    return 0;
}

编译运行:

复制代码
gcc read_lesson.c -o read_lesson
./read_lesson  # 子进程运行

输出:lesson的值:(null)(子进程读不到,因为本地变量不在环境变量表)。

步骤 3:升级为环境变量(导出)

在终端输:

复制代码
export lesson  # 把本地变量加入环境变量表,升级为环境变量

此时用env命令能看到lesson=环境变量教程(已进表)。

步骤 4:子进程能读到环境变量

再运行./read_lesson,输出:lesson的值:环境变量教程(子进程复制了终端的环境变量表,所以能读到)。

export小问题:

那么大家可能会有一些疑问,我记得之前说过了,子进程不能改变父进程的数据,因为写时拷贝,那么环境变量是bash这个最大的父进程的数据,而按道理来说,export这个指令肯定是bash的子进程,那么凭什么export指令可以去修改bash的环境变量的数据呢?

这个问题的核心误解在于:export 并不是 bash 的子进程,它是 bash 自带的 "内置命令"(由 bash 自己直接执行的指令),因此可以直接操作 bash 进程的内存数据(包括环境变量表)。

关键原因:export 是 bash 的 "内置命令",而非独立子进程

首先要明确两个概念的区别:

  • 外部命令 :像 lsgcccp 这类命令,本质是磁盘上的可执行程序。当你在 bash 中输入这些命令时,bash 会创建一个子进程(通过 fork),然后在子进程中运行这个程序 ------ 这时候子进程和 bash 是两个独立的进程,遵循 "写时拷贝"(子进程修改的数据只影响自己,不影响父进程)。
  • 内置命令 :像 exportcdsetecho 这类命令,没有独立的可执行程序,它们是 bash 自身代码的一部分。当你输入这些命令时,bash 不会创建子进程,而是直接在自己的进程内部执行对应的代码 ------ 因此可以直接操作 bash 自己的内存(包括环境变量表)。
为什么 export 能修改 bash 的环境变量表?

bash 进程的内存中维护着两张关键的 "表":

  1. 本地变量表:仅 bash 自己可见的变量(如 student="张三" 未导出时)。
  2. 环境变量表:会被子进程继承的变量(如 export student="张三" 后)。

export 作为内置命令,其作用就是直接修改 bash 内存中的这两张表

  • 当你执行 export 变量名=值 时,bash 会直接在自己的环境变量表中添加或更新这个变量(如果是新变量,同时会从本地变量表移到环境变量表);
  • 整个过程都在 bash 自己的进程内完成,没有子进程参与,自然不存在 "子进程修改父进程数据" 的问题。
反证:如果 export 是子进程会怎样?

假设 export 是一个外部命令(会创建子进程),那么:

  • 当你执行 export student="张三" 时,bash 会 fork 一个子进程,子进程内部修改自己的环境变量表(添加 student=张三);
  • 但根据 "写时拷贝",子进程的环境变量表是 bash 表的副本,子进程的修改只会影响自己,不会同步到 bash 的表中;
  • 这时候子进程退出后,bash 的环境变量表毫无变化,export 命令就完全失效了 ------ 这显然和实际情况矛盾。
总结

export 能修改 bash 的环境变量,核心原因是:它是 bash 自带的内置命令,直接在 bash 进程内部执行,操作的是 bash 自己内存中的环境变量表,而非子进程的副本。因此不受 "子进程无法修改父进程数据" 的限制。

这也是为什么像 cd 必须是内置命令(如果 cd 是子进程,子进程切换目录后退出,bash 的目录不会变)------ 所有需要修改 shell 自身状态的命令,都必须是内置命令。

九、怎么让环境变量 "永久生效"(重启不消失)?

export设的环境变量,关掉终端就没了(只在当前终端会话有效)。想一直保留,得改终端启动时会读的配置文件(就是前面说的 "传承链" 里的文件):

1. 只对自己永久生效

改个人配置文件~/.bashrc~表示你的主目录):

复制代码
# 用编辑器打开(不会vim可以用gedit)
gedit ~/.bashrc  

在文件末尾加要永久保存的变量,比如:

复制代码
export MY_NAME="李四"  # 永久保存MY_NAME
export PATH=$PATH:/home/lisi/mytools  # 永久给PATH加目录

保存后,让配置立即生效(不用重启终端):

复制代码
source ~/.bashrc  # 刷新配置(让终端重新读一次~/.bashrc)

以后不管重启终端还是电脑,终端启动时都会读~/.bashrcecho $MY_NAME都会输出 "李四"。

2. 对所有用户永久生效(谨慎操作)

改全局配置文件/etc/profile(需要管理员权限):

复制代码
sudo gedit /etc/profile  # 用root权限打开

在末尾加变量(比如export MY_PUBLIC="所有用户可见"),保存后刷新:

复制代码
source /etc/profile  # 全局配置生效

此时所有用户登录后,终端都会读到这个变量。但修改全局配置可能影响系统,建议优先用个人配置。

当然,这部分内容并不是很重要,大家仅做了解。

总结:环境变量的本质和价值

环境变量的本质,就是 **"系统和所有程序之间共享的'基础信息清单'"**------ 它记着所有程序运行时可能需要的 "通用答案",比如 "命令在哪""用户目录在哪""用什么语言显示"。

这些信息之所以重要,是因为程序运行时不能 "瞎猜":ls要知道自己的程序在哪,浏览器要知道你的个人目录在哪,中文软件要知道用什么编码才不乱码...... 环境变量把这些 "标准答案" 提前写好,所有程序都能查,避免了重复询问或硬编码的麻烦。

它的核心作用是让系统和程序 "有章可循":

  • 统一 "查找路径"(比如PATH让命令能被找到);
  • 标识 "用户环境"(比如HOME让程序知道你的 "家" 在哪);
  • 统一 "系统配置"(比如LANG让中文正常显示)。

而它和本地变量的核心区别,就在于 "共享性" 和 "继承性":环境变量是所有程序都能看到的 "公共信息",能被子进程继承,不用每个程序单独配置 ------ 这就是它 "减少重复工作、统一系统行为" 的核心价值。

简单说:没有环境变量,程序就像没带地图的旅行者,不知道往哪走;而环境变量,就是那张所有程序都能看懂的 "通用地图",让整个系统能顺畅运行。

结语:那些藏在命令背后的 "隐形骨架"

敲下最后一个字符时,窗外的月光刚好落在终端屏幕上,映出一行echo $PATH的输出 ------ 那些用冒号分隔的目录,像一串沉默的路标,突然变得生动起来。原来我们每天随手输入的lsgcc,每一次顺畅的回车,背后都藏着环境变量搭建的 "隐形骨架":它是程序找得到 "家" 的地图,是系统辨得清 "身份" 的名片,是所有进程之间心照不宣的 "通用语言"。

回望这篇关于环境变量的絮叨,从命令行参数的 "外部指令",到本地变量与环境变量的 "公私之分";从export指令如何 "穿透" 进程边界的困惑,到getenv函数在代码中精准捕捉变量值的笃定;从PATH变量让程序 "随处可见" 的便利,到配置文件让变量 "代代相传" 的智慧 ------ 我们其实一直在拆解一个更本质的问题:计算机系统是如何让 "无序" 的代码和数据,变成 "有序" 的协作?

环境变量给出的答案,是 "约定"。它像一群提前商量好的规则:系统说 "命令放在这些目录里准没错",于是有了PATH;程序问 "用户的家在哪",HOME会立刻举手;甚至连中文显示不乱码、动态库能被找到,都是LANGLD_LIBRARY_PATH在默默兜底。这些约定不需要每个程序单独定义,不需要用户反复告知,它们就像空气一样存在,却让整个系统的运转少了无数摩擦。

你或许会说:"我平时敲命令就行了,何必纠结这些'底层细节'?" 但就像我们学走路时不必懂肌肉如何收缩,可若想跑得更快、跳得更高,总要知道力从何而来。理解环境变量,正是为了看透那些 "理所当然" 背后的 "所以然":

当你发现./程序才能运行时,会明白是PATH少了当前目录;当你远程登录后变量丢失,会想起~/.bashrc~/.bash_profile的区别;当程序报错 "找不到库文件",会下意识检查LD_LIBRARY_PATH------ 这些曾经让你手足无措的 "小问题",突然就有了清晰的解决路径。

更珍贵的,是这种 "穿透表象" 的思维方式。环境变量教会我们:计算机世界里没有真正的 "魔法",所有便利都是精心设计的结果。export不是什么 "跨进程修改的神器",只是 Shell 内置的内存操作;子进程能继承环境变量,不过是fork时的一次内存拷贝;甚至main函数的argcargvenvp,本质上都是系统给程序 "递纸条" 的方式。当我们剥离这些功能的 "外壳",看到的是一个个朴素的逻辑:数据如何传递,状态如何维护,进程如何协作。

写到这里,突然想起第一次学export时的困惑:为什么输入export a=1后,子进程能读到a的值?当时觉得这是 "系统的馈赠",直到后来调试程序时,在environ变量的数组里看到了a=1的字符串 ------ 原来所谓的 "继承",不过是一份完整的拷贝;所谓的 "全局",只是每个进程都捧着同一份从父进程那里接过的 "清单"。那一刻,仿佛突然看懂了系统的 "暗号",那种豁然开朗的感觉,或许就是技术学习最迷人的地方。

环境变量的故事,也是所有 "底层知识" 的缩影:它们不总是耀眼的,但一定是坚实的。就像盖房子时的钢筋,平时藏在墙里看不见,可少了它们,再华丽的装饰也撑不起整座建筑。我们不必时刻惦记着它们,但当需要改造房子、修补漏洞时,知道钢筋在哪、如何受力,总会更有底气。

最后,想对你说:别害怕那些看似 "枯燥" 的细节。当你下次输入echo $HOME时,试着想想这个变量从init进程到你的终端,经历了多少道 "传承";当你修改PATH让自己的程序被找到时,感受一下 "制定规则" 的乐趣;当你在代码里用getenv读取变量时,体会一下程序与系统 "对话" 的奇妙。

技术的世界里,从来没有 "多余" 的知识。那些你今天弄懂的环境变量,明天可能就会帮你解决一个棘手的 bug;那些你现在记下的environenvp,未来或许会成为你理解进程通信的钥匙。就像环境变量默默支撑着系统一样,这些细碎的知识,也会悄悄支撑起你的技术底气。

所以,继续保持好奇吧。去探索那些 "习以为常" 背后的原理,去追问那些 "就该这样" 的逻辑。或许有一天,当你调试一个跨进程的问题时,会突然想起今天看过的环境变量 "继承链";当你设计一个需要多程序协作的系统时,会下意识地用环境变量搭建起它们的 "沟通桥梁"。

那时你会发现:原来所有的学习,都像环境变量一样 ------ 看似无形,却早已在你的知识体系里,搭建起了通往更高处的 "隐形骨架"。而这,就是我们探索技术细节的意义所在。

愿你下次敲下命令时,眼前不止有输出的结果,还有背后那些默默工作的 "环境变量",以及一个对世界运转规律更清晰的认知。

相关推荐
C雨后彩虹2 小时前
箱子之字形摆放
java·数据结构·算法·华为·面试
坚持就完事了2 小时前
Linux上编写和运行Python\Java
linux·运维·服务器
超绝振刀怪2 小时前
【Linux 环境变量和地址空间】
linux·环境变量·fork·写诗拷贝
lwx9148529 小时前
Linux-特殊权限SUID,SGID,SBIT
linux·运维·服务器
皮卡狮10 小时前
Linux权限的概念
linux
炘爚11 小时前
深入解析printf缓冲区与fork进程复制机制
linux·运维·算法
小义_12 小时前
随笔 3(Linux)
linux·运维·服务器·云原生·红帽
cccccc语言我来了12 小时前
Linux(10)进程概念
linux·运维·服务器
伐尘12 小时前
【linux】查看空间(内存、磁盘、文件目录、分区)的几个命令
linux·运维·网络