
往期《Linux系统编程》回顾:/------------ 入门基础 ------------/ 【Linux的前世今生】 【Linux的环境搭建】 【Linux基础 理论+命令】(上) 【Linux基础 理论+命令】(下) 【权限管理】 /------------ 开发工具 ------------/ 【软件包管理器 + 代码编辑器】 【编译器 + 自动化构建器】 【版本控制器 + 调试器】 【实战:倒计时 + 进度条】 /------------ 系统导论 ------------/ 【冯诺依曼体系结构 + 操作系统基本概述】 /------------ 进程基础 ------------/ 【进程入门】 【进程状态】 【进程优先级】 【进程切换 + 进程调度】
hi~ 小伙伴们的大家好啊!( ˘ ³˘)♥ ⏳转眼间十一月就走到了月末,这篇博客也算是本月的最后一篇啦~ 告别进程基础篇,咱们正式开启进程学习的第二阶段 —— 换个视角,深挖进程背后更核心的系统逻辑! (*°ω°)ノ"
----- 2025 年 11 月 28 日(十月初九)周五,感恩节后一天 |
|---|
今天的主角,就是进程运行的 “隐形靠山”——【环境变量】 🛠️,先给大家通俗剧透核心内容,不搞抽象概念:
环境变量:环境变量看似 “隐形”,但不管是日常使用 Linux 命令,还是编写需要依赖外部配置的程序,都离不开它~ 搞懂它,你就能解决 “命令找不到”“依赖库加载失败” 等常见问题,还能更灵活地控制进程运行环境!(´。• ᵕ •。) ♡♡(>ᴗ•)
环境变量:是操作系统或程序运行时用来存储配置信息和系统路径的一些键值对
“全局设置”或“公共告示板”,所有程序都可以查看和使用它们环境变量是操作系统为进程间传递配置信息而设计的一种动态参数机制,全局生效且可灵活修改。
环境变量的核心特点:
当前终端或后续启动的进程都能读取到 PATH 变量,所有程序都能通过它找到可执行文件的位置修改、新增或删除环境变量,修改后对 “新启动的进程” 立即生效(已运行的进程需重新加载才会识别)“变量名=变量值” 的格式 USER=张三,HOME=/home/zhangsan “键” 是变量名(区分大小写,通常大写)“值” 是具体的配置内容想象
一家公司(操作系统)里有一个公共布告栏:
COMPANY_NAME)和内容(变量值,如:"Awesome Tech Inc.")公司的任何员工(任何一个程序或用户)都可以走到布告栏前,查看这些通知来获取公共信息行政部(系统)贴的,全员适用。有些是部门经理(用户)贴的,只在本部门生效在这个比喻中:
STANDARD_FILE_FORMAT 这个通知。”LOG_PATH 就知道了。”环境变量的核心思想就是: 提供一种统一、中心化的方式来管理配置,避免将配置信息硬编码在成千上万个程序中
通过上面对环境变量的学习,小伙伴们应该对 “什么是环境变量” 还是不懂吧? 什么?(○´・д・)ノ 你说已经完全搞懂了?那鼠鼠认为你呢,要不是之前学过,要么就是天才啊! 因为刚才讲的内容,其实和很多教材、还有部分老师上课的风格很像 —— 知识点和定义的简单罗列。 这种方式虽然能快速覆盖内容,但有个明显的问题:如果知识点之间不能形成关联,就很难真正融会贯通;而且那些精炼的定义,只有两种人能明白是什么意思:第一种是已经学过的人,第二种鼠鼠愿称为天才! 对于刚开始学习的小伙伴来说,理解这些抽象概念需要一个 “循序渐进” 的过程。就像鼠鼠接下来要讲的内容,标题可能会起得有点古怪,但其实这是一个从“main函数参数”到“环境变量”,循序渐进学习的过程。
疑问:在 C语言 和 C++ 中,
main函数是程序的入口点,很多人熟悉没有参数形式的main函数,即int main(),但是main函数真的没有参数吗? 我们不妨换个角度思考:main函数虽然是程序的入口点,但它并非在程序运行时 “自主启动” 的
main函数本质上也是被其他代码调用的函数(比如:C 语言运行时库中的_start函数,会在准备好程序运行环境后调用main) 事实上,main函数确实支持带参数的形式,其中最常用的就是int main(int argc, char *argv[])这种定义
1. argc
argc是argument count的缩写,中文意思是参数个数,它是一个整数类型变量,用于记录命令行中输入的参数数量, 包括程序名本身test的可执行文件 ,在命令行中输入./test hello world./test是程序名,hello和world是额外输入的参数,此时argc的值为 32. argv
argv是argument 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(程序名)、a、b、c这 4 个部分argv数组:argv[0]指向./code,argv[1]指向a,argv[2]指向b,argv[3]指向cargc(参数个数)的值就是 4—— 正好对应argv数组中有效元素的数量 需要特别注意的是,argv数组在存储完所有有效参数后,末尾必须以NULL(空指针)结尾。
这个NULL不计入argc的计数,它的作用是给程序一个 “结束标记”—— 当遍历argv时,遇到NULL就知道已经处理完所有参数了,避免数组越界访问。
现在小伙伴们可以回想一下:我们在命令行输入的那串完整命令(比如:
./code.exe -a -b),究竟是被谁分割成一个个参数的呢? 答案是:Shell 程序(比如:我们常用的 bash、zsh 等)
argv数组./program)和各个参数(如:-a、-b)依次存入数组中之后,当 Shell 启动新进程运行我们的程序时,会把构建好的argv数组传递给进程。进程内部就通过这张argv表,获取到所有命令行参数,从而实现类似-a、-b这样的选项功能
main函数带参数的作用:

