【Linux】进程调度算法、进程切换、环境变量

前言

这篇文章聚焦 Linux 的进程调度算法、进程切换与环境变量:拆解调度规则如何分配 CPU 资源,解析进程切换的底层步骤,梳理环境变量对程序运行的影响,帮你理清这些机制在系统中的核心作用。

📚 Linux 入门篇

-【 Linux 历史溯源与指令入门 】

-【 Linux 指令进阶 】

-【 Linux 权限管理 】

🔧 Linux 工具篇

-【 yum + vim 】

-【 sudo白名单配置 + GCC/G++ 】

-【 自动化构建:make + Makefile 】

-【 倒计时 + 进度条 】

-【 Git + GDB调试器 】

⚙️ Linux 进程篇

-【 冯诺依曼体系 + 操作系统 】

-【 进程概念 + PID + fork函数 】

-【 进程状态 】

-【 进程优先级 】


目录

一、进程调度

Ⅰ、进程调度概念

Ⅱ、位图(bitmap)中位的定位与状态判断

Ⅲ、进程调度流程

二、进程切换

Ⅰ、进程切换概念

Ⅱ、进程上下文是什么?

Ⅲ、cpu中的寄存器是什么?

【问题】:为什么函数返回值会被外部拿到?

【问题】:系统如何知道进程当前执行到哪行代码?

Ⅳ、如何进行进程切换?

【小故事】:学生当兵

【进程切换核心步骤】

三、环境变量

Ⅰ、环境变量概念

