《Linux系统编程之进程环境》【环境变量】

【环境变量】目录

  • 前言:
  • ---------------环境变量---------------
    • [1. 什么是环境变量?](#1. 什么是环境变量?)
    • [2. 前所未闻的main函数的参数?](#2. 前所未闻的main函数的参数?)
    • [3. main函数带参数有什么用?](#3. main函数带参数有什么用?)
    • [4. 如何让自己写的程序像命名一样运行?](#4. 如何让自己写的程序像命名一样运行?)
    • [5. 命令行交互的核心逻辑是什么?](#5. 命令行交互的核心逻辑是什么?)
    • [6. 其他常见的环境变量有哪些?](#6. 其他常见的环境变量有哪些?)
    • [7. 如何操作环境变量?](#7. 如何操作环境变量?)
      • [一、export 命令](#一、export 命令)
      • [二、unset 命令](#二、unset 命令)
    • [8. 如何使用程序获取环境变量?](#8. 如何使用程序获取环境变量?)
    • [9. 实现一个"量子"程序?](#9. 实现一个“量子”程序?)
    • [10. 什么是本地变量?](#10. 什么是本地变量?)
    • [11. 本地变量有什么特别的?](#11. 本地变量有什么特别的?)
    • [13. export命令不为人知的一面?](#13. export命令不为人知的一面?)

往期《Linux系统编程》回顾:

/------------ 入门基础 ------------/
【Linux的前世今生】
【Linux的环境搭建】
【Linux基础 理论+命令】(上)
【Linux基础 理论+命令】(下)
【权限管理】

/------------ 开发工具 ------------/
【软件包管理器 + 代码编辑器】
【编译器 + 自动化构建器】
【版本控制器 + 调试器】
【实战:倒计时 + 进度条】

/------------ 系统导论 ------------/
【冯诺依曼体系结构 + 操作系统基本概述】

/------------ 进程基础 ------------/
【进程入门】
【进程状态】
【进程优先级】
【进程切换 + 进程调度】

前言:

hi~ 小伙伴们的大家好啊!( ˘ ³˘)♥

⏳转眼间十一月就走到了月末,这篇博客也算是本月的最后一篇啦~ 告别进程基础篇,咱们正式开启进程学习的第二阶段 ------ 换个视角,深挖进程背后更核心的系统逻辑! (*°ω°)ノ"

----- 2025 年 11 月 28 日(十月初九)周五,感恩节后一天

今天的主角,就是进程运行的 "隐形靠山"------【环境变量】 🛠️,先给大家通俗剧透核心内容,不搞抽象概念:

  • 环境变量:就是操作系统给进程提供的 "全局配置信息",相当于进程的 "随身说明书 + 地图"。,深挖环境变量的本质 ( ̄︶ ̄)

环境变量看似 "隐形",但不管是日常使用 Linux 命令,还是编写需要依赖外部配置的程序,都离不开它~

搞懂它,你就能解决 "命令找不到""依赖库加载失败" 等常见问题,还能更灵活地控制进程运行环境!(´。• ᵕ •。) ♡♡(>ᴗ•)

---------------环境变量---------------

1. 什么是环境变量?

环境变量 :是操作系统或程序运行时用来存储配置信息系统路径一些键值对

  • 它们就像是计算机系统的"全局设置""公共告示板",所有程序都可以查看和使用它们
  • 上面记录着操作系统或应用程序运行时需要用到的关键信息(比如:文件路径、用户名、配置参数等),程序不用自己到处找这些信息,直接查 "公共告示板" 就行

  • 比如我们在编写 C/C++ 代码时,进行链接操作时,并不需要手动指定所依赖的动态库或静态库具体存放在哪个路径下,但依然能够成功完成链接,生成可执行程序
  • 这背后的原因就在于,系统中存在相关的环境变量,它们会为编译器提供库文件的查找路径等关键信息,帮助编译器在这些指定的路径里去定位并找到所需的动态库或静态库,从而顺利完成链接过程

环境变量是操作系统为进程间传递配置信息 而设计的一种动态参数机制,全局生效且可灵活修改。


环境变量的核心特点:

  • 全局性:环境变量属于 "系统/用户级" 的配置,一旦设置,当前终端后续启动的进程都能读取到
    • 比如:你设置了一个 PATH 变量,所有程序都能通过它找到可执行文件的位置
  • 动态性:可以在系统运行中随时修改新增删除环境变量,修改后对 "新启动的进程" 立即生效(已运行的进程需重新加载才会识别)
  • 键值对结构:所有环境变量都遵循 "变量名=变量值" 的格式
    • 比如:USER=张三HOME=/home/zhangsan
      • "键"变量名(区分大小写,通常大写)
      • "值"具体的配置内容
  • 进程继承性:子进程会自动 "复制" 父进程的环境变量
    • 比如:终端是父进程,在终端里启动的浏览器、代码编辑器等子进程,会继承终端的环境变量

小故事:公共布告栏

想象一家公司(操作系统)里有一个公共布告栏

  • 布告栏本身:就是环境
  • 布告栏上的通知:就是环境变量
  • 每条通知都有一个标题变量名,如:COMPANY_NAME)和内容变量值,如:"Awesome Tech Inc."
  • 公司的任何员工(任何一个程序或用户)都可以走到布告栏前,查看这些通知来获取公共信息
  • 有些通知是行政部(系统)贴的,全员适用。有些是部门经理(用户)贴的,只在本部门生效

在这个比喻中:

  • 程序员告诉程序:"如果你需要知道公司的统一文件格式,就去布告栏看 STANDARD_FILE_FORMAT 这个通知。"
  • 程序自己会想:"我应该把日志文件存在哪里?哦,去布告栏看一下 LOG_PATH 就知道了。"

环境变量的核心思想就是: 提供一种统一中心化 的方式来管理配置,避免将配置信息硬编码在成千上万个程序中

2. 前所未闻的main函数的参数?

通过上面对环境变量的学习,小伙伴们应该对 "什么是环境变量" 还是不懂吧?

什么?(○´・д・)ノ 你说已经完全搞懂了?那鼠鼠认为你呢,要不是之前学过,要么就是天才啊!

因为刚才讲的内容,其实和很多教材、还有部分老师上课的风格很像 ------ 知识点和定义的简单罗列。

这种方式虽然能快速覆盖内容,但有个明显的问题:如果知识点之间不能形成关联,就很难真正融会贯通;而且那些精炼的定义,只有两种人能明白是什么意思:第一种是已经学过的人,第二种鼠鼠愿称为天才!

对于刚开始学习的小伙伴来说,理解这些抽象概念需要一个 "循序渐进" 的过程。就像鼠鼠接下来要讲的内容,标题可能会起得有点古怪,但其实这是一个从"main函数参数"到"环境变量",循序渐进学习的过程。


疑问:在 C语言 和 C++ 中,main函数是程序的入口点,很多人熟悉没有参数形式的 main函数,即int main() ,但是main函数真的没有参数吗?

我们不妨换个角度思考:main函数虽然是程序的入口点,但它并非在程序运行时 "自主启动" 的

  • 从程序执行的底层逻辑来看,main函数本质上也是被其他代码调用的函数(比如:C 语言运行时库中的_start函数,会在准备好程序运行环境后调用main
  • 既然是被调用的函数,按照函数调用的一般规则,它自然可以像其他函数一样接收参数

事实上,main函数确实支持带参数的形式,其中最常用的就是int main(int argc, char *argv[])这种定义

  • 这两个参数专门用于接收程序启动时从命令行传入的信息 , 让程序能根据外部输入调整运行逻辑,这种设计也体现了程序与外部环境交互的灵活性

1. argc

  • 含义argcargument count的缩写,中文意思是参数个数,它是一个整数类型变量 ,用于记录命令行中输入的参数数量, 包括程序名本身
  • 示例
    • 假设我们编译生成了一个名为test的可执行文件 ,在命令行中输入./test hello world
    • 这里./test是程序名,helloworld是额外输入的参数,此时argc的值为 3
    • 因为程序名本身也算一个参数,加上后面输入的两个参数,总共是三个

2. argv

  • 含义argvargument vector的缩写,它是一个字符指针数组 (等价于char **argv),用于存储命令行中输入的各个参数
    • 其中argv[0]存储的是程序本身的名称,从argv[1]开始依次存储命令行中输入的其他参数
  • 示例:对于./test hello world这个命令,argv[0]指向的字符串是"./test"argv[1]指向的字符串是"hello"argv[2]指向的字符串是"world"

argv将我们在命令行输入的参数,以空格为分隔符 "拆分" 成一个个独立的字符串,再用一个指针数组来存储这些字符串的地址。

具体来说,当我们在命令行输入类似./code a b c这样的指令时,整个输入其实是一个连续的字符串,但系统会自动按空格分割处理:

  • 首先拆分出./code(程序名)、abc这 4 个部分
  • 然后将它们依次存入argv数组:argv[0]指向./codeargv[1]指向aargv[2]指向bargv[3]指向c
  • 此时argc(参数个数)的值就是 4------ 正好对应argv数组中有效元素的数量

需要特别注意的是,argv数组在存储完所有有效参数后,末尾必须以NULL(空指针)结尾。

这个NULL不计入argc的计数,它的作用是给程序一个 "结束标记"------ 当遍历argv时,遇到NULL就知道已经处理完所有参数了,避免数组越界访问。


现在小伙伴们可以回想一下:我们在命令行输入的那串完整命令(比如:./code.exe -a -b),究竟是被谁分割成一个个参数的呢?

答案是:Shell 程序(比如:我们常用的 bash、zsh 等)

  • 当我们在终端敲下回车时,命令首先会被当前的 Shell 捕获
  • Shell 会按照一定规则(比如:以空格为分隔符,同时处理引号、转义字符等特殊情况)对命令字符串进行解析和分割,然后构建出argv数组
  • 把程序路径(如:./program)和各个参数(如:-a-b)依次存入数组中

之后,当 Shell 启动新进程运行我们的程序时,会把构建好的argv数组传递给进程。进程内部就通过这张argv表,获取到所有命令行参数,从而实现类似-a、-b这样的选项功能

3. main函数带参数有什么用?

main函数带参数的作用:

  • 程序定制化运行:通过在命令行传递不同参数,让程序以不同方式运行。
  • 脚本和自动化:在编写脚本或进行自动化任务时,main函数的参数可以方便地从外部传递数据。
  • 命令行工具开发:许多常用的命令行工具,像lsgrep等,都是利用main函数的参数来实现丰富功能的。
    • 例如:grep -r "keyword" /path/to/search
      • -r:是grep命令的递归搜索参数
      • "keyword":是要搜索的关键词
      • /path/to/search:是搜索路径

4. 如何让自己写的程序像命名一样运行?

4.1:将可执行程序拷贝到/bin/user目录下

看了上面的演示,大家应该能发现:ls -als -b 这类系统命令,和我们自己写的 ./code.exe -a./code.exe -b,在 "靠命令行参数控制程序功能" 的逻辑上其实完全一样 ------ 本质都是程序接收外部传的参数,再根据参数做对应的事。


不过有些小伙伴可能会疑惑:"ls 直接敲就能用,根本不用加 ./,但我们的程序必须写 ./code.exe 才能运行,这俩真的一样吗?"

其实从核心来说,系统自带的命令(比如:ls)和我们自己编的程序(比如:code.exe)没任何区别 ------ 它们都是能被系统执行的二进制文件。

区别就区别在,一个生在"罗马",一个生在"深山"。

啊,运行程序的"身世"这么重要吗?------🙁yes,因为要运行一个程序就得先让系统找到文件在哪儿


具体来说,当你在终端输一个指令(比如:lscode.exe)时,系统找程序的逻辑很固定:

  • 指令里有 "路径":要是有路径,系统就直接去这个路径下找文件,找到就跑,找不到就报错
    • ./code.exe 里的 ./ 是 "当前目录" 的路径(去"深山"的路径)
    • /bin/ls 里的 /bin/ 就是"固定目录"的路径(去"罗马"的路径)
  • 指令里没"路径":系统就只会去几个 "固定的常用目录" 里找 ------ 这些目录是系统提前设定好的,专门用来放像 lscd 这种大家常用的命令(出生在"罗马"的孩子)
    • 比如:直接输 lscode.exe

这就解释了为什么两者用起来不一样:

  • 系统提供的命令:它的文件就放在那些 "固定常用目录" 里(比如:/bin/usr/bin),所以你直接输 ls,系统去这些目录里一找就着,不用你手动写路径
  • 我们自己的程序:默认是放在你当前打开的目录(比如:你新建的 test 文件夹)里的,这个 "当前目录" 不在系统提前设定的 "固定常用目录" 里
    • 所以你直接输 code.exe,系统去那些常用目录里找一圈,肯定找不到
    • 而加个 ./(代表 "当前目录"),就是明确告诉系统:"程序在我现在打开的这个目录里,去这儿找",系统就能精准找到并运行了

简单说

  • 不是 ls 特殊,而是它的存放位置在系统 "默认会搜的目录" 里(而是因为它出生在"罗马")
  • 我们的程序在 "默认不搜的目录"(当前目录)里,所以得手动指明路径 ------ 这就是两者差异的本质。

4.2:添加环境变量

有些小伙伴可能会好奇,/usr/bin 这类目录为何如此 "特别",系统为什么会专门去这些目录里查找可执行程序呢?

进一步追问的话:系统是怎么知道,当我们输入一个可执行程序的名字时,要到 /usr/bin 这样的路径下去寻找呢?


其实答案很简单,就是环境变量啦,更准确地说,是环境变量中的 PATH 变量在 "指引" 系统。

PATH 变量里存储了一系列目录的路径,系统会按照 PATH 里的顺序,依次到这些目录中去查找我们输入的可执行程序,看看能不能找到对应的文件。


相信通过上面关于环境变量的介绍,大家对环境变量已经有了比较清晰的认识。

这时候可能有小伙伴会顺着思路想:

  • "哦,现在我明白了!之前操作系统找不到我们自己写的程序,根本原因是程序存放的目录没在环境变量里 ------ 准确来说,是没在 PATH 变量记录的路径中。
  • 那如果我把自己程序的存放目录也添加到 PATH 变量里,系统不就能像找 ls 那样,通过 PATH 找到我的程序了吗?
  • 这样一来,直接输程序名应该就能执行,不用再写 ./ 了吧?"

(系统很娇贵,像是深山老林这种穷乡僻壤它是不可能来的,所以那就让我们用双手将深山老林,打造成金山银山吧!)


没错,这个思路完全正确。既然 "添加环境变量" 的需求已经明确,那具体该怎么操作呢?

嗯,这个嘛不急不急,我们先看到环境变量之后,再提添加环境变量吧!

(1)printenv/env:查看所有的环境变量

查看所有环境变量的两种不同的方式:

1. printenv 命令

  • 作用:专门用于打印环境变量,输出格式为 "变量名 = 变量值",每个变量占一行,可读性很高
  • 用法:直接在终端输入命令,无需额外参数:printenv

2. env 命令

  • 作用:与 printenv 功能基本一致,默认也是打印所有环境变量,额外优势是可以通过参数 "临时设置环境变量并执行命令"
  • 用法:直接输入命令:env

(2)echo $环境变量名:查看指定的环境变量

实际学习中,我们很少需要一次性看所有环境变量,更多是查看某个特定变量(比如:核心的 PATH 变量),Linux 中最直接的方式是用 echo 命令搭配 环境变量的 "引用规则"

核心语法:echo $环境变量名

  • Linux 中引用环境变量时,必须在变量名前加 $ 符号,告诉系统 "这是一个变量,需要解析它的值"
  • PATH 是 Linux 中最核心的环境变量,决定了系统去哪里找可执行命令,查看它的命令:
    • 输出的多个路径用 : 分隔,系统会按这个顺序依次搜索命令
    • 比如输入 ls 时,系统会先在 /usr/local/sbin 找,找不到再去 /usr/local/bin,直到找到 /bin/ls 并执行)

如果不小心把环境变量 PATH 搞丢了,会导致 lscdpwd 这些系统基础指令全都无法执行(因为系统找不到这些指令的存放路径了),这时候不用慌,因为重启 bash 进程就可以复原。

要理解为什么重启 bash能复原,首先要明确一个关键点:

  • 我们在终端里修改的 PATH,本质是当前 bash 进程(终端对应的命令行解释器) 上下文里的 "临时变量"------ 就像 bash 在内存里用 malloc 申请了一块空间,专门用来存储 PATH 对应的路径字符串
  • 这种内存级的变量只在当前 bash 进程中生效,不会直接修改系统的永久配置文件。 所以只要能重新让 bash 加载正确的 PATH 配置,或者重启一个新的 bash 进程,就能恢复正常


5. 命令行交互的核心逻辑是什么?

当我们登录系统时,系统会自动为我们创建一个 bash 进程(命令行解释器)

bash 启动后,首先要做的就是从系统的配置文件(如:/etc/profile~/.bashrc 等)中读取环境变量信息,然后在自己的内存空间里构建一张 环境变量表

这张环境变量表的本质是一个 指针数组 ,和我们之前讲的 命令行参数表argv)在数据结构上完全一致 ------ 都是用指针按顺序指向不同的内容,只是存储的信息不同而已。

  • 具体来说,表中的每个元素都是一个字符串(比如:PATH=/usr/binHOME=/home/user
  • 第一个指针指向第一个环境变量,第二个指针指向第二个,以此类推,最后以 NULL 指针结束

当我们在命令行输入 ls -a 这样的指令时,整个过程是这样的:

  1. 首先ls -a 这个字符串会被当前的 bash 进程捕获,而不是直接被后续执行的 ls 进程拿到
  2. 其次bash 会先解析这个字符串,构建出命令行参数表 (即 argv 数组):argv[0] = "ls"argv[1] = "-a",末尾以 NULL 结束
  3. 然后bash 需要找到 ls 这个命令的可执行文件位置
    • 它会从自己维护的环境变量表 中提取 PATH 变量的值(比如 PATH=/usr/bin:/bin
    • 然后把 PATH 中的每个路径与命令名拼接(比如 /usr/bin/ls/bin/ls),逐个检查文件是否存在
  4. 接着:找到 ls 的可执行文件后,bash 会创建一个新的子进程,并在子进程中执行 ls 程序,同时将构建好的命令行参数表传递给子进程
  5. 最后:如果在 PATH 的所有路径中都找不到对应的命令,bash 就会报出 "命令不存在" 的错误(如:command not found: ls

从内存角度看:

  • bash 进程启动时,会在自己的内存空间中分配一块区域,为每个环境变量单独申请存储空间,最终形成一个二维数组结构(指针数组指向多个字符串)
  • 当我们执行 envprintenv 命令时,本质上就是让 bash 打印出这张环境变量表中的所有内容

总结来说,在 bash 进程内部,始终维护着两张核心的表:

  • 命令行参数表:用于存储当前输入的命令及其参数(如:ls -a 解析后的结果)
  • 环境变量表:用于存储系统的环境变量信息(如:PATHHOME 等)

这两张表共同支撑了命令行交互的核心逻辑 ------ 从解析用户输入,到找到并执行对应程序,背后都是这两张表在发挥作用。

6. 其他常见的环境变量有哪些?

7. 如何操作环境变量?

一、export 命令

export:主要用于将指定的变量设置为环境变量,使得该变量不仅在当前 Shell 进程中有效,还能被其创建的子进程继承。

这样,在子进程中也能访问到这个变量的值。常见的使用场景包括:

  • 设置自定义环境变量:当你希望某个变量在多个脚本或程序中都能被使用时,可以用 export 将其设置为环境变量
  • 修改或补充系统默认环境变量:例如向 PATH 环境变量中添加新的目录,以便系统能够找到更多可执行程序

语法

  • 单纯声明一个变量为环境变量export 变量名
    前提是该变量之前已经定义过,比如先执行MY_VAR="hello",再执行export MY_VAR,就能让MY_VAR变为环境变量
  • 在声明的同时赋值export 变量名=变量值
    • 例如,想要设置一个名为 APP_CONFIG_DIR 的环境变量,指定其值为 /etc/myapp,可以这样写:export APP_CONFIG_DIR=/etc/myapp
  • 查看已设置的环境变量:如果不带任何参数直接执行 export 命令,会列出当前 Shell 进程中所有通过 export 设置的环境变量及其值

示例

1. 自定义环境变量示例

  • 在上述示例中,在主 Shell 中定义并导出 MY_NAME 变量
  • 然后进入子 Shell,发现仍然可以获取到 MY_NAME 的值,这体现了环境变量的继承性

2. 修改 PATH 变量示例

二、unset 命令

unset :用于删除已经定义的普通变量环境变量

  • 一旦使用 unset 删除了某个变量,无论是普通变量还是环境变量,后续的脚本或命令将无法再访问到该变量的值,它占用的内存空间也会被释放

语法unset 变量名

8. 如何使用程序获取环境变量?

方法一:char* env[]参数

main函数的三个参数解析:

  1. int argc命令行参数的数量(argument count),至少为1(程序自身路径)
  2. char *argv[]命令行参数数组(argument vector),每个元素是指向参数字符串的指针
  3. char *env[]环境变量数组(environment vector),每个元素是指向环境变量字符串的指针
c 复制代码
#include <stdio.h>   
#include <string.h>  

/**
 * main函数的三个参数解析:
 * 1. int argc:命令行参数的数量(argument count),至少为1(程序自身路径)
 * 2. char *argv[]:命令行参数数组(argument vector),每个元素是指向参数字符串的指针
 * 3. char *env[]:环境变量数组(environment vector),每个元素是指向环境变量字符串的指针
 *
 * 这三个参数均由父进程(通常是shell)在启动程序时传递进来
 */
int main(int argc, char* argv[], char* env[])
{
    //1.(void)变量名:告诉编译器该变量已声明但暂不使用,避免未使用变量的警告
    (void)argc;  // 本示例不处理"命令行参数数量",故标记为未使用
    (void)argv;  // 本示例不处理"具体命令行参数",故标记为未使用

    //2.遍历环境变量数组并打印
    for (int i = 0; env[i] != NULL; i++) // 环境变量数组以NULL指针结尾,因此循环条件为env[i] != NULL
    {
        //打印格式:env[索引] -> 环境变量字符串(格式为"变量名=变量值")
        printf("env[%d] -> %s\n", i, env[i]);
    }

    //3.程序正常退出,返回0(约定俗成的成功退出码)
    return 0;
}


方法二:getenv库函数

getenv:是一个用于获取环境变量值的标准库函数。

  • 它在 UNIX、Linux 以及 Windows(通过 MinGW 等模拟 POSIX 环境)系统中都可以使用
  • 它包含在 <stdlib.h> 头文件中
  • 它的主要功能是从当前进程的环境变量列表中查找指定名称的环境变量,并返回其对应的值

通过这个函数,程序可以方便地获取系统或用户设置的各种环境变量信息,以便根据这些信息做出不同的行为。


getenv 函数的原型为char *getenv(const char *name);

  • 函数接受一个 const char * 类型的参数 name,表示要获取值的环境变量名
  • 返回值是一个指向字符数组的指针,指向对应环境变量的值,如果指定的环境变量不存在,则返回 NULL

方法三:environ变量

environ :是一个指向字符指针数组的外部变量,用于存储当前进程的环境变量

environ 指向的是一个字符指针数组,数组中的每个元素都是一个指向以空字符 '\0' 结尾的字符串的指针,这些字符串的格式通常是 变量名=变量值

  • 例如其中可能会有 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin""HOME=/home/user"
  • 通过遍历这个数组,就可以获取到当前进程的所有环境变量信息



鼠鼠的建议:

不过通常情况下,不太推荐直接获取所有环境变量的方式:

  • 比如,通过 main 函数的 env 参数遍历所有环境变量
  • 或者,通过 environ 变量遍历所有环境变量

因为在实际的开发场景中,我们往往只需要获取某一个具体的环境变量的值,而不是一次性获取所有环境变量。

9. 实现一个"量子"程序?

接下来我们可以设计一个 "仅自己能执行" 的程序,要实现这个需求,得先理解 Linux 中进程与环境变量的核心逻辑:

  • 系统中,程序的启动和执行都离不开 bash(或其他 Shell)
  • 当我们输入命令启动程序时,bash 会作为父进程,创建一个子进程来运行程序,并且会将自身的环境变量传递给这个子进程

而环境变量之所以需要被子进程继承,核心目的就是:

  • 让子进程能获取父进程的 "运行上下文信息"
    • 比如:当前用户身份、系统配置、自定义标识等
  • 借助这种 "继承特性",我们就能结合环境变量里的信息,实现程序的个性化逻辑
    • 比如:让程序只允许特定用户(也就是我们自己)执行

具体怎么实现呢?核心思路是利用 bash 传递给子进程的 "用户身份相关环境变量"(比如:USERUID),让程序启动时先校验这个变量的值:

  • 只有当变量匹配我们自己的用户信息时,程序才继续执行
  • 如果不匹配,就直接退出并提示 "无权限"
c 复制代码
#include <stdio.h>    
#include <stdlib.h>   // 提供getenv函数声明,用于获取环境变量
#include <string.h>   // 提供strcmp函数声明,用于字符串比较

int main(int argc, char* argv[], char* env[])
{
    //1.(void)变量名:告诉编译器这些变量已声明但暂不使用,避免"未使用变量"的警告
    (void)argc;  // 本程序不处理命令行参数数量,标记为未使用
    (void)argv;  // 本程序不处理具体命令行参数,标记为未使用
    (void)env;   // 本程序通过getenv获取环境变量,不直接使用env数组,标记为未使用

    //2.从环境变量中获取当前用户的用户名(USER变量由bash自动设置并传递给子进程)
    const char* who = getenv("USER");

    //3.容错处理:若无法获取USER环境变量(理论上极少发生),程序异常退出(返回1)
    if (who == NULL)
        return 1;

    //4.身份校验:仅允许用户名为"sanqiu"的用户执行程序核心逻辑
    if (strcmp(who, "sanqiu") == 0) //strcmp函数用于比较两个字符串,返回0表示相等
    {
        // 校验通过:执行程序的正常逻辑(此处仅为示例输出)
        printf("程序正常执行\n");
    }
    else
    {
        // 校验失败:提示仅允许"sanqiu"用户执行
        printf("只有sanqiu能执行程序!!!\n");
    }

    //5.程序正常结束,返回0(约定的成功退出状态码)
    return 0;
}

这个程序的核心逻辑,就是依赖环境变量的 "继承特性" 实现的:

  • 当我们(sanqiu)在 bash 中编译并启动这个程序时,bash 会将自己的 USER=sanqiu 环境变量传递给程序所在的子进程
  • 程序通过 getenv("USER") 获取到这个继承来的变量,与预设的 "允许用户" 对比,从而实现身份校验
  • 如果其他用户(比如:root)试图启动这个程序,子进程继承的 USER 变量会变成 root,校验不通过,程序就会拒绝执行

从这个例子也能更直观地理解 "环境变量被子进程继承" 的意义:

它让子进程(我们的程序)能 "感知" 到父进程(bash)的运行环境,进而基于这些环境信息做灵活的逻辑判断 ------ 除了 "身份校验",还能实现更多个性化操作,比如:

  • 根据 LANG 环境变量,让程序自动切换 中文/英文 提示
  • 根据自定义环境变量(如:MY_APP_CONFIG),让程序加载不同的配置文件
  • 根据 PATH 环境变量,让程序自动查找依赖的工具路径

本质上:环境变量的继承机制,就是为了让子进程能 "无缝衔接" 父进程的环境配置,从而实现更灵活、更贴合用户需求的程序逻辑。

10. 什么是本地变量?

本地变量 :是指作用范围被限制在特定 "局部环境" 内的变量。

  • 它仅在定义它的 "局部空间" 中有效,超出这个范围后就无法被访问或修改
  • 其核心特点是 "作用域受限",不会随意影响其他环境的变量

bash 等 Shell 环境中,本地变量的 "作用域" 主要与 Shell进程/子进程、函数 绑定,是最常接触的本地变量场景。

定义方式 :直接通过 "变量名 = 值" 的格式定义,不需要 export 命令(export 会将变量升级为环境变量,能被子进程继承)

11. 本地变量有什么特别的?

本地变量核心特性是作用域受限,Shell 本地变量的作用域主要有两个限制:

  • 不被子进程继承:如果在当前 Shell 中启动子进程(如:执行 bash 打开新 Shell、运行脚本),子进程无法访问父进程的本地变量

  • 函数内的本地变量(狭义本地变量):如果在 Shell 函数中用 local 关键字定义变量,该变量仅在函数内部有效,函数外部无法访问(即使是同一 Shell 进程)

好麻烦啊,为什么要搞一个什么本地变量?

因为它解决了 "全局变量污染、资源浪费、协作困难" 等核心问题,是所有编程范式(包括 Shell 脚本)中 "管理变量状态" 的最优解之一。

  • 本地变量本质是一种 "约束"------ 通过限制变量的作用域,换取了程序的稳定性、资源的高效性、代码的可维护性
  • 如果所有变量都是 "全局的"(作用域覆盖整个 程序/Shell),不同模块、函数或进程会轻易修改彼此的变量,导致状态混乱 ------ 这就是 "变量污染",本地变量通过 "作用域隔离",从根源上解决这个问题

简单来说不需要全局使用的变量,就应该用本地变量 ------ 这是写出可靠、高效代码的基本准则

13. export命令不为人知的一面?

我们知道,在 Shell 中定义本地变量后,只要在前面加上 export 命令,这个变量就能从 "仅当前 Shell 可见的本地变量" 升级为 "可被子进程继承的环境变量"

但这里很容易产生一个疑问:如果 export 是像 ls那样的 外部命令 ,执行时会创建子进程,可按照环境变量的继承规则:只有父进程能将环境变量传递给子进程,子进程无法反向修改父进程的环境变量

export 是怎么做到让父进程(也就是我们当前的 bash)更新自身环境变量的呢,难道说export是......嘿嘿🤭?


答案确实是export 并不是需要创建子进程的 "外部命令",而是 bash 自带的内建命令(Built-in Command)

那什么是内建命令?

简单来说就是 bash 自身代码中实现的命令,执行时不需要通过 fork 系统调用创建新的子进程,而是直接由当前的 bash 进程亲自执行

  • 要么调用自身内部的函数,要么直接发起系统调用来完成操作
  • 正因为不需要创建子进程export 才能直接修改当前 bash 进程的环境变量列表,而不是在一个临时子进程中做无用功

举个更直观的例子就能理解:

  • 如果我们执行 ls 这类外部命令,bash 会先创建一个子进程,再让子进程去加载 ls 的可执行文件并运行
  • 但执行 export MY_VAR=123 时,bash 不会创建任何子进程,而是直接在自身的内存空间里,将 MY_VAR=123 添加到环境变量列表中

这也是为什么 export 能 "实时生效",且后续在当前 bash 中启动的子进程(比如:执行 pythongcc,或是新打开一个 bash 子 Shell)都能继承到这个变量。


之前我们可能遇到过这样的情况:如果不小心弄丢了关键的环境变量(比如:误操作清空了 PATH),会发现:

  • lsgcc 这些命令突然用不了了
  • 因为 PATH 变量存储的是系统查找可执行文件的路径,没了 PATHbash 不知道该去哪里找这些外部命令的可执行文件
  • 但奇怪的是,pwdecho 这些命令却还能正常使用
    • 原因正是它们和 export 一样,都是 bash 的内建命令:它们的实现代码本身就包含在 bash 进程里,不需要依赖 PATH 去查找外部文件,只要当前的 bash 进程还能正常运行,这些内建命令就能直接被调用,自然不会受 PATH 丢失的影响

简单总结一下:

  • export 之所以能修改当前 Shell 的环境变量,核心是因为它是 bash 的内建命令,无需创建子进程就能直接操作父进程(当前 bash)的环境变量列表
  • 而像 pwd 这类内建命令,之所以在环境变量丢失时仍能使用,也是因为它们无需依赖外部文件,是 bash 自身携带的 "基础功能"

这也从侧面体现了内建命令在 Shell 中的特殊作用 ------ 它们是 bash 与用户交互、管理自身状态的核心工具,而非独立的外部程序。

相关推荐
乌萨奇也要立志学C++2 小时前
【洛谷】二分查找专题 告别二分死循环!模板 + 细节 + 实战
c++·算法
Yue丶越2 小时前
【C语言】数据在内存中的存储
c语言·开发语言·网络
Rock_yzh2 小时前
LeetCode算法刷题——128. 最长连续序列
数据结构·c++·算法·哈希算法
云边有个稻草人2 小时前
手机也能控 Linux?Cpolar+JuiceSSH 搞定内网远程
运维·服务器·cpolar
wheeldown2 小时前
【Rokid+CXR-M】基于Rokid CXR-M SDK的博物馆AR导览系统开发全解析
c++·人工智能·ar
Altair12312 小时前
nginx的https的搭建
运维·网络·nginx·云计算
利刃大大2 小时前
【c++中间件】语音识别SDK && 二次封装
开发语言·c++·中间件·语音识别
云计算练习生2 小时前
linux shell编程实战 10 Git工具详解与运维场景实战
linux·运维·git
Umi·2 小时前
iptables的源地址伪装
运维·服务器·网络