main函数的参数可以方便地从外部传递数据。ls、grep等,都是利用main函数的参数来实现丰富功能的。 grep -r "keyword" /path/to/search -r:是grep命令的递归搜索参数"keyword":是要搜索的关键词/path/to/search:是搜索路径看了上面的演示,大家应该能发现:
ls -a、ls -b这类系统命令,和我们自己写的./code.exe -a、./code.exe -b,在 “靠命令行参数控制程序功能” 的逻辑上其实完全一样 —— 本质都是程序接收外部传的参数,再根据参数做对应的事。
不过有些小伙伴可能会疑惑:“ls 直接敲就能用,根本不用加 ./,但我们的程序必须写 ./code.exe 才能运行,这俩真的一样吗?”
其实从核心来说,系统自带的命令(比如:ls)和我们自己编的程序(比如:code.exe)没任何区别 —— 它们都是能被系统执行的二进制文件。
区别就区别在,一个生在”罗马“,一个生在”深山“。
啊,运行程序的”身世“这么重要吗?——🙁yes,因为要运行一个程序就得先让系统找到文件在哪儿
具体来说,当你在终端输一个指令(比如:ls 或 code.exe)时,系统找程序的逻辑很固定:
./code.exe 里的 ./ 是 “当前目录” 的路径(去“深山”的路径)/bin/ls 里的 /bin/ 就是“固定目录”的路径(去“罗马”的路径)ls、cd 这种大家常用的命令(出生在“罗马”的孩子) ls 或 code.exe这就解释了为什么两者用起来不一样:
/bin 或 /usr/bin),所以你直接输 ls,系统去这些目录里一找就着,不用你手动写路径test 文件夹)里的,这个 “当前目录” 不在系统提前设定的 “固定常用目录” 里 code.exe,系统去那些常用目录里找一圈,肯定找不到./(代表 “当前目录”),就是明确告诉系统:“程序在我现在打开的这个目录里,去这儿找”,系统就能精准找到并运行了简单说:
ls 特殊,而是它的存放位置在系统 “默认会搜的目录” 里(而是因为它出生在“罗马”)
有些小伙伴可能会好奇,
/usr/bin这类目录为何如此 “特别”,系统为什么会专门去这些目录里查找可执行程序呢? 进一步追问的话:系统是怎么知道,当我们输入一个可执行程序的名字时,要到/usr/bin这样的路径下去寻找呢?
其实答案很简单,就是环境变量啦,更准确地说,是环境变量中的 PATH 变量在 “指引” 系统。
PATH 变量里存储了一系列目录的路径,系统会按照 PATH 里的顺序,依次到这些目录中去查找我们输入的可执行程序,看看能不能找到对应的文件。
相信通过上面关于环境变量的介绍,大家对环境变量已经有了比较清晰的认识。 这时候可能有小伙伴会顺着思路想:
PATH 变量记录的路径中。PATH 变量里,系统不就能像找 ls 那样,通过 PATH 找到我的程序了吗?./ 了吧?”(系统很娇贵,像是深山老林这种穷乡僻壤它是不可能来的,所以那就让我们用双手将深山老林,打造成金山银山吧!)
没错,这个思路完全正确。既然 “添加环境变量” 的需求已经明确,那具体该怎么操作呢? 嗯,这个嘛不急不急,我们先看到环境变量之后,再提添加环境变量吧!
查看所有环境变量的两种不同的方式: 1. printenv 命令
printenv2. env 命令
printenv 功能基本一致,默认也是打印所有环境变量,额外优势是可以通过参数 “临时设置环境变量并执行命令”env
实际学习中,我们很少需要一次性看所有环境变量,更多是查看某个特定变量(比如:核心的
PATH变量),Linux 中最直接的方式是用echo 命令搭配环境变量的 “引用规则”: 核心语法:echo $环境变量名
$ 符号,告诉系统 “这是一个变量,需要解析它的值”PATH 是 Linux 中最核心的环境变量,决定了系统去哪里找可执行命令,查看它的命令: : 分隔,系统会按这个顺序依次搜索命令ls 时,系统会先在 /usr/local/sbin 找,找不到再去 /usr/local/bin,直到找到 /bin/ls 并执行)