[问题 1:为什么系统指令不用加./就能直接执行,自己写的指令却需要?](#问题 1:为什么系统指令不用加./就能直接执行,自己写的指令却需要?)

[问题 2:如何查看环境变量?](#问题 2:如何查看环境变量?)

[问题 3:如何让自己写的指令不用加./就能执行?](#问题 3:如何让自己写的指令不用加./就能执行?)

[问题 4:不小心覆盖了系统指令的默认搜索路径,怎么恢复?](#问题 4:不小心覆盖了系统指令的默认搜索路径,怎么恢复?)

[问题 5:为什么登录系统后会直接进入家目录?](#问题 5:为什么登录系统后会直接进入家目录?)

[问题 6:echo SHELL输出/bin/bash是什么意思?](#问题 6:echo SHELL输出/bin/bash是什么意思?)

[问题 7:SHELL的核心作用是什么?](#问题 7:SHELL的核心作用是什么?)

env命令

【核心环境变量表格】

环境变量如何被组织?

Ⅱ、命令行参数

[Q:为什么要让 main 函数支持命令行参数(argc/argv)?](#Q:为什么要让 main 函数支持命令行参数(argc/argv)?)

C:环境变量参数

Ⅲ、如何获取和设置环境变量?

G:如何获取环境变量?

S:如何设置环境变量?

1、临时添加环境变量(仅当前终端会话生效)

2、永久添加环境变量(所有终端会话生效)

3、临时取消(仅当前终端生效,关闭终端即失效)

4、永久取消(所有终端生效,需删除配置文件中的定义)

[Ⅳ、环境变量的继承性 + 全局属性 +本地变量](#Ⅳ、环境变量的继承性 + 全局属性 +本地变量)

1、继承性

2、全局属性

Q:为啥子进程修改环境变量不影响父进程?

3、本地变量

4、内建命令和常规命令

【模拟cd内建指令】


一、进程调度

Ⅰ、进程调度概念

Linux 的进程调度,是内核借助 "调度器" 这个组件,按照预设的调度算法,来分配 CPU 执行权的管理过程,解决 "CPU 单时刻只能跑一个任务,但系统里同时堆了一堆进程 / 线程要执行" 这个矛盾的管理过程 ------ 核心管三件事:选哪个任务用 CPU、给它用多久、啥时候让它把 CPU 让出来

(注:"调度" 是分配 CPU 执行权的整体过程,而 "调度算法" 是支撑这一过程的具体规则,决定 "给谁先分配、分配多久、啥时候让他把cpu让出来")

  • ***为啥要做这事?***因为 CPU 是 Linux 里最金贵的硬件资源,但它 "一心不能二用";可你开的终端、后台的服务、跑的程序,都等着抢 CPU 干活。
  • 调度的目标:让这些任务 "看着像同时在跑"、别让 CPU 空着浪费,还得保证高优先级的任务(比如系统服务)能先抢到资源。

它是 Linux 能实现多任务并发的 "幕后指挥",也是把 "一堆待执行的任务" 和 "CPU 硬件" 连起来的核心纽带。

Ⅱ、位图(bitmap)中位的定位与状态判断

cpp 复制代码
### 位图(bitmap)中位的定位与状态判断
在优先级调度的位图机制中,通过以下方式定位指定序号N对应的比特位,并判断其状态:
1. 定位所在char元素:
    i = N / (sizeof(char)*8)
   (`sizeof(char)*8`是单个char的比特数,通常为8;i表示N对应的比特位所在的char数组下标)

2. 定位char内的比特位:
    pos = N % (sizeof(char)*8)
   (pos表示N在当前char元素中的具体比特位下标)

3. 判断比特位是否置1:
   实际操作应为  b.bits[i] & (1 << pos)
   (通过位运算判断该比特位是否被标记为1)

Ⅲ、进程调度流程

【简略版运行队列结构体】:


这两个指针数组分别存储了140个优先级对应的进程队列;

  • ***普通优先级:***我们只关心下标100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
  • ***实时优先级:***0~99(不关心)

进程被调度时,会按照从上到下,从左到右的顺序进行轮换调度,这个时候有人就会有疑问了,进程被调度时,如果调度过程中突然有新进程插进来怎么办?

其实完全不用担心新进程插入会打乱现有队列秩序 ------ 这类新进程会被精准插入到waiting数组对应优先级的队列(和running数组完全一样),并且是直接追加到队尾,不会插队到当前正在排队的进程前面,原有进程的排队顺序完全不会被改动。

调度的顺序是先把running数组里的进程调度完,再去调度waiting数组里的进程。


这里的runwait两个指针,就是用来管理这两个数组的:run指针负责指向running数组,wait指针负责指向waiting数组。等要切换调度的数组时,不用动数组里的进程数据,只需要把这两个指针存储的地址交换一下,就能从调度running数组的进程,切换到调度waiting数组的进程了。


【总流程梳理】:

<1> 基于优先级的时间片轮转调度算法

  1. 选进程运行 遍历普通优先级队列queue[100~139],取首个非空队列的队首进程,通过上下文切换使其进入运行态(R 态),占用 CPU 执行。

  2. 监控切换触发条件 进程运行中,触发任意情况则立即执行进程切换

    • 被动切换 1:当前进程时间片耗尽;
    • 被动切换 2:有更高优先级进程就绪(抢占 CPU);
    • 主动切换:当前进程请求 I/O/ 调用sleep()(主动放弃 CPU)。
  3. 切换后的即时处理(调度收尾) 完成进程切换后,对原运行进程做状态 + 队列安置:

    • 时间片耗尽:保持R 态,尾插到原优先级队列队尾;
    • 被抢占:保持R 态,回到原优先级队列队首;
    • 主动放弃 CPU:进入可中断睡眠态(S 态),移出运行队列(runqueue),加入等待队列(wait queue)。
  4. 回到调度起点重复步骤 1,从运行队列选新进程运行。


<2> 阻塞进程的后续处理(仅针对主动放弃 CPU 的场景)

当处于等待队列的S 态进程完成等待事件(如 I/O 结束)后:

  1. 从等待队列(wait queue)移出;
  2. 尾插到原优先级的运行队列(runqueue),恢复为R 态,等待下一轮调度。

"遍历 queue 140(哪怕只看 100,139 区间),虽然时间复杂度是常数,但每次判空都要逐个检查队列是否为空,实际还是不够高效!

因此这时候就用到了我们先前说的位图:用char bits[5](共 40 个比特位)的每一位对应一个优先级队列,位为 1 表示对应队列非空,位为 0 表示对应队列为空。这样不需要遍历数组,直接通过位运算就能快速定位到第一个非空的队列,彻底解决了'遍历判空低效'的问题。

【总结】:

这整个流程是标准的O (1) 调度算法 :从位图快速定位非空队列、调度时直接取队列头进程执行,到新进程队尾插入、交换 run/wait 指针切换调度数组,所有关键操作均只需固定步数完成,耗时与进程 / 队列数量无关,整体为常数时间复杂度。


二、进程切换

Ⅰ、进程切换概念

*进程切换:*操作系统中,CPU 从正在执行的 A 进程,暂停其运行并切换到 B 进程继续执行的核心操作。


核心是 "保存 + 加载" 上下文:先把 A 进程的运行现场 (执行位置、寄存器数据、地址映射等)保存 到进程控制块(PCB),再从B 进程的 PCB 中加载其上下文并恢复,让 B 进程从上次暂停的节点无缝继续执行,最终实现多进程 "并发"(宏观同时运行,微观 CPU 轮流执行)。

Ⅱ、进程上下文是什么?

*进程上下文:*进程运行的完整 "现场快照",是进程能被暂停后无缝恢复执行的核心依据,包含进程继续运行所需的所有关键信息,可分为四大类核心组成(每类都对应实际运行必需的功能)


1、CPU 执行相关(核心中的核心)

  • 程序计数器(PC):记录 CPU 下一条要执行的指令地址,确保恢复时知道 "从哪继续跑"。
  • 通用寄存器:存储当前执行的中间数据(比如计算结果、变量值),切换时必须完整保存 / 恢复,否则数据丢失。
  • 状态寄存器、栈指针(SP)、基址指针(BP):记录 CPU 运行状态、栈空间地址范围,保障函数调用、数据存取的连续性。

2、内存映射相关

  • 页表:记录进程虚拟地址与物理内存地址的映射关系,确保恢复后 CPU 能精准找到进程的代码、堆、栈数据。
  • 地址空间信息:进程专属的虚拟地址范围(如代码段、数据段、堆、栈的边界),避免与其他进程内存冲突。

3、进程资源相关

  • 打开的文件句柄:比如进程正在读写的文件、网络套接字,切换后仍能正常操作这些资源。
  • 信号掩码与处理函数:记录进程能响应的信号、以及信号触发后的处理逻辑,避免信号处理混乱。
  • I/O 设备状态:如打印机、键盘等设备的连接状态,确保恢复后 I/O 操作能继续

4、进程管理相关

  • 进程状态(R/S/D/T 等):标记进程当前是就绪、阻塞还是运行态。
  • 优先级与时间片:决定进程下次被调度的顺序和可占用 CPU 的时长。
  • 进程控制块(PCB)指针:上下文的所有信息最终都存储在 PCB 中,切换时直接操作 PCB 即可。

总之,进程上下文就是进程运行的完整现场快照,核心目的是让进程被切换后重新调度时,能无缝衔接上次的执行状态,就像从未被打断过一样。

Ⅲ、cpu中的寄存器是什么?

CPU 寄存器是CPU 内部的高速存储单元,用于临时存放进程运行的关键数据(属于进程上下文的核心部分),特点是速度远快于内存,主要作用是支撑进程的高效执行:


和进程的关系 寄存器里存的是当前进程的临时数据,属于进程上下文的一部分 ------ 进程切换时,这些数据会被保存到 PCB,恢复时再加载回寄存器,确保进程能无缝续跑。

【问题】:为什么函数返回值会被外部拿到?

函数执行完return a(对应mov eax 10)后,外部代码会把寄存器(如 EAX)中的值 "拷贝" 到自己的变量中 ------ 比如外部写int b = func(),汇编层面会执行mov b, eax,把 EAX 里的返回值拷贝到变量b的内存地址中。

【问题】:系统如何知道进程当前执行到哪行代码?

程序计数器(PC/EIP 寄存器):这个寄存器专门记录 "当前进程正在执行的指令的下一行指令地址";进程运行时,CPU 会根据 PC/EIP 的地址取指令执行;进程切换时,PC/EIP 的值会随上下文保存,恢复时就能精准回到上次的执行位置。

Ⅳ、如何进行进程切换?

【小故事】:学生当兵

你是个大二高个男生,某天在食堂门口瞥见 "大学生士兵招募" 的海报 ------ 服役一年就能返校接着读书,你揣着好奇报了名,居然一路通关选上了。

兴奋冲昏了头,你踹开宿舍门吼:"哥几个,我去当兵了!明年见!" 转身就走 ------ 可转念一想:这一年不上课、不考试,回来怕是要被退学。

赶紧找辅导员救命,他一拍桌子:"得先办'保留学籍'!" 第二天,他塞给你个牛皮档案袋,沉甸甸的:"这里面是你这学期的成绩单、学到哪章的进度表,还有你的学籍信息 ------这袋东西就是你的'全部家底',退伍回来必须原封不动给我,我才能把你'接回'大二。"

揣好档案袋,你才算踏实入伍。

一年后扛着行李返校,没急着进教室,先攥着档案袋找辅导员 ------ 他对着袋里的进度表核对半天,把你塞进了这届大二的班级:"跟上,从你去年停的那章接着学。"

这个事儿,和操作系统里的 "进程切换" 简直是一个模子刻的:

  • 你 = 进程:是要干的 "活儿"(你学知识,进程算数据);
  • 那个档案袋 = 进程上下文:装着你 "接着干" 的所有信息(档案袋存学习进度,上下文存寄存器、执行位置);
  • 办保留学籍 = 保存上下文:先把 "干活的状态" 存好;
  • 去当兵 = 进程离开 CPU:暂时停下,把核心资源让出去;
  • 返校复学 = 恢复上下文:把存好的状态调出来,接着从上次停的地方干。

说白了,进程切换不是 "说停就停、说开就开"------ 得先把 "干活的摊子" 收好,腾地方,回来再原样摆开接着干,就像你揣着档案袋去当兵,回来还能续上大二的课。

【进程切换核心步骤】

1、保存当前进程上下文: 进程离开 CPU 时,将其寄存器、PC、内存映射等信息写入自身 PCB(相当于 "存档案")

2、调度器选择目标进程: 由进程调度器从运行队列队列中选一个待执行的进程

3、恢复目标进程上下文: 从目标进程的 PCB 中,把之前保存的信息加载回 CPU 寄存器、更新内存页表(相当于 "取档案恢复状态")

4、CPU 执行目标进程: 目标进程从上次暂停的位置继续运行


三、环境变量

Ⅰ、环境变量概念

环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数

  • 格式:变量名 = 变量值(键名通常大写,值可是字符串、路径或路径列表)。
  • 作用:动态传递配置(如路径、身份标识),替代硬编码,适配不同环境。
  • 特点:全局可见(进程可继承)、可临时 / 永久修改,系统自带或用户自定义均可。

如:我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

问题 1:为什么系统指令不用加./就能直接执行,自己写的指令却需要?

因为系统中有PATH环境变量,它存储了系统指令的默认搜索路径;执行指令时,系统会依次在PATH的路径中查找,找到对应程序就直接执行(系统指令在PATH路径里,所以不用加./);而自己写的指令不在PATH默认路径中,所以需要用./指定当前目录。

问题 2:如何查看环境变量?

echo $NAME //NAME:你的环境变量名称

例:在终端执行echo $PATH,即可显示PATH中所有的默认搜索路径(多个路径用冒号:分隔)。

问题 3:如何让自己写的指令不用加./就能执行?

把自己写的指令所在目录添加到PATH中,临时生效的命令是expor PATH=PATH:指令所在目录路径(比如PATH=PATH:/home/whb/108/lesson13);添加后,系统会在新路径中搜索指令,此时直接输入指令名即可执行。

  • PATH=PATH 环境变量 "赋值 / 重置" 的语法开头
  • $PATH:代表当前已有的PATH内容(保留原有的所有搜索路径,避免覆盖);
  • ::是路径的分隔符(Linux 中用冒号分隔多个路径);
  • 指令所在目录路径:要追加的新目录(比如/home/whb/108/lesson13),即让系统以后在这个目录下搜索指令。

此时直接输入 mytest,无需加 ./ 即可执行,输出结果为 "我是一个进程:PID:15019"------ 这说明自定义指令已能通过 PATH 搜索到并正常运行。

问题 4:不小心覆盖了系统指令的默认搜索路径,怎么恢复?

只需重新登录 Xshell 即可恢复 ------ 因为临时修改的PATH仅在当前终端会话生效,会话关闭(重新登录)后会自动加载系统默认的PATH配置。


Linux 终端(比如 Xshell)启动时,会从系统 / 用户的配置文件(如/etc/profile~/.bashrc)中读取默认PATH并加载到当前会话的内存里

当你在终端里临时修改PATH(比如执行PATH=/错误路径),这个修改只在当前会话的内存中生效 ,并没有写入任何配置文件。一旦关闭终端(或重新登录),当前会话的内存数据会被清空;新会话启动时,会重新从配置文件加载默认PATH,自然就恢复成系统原始配置了。

简单说:临时修改是 "内存级的临时变量",没动到存储配置的文件,所以重启会话就能恢复~

问题 5:为什么登录系统后会直接进入家目录?

因为$HOME环境变量记录了当前用户的家目录路径;系统登录流程会自动读取$HOME的值,然后切换到对应的目录(比如 root 用户的$HOME/root,普通用户的$HOME/home/用户名)。

问题 6:echo $SHELL输出/bin/bash是什么意思?

$SHELL是记录当前用户默认 Shell 程序的环境变量,/bin/bash是该 Shell(bash)在系统中的实际存储路径 ------ 即当前用户登录后默认用 bash 进行命令交互。

问题 7:$SHELL的核心作用是什么?

核心作用是告诉系统 "当前用户登录后默认使用哪个 Shell 程序进行命令交互",其输出的路径对应该 Shell 程序在系统中的存放位置(比如默认 Shell 是 zsh 时,echo $SHELL会显示/bin/zsh)。

env命令

*作用:*用于查看当前终端的所有环境变量

【核心环境变量表格】

| 环境变量名 | 核心作用 | 示例值 | 实用场景 |
| PATH | 系统搜索可执行命令的路径列表,决定输入命令时系统去哪里找程序 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin | 1. 不用加./ 直接执行自定义程序(需把程序路径加入 PATH);2. 系统指令(如 ls/cd)能直接执行的核心原因 |
| HOME | 当前用户的家目录路径,登录后默认进入的目录 | /home/ljh(普通用户)、/root(root 用户) | 1. cd ~/cd HOME 一键回到家目录;2. 存储用户个人配置文件(如.bashrc) | | SHELL | 当前用户默认使用的 Shell 解释器路径 | /bin/bash(最常用)、/bin/zsh | 决定终端的命令交互规则(如语法、快捷键),新手主要用 bash | | USER | 当前登录的用户名 | ljh、root | 区分当前操作的用户身份,权限相关操作(如 sudo)会用到 | | PWD | 当前所在的工作目录路径 | /home/ljh/code-under-linux/Test-course | 等同于 pwd 命令的输出,脚本中可动态获取当前目录 | | LANG | 系统语言 / 字符编码设置 | en_US.utf8、zh_CN.utf8 | 决定终端 / 程序的字符显示格式(避免中文乱码) | | TERM | 终端类型,适配终端的显示 / 交互规则 | xterm-256color | 确保终端能正常显示颜色、光标、快捷键等(Xshell/SSH 登录时自动适配) | | LOGNAME | 当前登录用户的名称(和 USER 基本一致) | ljh | 脚本中判断当前操作用户身份 | | OLDPWD | 上一次所在的工作目录路径 | /home/ljh | 执行 cd - 可一键回到上一次的目录,新手快速切换目录常用 | | PS1 | 终端命令行提示符的格式(决定 \[ljh@[localhost](https://localhost/ "localhost") \~\] 这类显示) | \\u@\\h \\W$ | 自定义提示符样式(如显示当前路径、用户名),新手了解无需修改 |

HISTSIZE 控制终端保存的历史命令数量,是 "按上下键调取历史指令" 的功能源头 3000 1. 按↑/↓键快速调取之前执行过的命令;2. 调整数值可增加 / 减少历史命令的存储条数

环境变量如何被组织?


每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以 '\0' 结尾的环境字符串


Ⅱ、命令行参数

我们编写代码时写的 main 函数实际是有参数的,只不过我们先前用不到罢了 ------ 而这些参数,对应的就是命令行参数 :比如在终端执行./mytest arg1 arg2时,程序名./mytest后面跟着的arg1arg2,就是命令行参数。这些内容会被传递给 main 函数的参数,让程序能根据输入的不同参数,执行对应的逻辑。

常用的main函数参数:

int argc

  • 全称argument count(参数个数)
  • 作用 :记录终端执行程序时,传入的 "命令行参数的总数量"。
    • 注意:程序名本身会被算作第 1 个参数

char* argv[]

  • 全称argument vector(参数向量 / 数组)
  • 作用 :是一个字符串数组,存储了终端传入的每一个命令行参数
    • argv[0]:默认是程序本身的路径 / 名称
    • argv[1]:第 1 个用户传入的参数;
    • argv[2]:第 2 个用户传入的参数;
    • ...... 以此类推,最多有argc个元素。

在 C 语言的char* argv[]数组中(该数组也被称为 "命令行参数表" ),无论参数数量是多少,argv的最后一个有效元素的下一个位置(即argv[argc])会被系统自动初始化为 NULL。

在执行命令./test a b c时,这些参数会按顺序存入char* argv[]这个字符指针数组,具体对应关系是:

  • argv[0] 存储第一个参数:"./test"(程序本身的路径 / 名称)
  • argv[1] 存储第二个参数:"a"
  • argv[2] 存储第三个参数:"b"
  • argv[3] 存储第四个参数:"c"

(补充说明:终端中参数之间的空格是分隔符 ,所以./test a b c会被拆分成 4 个独立的字符串,依次填入argv数组的对应位置)

既然 main 函数可以接收 argc 和 argv 参数,这就直接证明 main 函数并非程序运行的 "原生起点"------ 程序启动时,操作系统先调用启动函数(如Startup()CRTStartup(),不同编译环境命名略有差异),由这些启动函数完成程序运行前的基础初始化(比如内存分配、运行环境配置),最终再由启动函数主动调用 main 函数,并将命令行参数封装为 argc 和 argv 传递给它。

简言之:操作系统 → Startup/CRTStartup(启动函数) → main函数(接收参数执行),main 函数的参数正是这一调用链的直接体现。


Q:为什么要让 main 函数支持命令行参数(argc/argv)?

核心是为指令、工具、软件等提供命令行选项的支持 ------ 通过在执行程序时传入不同参数,能让同一个程序实现不同功能。比如ls -lls是程序,-l是命令行参数),传入-l就能让ls以详细列表形式显示文件,不传则是默认格式,灵活适配不同使用场景。

如果想实现多选项,可以用循环遍历所有参数 + 逐个匹配判断,示例代码:

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    // 1. 无参数时打印用法提示(argc=1 表示只有程序名)
    if (argc == 1)
    {
        printf("Usage: %s -[a|b|c|d] [多个参数可叠加]\n", argv[0]);
        printf("示例: %s -a -b -c\n", argv[0]);
        return 1; // 退出程序,避免后续逻辑执行
    }

    // 2. 遍历所有传入的参数(i从1开始,跳过argv[0]程序名)
    for (int i = 1; i < argc; i++)
    {
        // 逐个判断参数,匹配则执行对应功能
        if (strcmp(argv[i], "-a") == 0)
        {
            printf("执行功能1(参数-a)\n");
        }
        else if (strcmp(argv[i], "-b") == 0)
        {
            printf("执行功能2(参数-b)\n");
        }
        else if (strcmp(argv[i], "-c") == 0)
        {
            printf("执行功能3(参数-c)\n");
        }
        else if (strcmp(argv[i], "-d") == 0)
        {
            printf("执行功能4(参数-d)\n");
        }
        else
        {
            // 3. 识别到非法参数时提示,并继续处理其他参数
            printf("警告:无效参数 %s,仅支持 -a/-b/-c/-d\n", argv[i]);
        }
    }

    return 0;
}

C:环境变量参数

实际上,我们也可以通过main函数的环境变量参数(如char *envp[]),获取当前程序所在 bash 进程的所有环境变量。

envpargv是同类型的字符指针数组(都属于 "参数向量表"),envp对应的是环境变量表;由于环境变量的数量不固定,所以需要通过envp末尾的NULL指针来判断遍历的结束。

Ⅲ、如何获取和设置环境变量?

G:如何获取环境变量?

前面已经讲了 3 种获取环境变量的方式:

  1. 终端中通过echo命令(如echo $PATH),直接打印单个环境变量的值;
  2. 终端中通过env命令,查看所有环境变量的列表;
  3. C 程序中通过main函数的环境变量参数(如char *envp[]),在代码中获取环境变量。

接下来我再讲解一种通过C语言接口获取环境变量的方法。

S:如何设置环境变量?

1、临时添加环境变量(仅当前终端会话生效)

基本语法

bash 复制代码
export 环境变量名=值

示例:

说明

  • export的作用是将变量标记为 "环境变量",使子进程可以继承该变量;
  • 关闭终端后,该环境变量会失效。

2、永久添加环境变量(所有终端会话生效)

需将设置指令写入 Shell 配置文件(以 Bash 为例):

编辑配置文件

bash 复制代码
echo 'export 环境变量名=值' >> ~/.bashrc

示例:

bash 复制代码
echo 'export MYENV="hello world"' >> ~/.bashrc

当前终端进程想用新配置 → 必须执行 source ~/.bashrc(相当于 "给当前 Shell'刷新内存'");

不想执行 source → 直接关闭当前终端,重新打开新终端即可(新终端启动时会自动读取修改后的~/.bashrc)。

说明

  • 不同 Shell 对应的配置文件不同(如 Zsh 对应~/.zshrc);
  • 配置文件修改后,新启动的终端会自动加载该环境变量。

3、临时取消(仅当前终端生效,关闭终端即失效)

核心命令:unset

bash 复制代码
# 取消单个环境变量(比如你设置的MYENV)
unset MYENV

# 验证是否取消:输出为空则成功
echo $MYENV

补充:清空 PATH(慎用!)

如果想临时清空某个环境变量的值(而非彻底取消),可以直接赋值为空:

bash 复制代码
# 仅清空值,变量仍存在(不推荐,容易踩坑)
MYENV=""
# 验证:输出为空,但变量还在
env | grep MYENV  # 仍能看到MYENV=

4、永久取消(所有终端生效,需删除配置文件中的定义)

针对你之前写到~/.bashrc里的export MYENV="hello world",操作步骤:

编辑配置文件,删除环境变量定义:

bash 复制代码
# 用vim打开~/.bashrc
vim ~/.bashrc

# 操作步骤:
# 1. 按G跳到文件最后一行
# 2. 找到你添加的export MYENV="hello world"这一行
# 3. 按dd删除该行(或在行首加#注释掉)
# 4. 按ESC → 输入:wq 保存退出

让修改生效:

bash 复制代码
# 刷新当前终端的配置(立即永久取消,不想用该指令重新打开终端也会生效)
source ~/.bashrc

# 验证:无论当前终端/新终端,echo $MYENV 都为空
echo $MYENV  # 输出空

Ⅳ、环境变量的继承性 + 全局属性 +本地变量

1、继承性

父进程(Shell)通过export MYENV=123456设置环境变量后,子进程(./test程序)能通过getenv继承并读取该值(运行./test输出 "获取到的 MYENV 值:123456");

2、全局属性

环境变量的 "全局" 是父→子进程链条内的单向共享:子进程会继承父进程的环境变量,但子进程修改的是独立副本,不影响父进程。

子进程会继承父进程的环境变量:


子进程对环境变量的修改不影响父进程:

**Q:**为啥子进程修改环境变量不影响父进程?

这是因为进程的环境变量是 "复制继承" 而非 "共享" ,底层逻辑是:当父进程创建子进程时,会把自己的环境变量完整复制一份给子进程 ------ 子进程拿到的是 "副本",不是和父进程共享同一份数据。所以:

  • 子进程修改的是 "自己的副本",不会动到父进程的原始数据;
  • 就像你复制了一份文件,修改副本不会影响原文件,是一个道理。

3、本地变量

本地变量是仅在当前 Bash 进程内生效的变量 (不加export的变量),不会被 Bash 的子进程继承。

证明本地变量不会被继承:

4、内建命令和常规命令

之前我们认为 "所有命令都会以 Bash 进程的子进程形式执行",这个说法其实不准确。实际使用中,Shell 命令可以分为两类:常规命令内建命令

1. 常规命令

常规命令的执行,是通过创建子进程来完成的------ 这些命令是独立的程序,Bash 会启动一个新的子进程,让子进程去运行这个程序,程序跑完子进程就结束。

2. 内建命令

内建命令的执行,Bash 不会创建子进程,而是由 Bash 自己直接执行 ------ 相当于 Bash 调用了自己内置的功能(类似 Bash 自己写好的函数),不用额外开新进程。

执行./test(常规命令)时,Bash 会创建子进程,但父进程的本地变量不会传给子进程,所以./test读不到MYENV

echo是内建命令,执行时 Bash 不会开子进程,直接在自身进程内运行,因此能访问到父进程的本地变量,所以echo $MYENV能输出123


【模拟cd内建指令】

在 Shell 中,cd是内建指令,它的底层是通过chdir系统调用函数实现的。

chdir函数

  • 函数原型int chdir(const char *path);
  • 功能:用于修改当前进程的工作目录。
  • 参数path:字符串形式的目标路径(支持绝对路径 / 相对路径)。
  • 返回值
    • 成功:返回0
    • 失败:返回-1,同时会设置errno标识错误原因
cpp 复制代码
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[], char *envp[])
{
    sleep(20); // 延迟,方便观察进程状态
    printf("change begin\n");
    if (argc == 2)  // 接收命令行参数作为目标路径
    {
        chdir(argv[1]); // 调用chdir修改当前进程的工作目录
    }
    sleep(20);
    return 0;
}

第一次(change begin前):指向的是原目录/home/lih/code-under-linux/Test-course

第二次(change begin后):指向的是目标目录/home/lih------ 说明chdir确实成功修改了子进程的工作目录!

ls -ld /proc/[进程ID]/cwd

作用 :查看cwd(进程工作目录的软链接)自身信息

相关推荐
大树8817 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠17 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质17 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush417 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52017 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz17 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工18 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智19 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩19 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_19 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化