如果不小心把环境变量
PATH搞丢了,会导致ls、cd、pwd这些系统基础指令全都无法执行(因为系统找不到这些指令的存放路径了),这时候不用慌,因为重启 bash 进程就可以复原。 要理解为什么重启 bash能复原,首先要明确一个关键点:
malloc 申请了一块空间,专门用来存储 PATH 对应的路径字符串PATH 配置,或者重启一个新的 bash 进程,就能恢复正常

当我们登录系统时,系统会自动为我们创建一个 bash 进程(命令行解释器) bash 启动后,首先要做的就是从系统的配置文件(如:
/etc/profile、~/.bashrc等)中读取环境变量信息,然后在自己的内存空间里构建一张 环境变量表 这张环境变量表的本质是一个指针数组,和我们之前讲的 命令行参数表(argv)在数据结构上完全一致 —— 都是用指针按顺序指向不同的内容,只是存储的信息不同而已。
PATH=/usr/bin、HOME=/home/user)NULL 指针结束 当我们在命令行输入 ls -a 这样的指令时,整个过程是这样的:
ls -a 这个字符串会被当前的 bash 进程捕获,而不是直接被后续执行的 ls 进程拿到bash 会先解析这个字符串,构建出命令行参数表(即 argv 数组):argv[0] = "ls",argv[1] = "-a",末尾以 NULL 结束bash 需要找到 ls 这个命令的可执行文件位置 PATH 变量的值(比如 PATH=/usr/bin:/bin)PATH 中的每个路径与命令名拼接(比如 /usr/bin/ls、/bin/ls),逐个检查文件是否存在ls 的可执行文件后,bash 会创建一个新的子进程,并在子进程中执行 ls 程序,同时将构建好的命令行参数表传递给子进程PATH 的所有路径中都找不到对应的命令,bash 就会报出 “命令不存在” 的错误(如:command not found: ls)从内存角度看:
bash 进程启动时,会在自己的内存空间中分配一块区域,为每个环境变量单独申请存储空间,最终形成一个二维数组结构(指针数组指向多个字符串)env 或 printenv 命令时,本质上就是让 bash 打印出这张环境变量表中的所有内容总结来说,在 bash 进程内部,始终维护着两张核心的表:
ls -a 解析后的结果)PATH、HOME 等) 这两张表共同支撑了命令行交互的核心逻辑 —— 从解析用户输入,到找到并执行对应程序,背后都是这两张表在发挥作用。




export:主要用于将指定的变量设置为环境变量,使得该变量不仅在当前 Shell 进程中有效,还能被其创建的子进程继承。 这样,在子进程中也能访问到这个变量的值。常见的使用场景包括:
语法:
MY_VAR="hello",再执行export MY_VAR,就能让MY_VAR变为环境变量APP_CONFIG_DIR 的环境变量,指定其值为 /etc/myapp,可以这样写:export APP_CONFIG_DIR=/etc/myappexport 命令,会列出当前 Shell 进程中所有通过 export 设置的环境变量及其值示例: 1. 自定义环境变量示例:

MY_NAME 变量MY_NAME 的值,这体现了环境变量的继承性2. 修改 PATH 变量示例:

unset:用于删除已经定义的普通变量或环境变量
unset 删除了某个变量,无论是普通变量还是环境变量,后续的脚本或命令将无法再访问到该变量的值,它占用的内存空间也会被释放语法:unset 变量名

main函数的三个参数解析:
int argc:命令行参数的数量(argument count),至少为1(程序自身路径)char *argv[]:命令行参数数组(argument vector),每个元素是指向参数字符串的指针char *env[]:环境变量数组(environment vector),每个元素是指向环境变量字符串的指针#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:是一个用于获取环境变量值的标准库函数。
<stdlib.h> 头文件中通过这个函数,程序可以方便地获取系统或用户设置的各种环境变量信息,以便根据这些信息做出不同的行为。
getenv 函数的原型为:char *getenv(const char *name);
const char * 类型的参数 name,表示要获取值的环境变量名NULL
environ:是一个指向字符指针数组的外部变量,用于存储当前进程的环境变量environ指向的是一个字符指针数组,数组中的每个元素都是一个指向以空字符'\0'结尾的字符串的指针,这些字符串的格式通常是变量名=变量值
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 、"HOME=/home/user" 等


鼠鼠的建议: 不过通常情况下,不太推荐直接获取所有环境变量的方式:
main 函数的 env 参数遍历所有环境变量environ 变量遍历所有环境变量因为在实际的开发场景中,我们往往只需要获取某一个具体的环境变量的值,而不是一次性获取所有环境变量。
接下来我们可以设计一个 “仅自己能执行” 的程序,要实现这个需求,得先理解 Linux 中进程与环境变量的核心逻辑:
bash(或其他 Shell)bash 会作为父进程,创建一个子进程来运行程序,并且会将自身的环境变量传递给这个子进程而环境变量之所以需要被子进程继承,核心目的就是:
具体怎么实现呢?核心思路是利用 bash 传递给子进程的 “用户身份相关环境变量”(比如:USER 或 UID),让程序启动时先校验这个变量的值:
#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 环境变量,让程序自动查找依赖的工具路径本质上:环境变量的继承机制,就是为了让子进程能 “无缝衔接” 父进程的环境配置,从而实现更灵活、更贴合用户需求的程序逻辑。
本地变量:是指作用范围被限制在特定 “局部环境” 内的变量。
在 bash 等 Shell 环境中,本地变量的 “作用域” 主要与 Shell进程/子进程、函数 绑定,是最常接触的本地变量场景。
定义方式:直接通过 “变量名 = 值” 的格式定义,不需要 export 命令(export 会将变量升级为环境变量,能被子进程继承)

本地变量核心特性是
作用域受限,Shell 本地变量的作用域主要有两个限制:
bash 打开新 Shell、运行脚本),子进程无法访问父进程的本地变量


好麻烦啊,为什么要搞一个什么本地变量? 因为它解决了 “全局变量污染、资源浪费、协作困难” 等核心问题,是所有编程范式(包括 Shell 脚本)中
“管理变量状态”的最优解之一。
“变量污染”,本地变量通过 “作用域隔离”,从根源上解决这个问题简单来说:不需要全局使用的变量,就应该用本地变量 —— 这是写出可靠、高效代码的基本准则
我们知道,在 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 中启动的子进程(比如:执行 python、gcc,或是新打开一个 bash 子 Shell)都能继承到这个变量。
之前我们可能遇到过这样的情况:如果不小心弄丢了关键的环境变量(比如:误操作清空了 PATH),会发现:
ls、gcc 这些命令突然用不了了PATH 变量存储的是系统查找可执行文件的路径,没了 PATH,bash 不知道该去哪里找这些外部命令的可执行文件pwd、echo 这些命令却还能正常使用 简单总结一下:
export 之所以能修改当前 Shell 的环境变量,核心是因为它是 bash 的内建命令,无需创建子进程就能直接操作父进程(当前 bash)的环境变量列表pwd 这类内建命令,之所以在环境变量丢失时仍能使用,也是因为它们无需依赖外部文件,是 bash 自身携带的 “基础功能” 这也从侧面体现了内建命令在 Shell 中的特殊作用 —— 它们是 bash 与用户交互、管理自身状态的核心工具,而非独立的外部程序。