公司动态
《从零入门Linux系统篇(二十二):进程篇·六——环境变量揭秘:从命令行参数到进程环境继承》
Hello大家好。今天这篇文章我们由浅入深拆一块Linux系统级编程里的核心基石环境变量。我们会从main函数里那个隐藏的命令行参数切入一步步往下挖环境变量在底层到底是怎么组织的它的指针数组结构长什么样以及获取环境变量的三大核心手段分别是什么。整个脉络从表到里从参数到本质。好话不多说我们正式开始。目录零、概念补充命令行参数——程序启动时的数据入口0.1 重新认识main函数——程序入口背后的参数传递0.1.1 main函数并不是程序真正的起点0.1.2 main函数为什么也可以接收参数0.2 命令行参数的组织形式——argc、argv与参数表结构0.3 命令行参数的实际应用演示一、初识环境变量——程序运行背后的隐藏配置1.1 环境变量的基本概念1.1.1 什么是环境变量1.1.2 Linux中常见的环境变量1.1.3 如何查看当前系统环境变量1.1.4 环境变量与本地变量的区别及使用1.1.4.1 核心概念对比——变量作用域与生命周期1.1.4.2 环境变量相关操作命令1.2 main 函数的第三个参数——环境变量入口1.3 为什么执行自己的程序需要加./二、环境变量的底层结构与存储机制2.1 环境变量表的组织形式2.2 环境变量的修改机制——内存级修改与磁盘级保存2.3 Windows系统中的环境变量管理三、获取环境变量的核心方法3.1 方法一通过main函数第三个参数获取环境变量3.2 方法二通过getenv接口获取指定环境变量3.3 方法三通过environ全局指针遍历环境变量表3.4 方法四子进程继承父进程环境变量3.4.1 环境变量继承的核心原理3.4.2 示例代码分析四、深入理解环境变量的实际应用4.1 Linux中常见环境变量解析4.1.1 USER与LOGNAME 的区别4.1.2 历史命令记录与HISTSIZE4.2 环境变量与程序控制、权限管理4.2.1 环境变量控制的核心逻辑与安全问题4.2.2 示例代码分析4.2.3 基于自定义环境变量实现程序控制4.2.3.1 示例代码零、概念补充命令行参数——程序启动时的数据入口0.1 重新认识main函数——程序入口背后的参数传递0.1.1 main函数并不是程序真正的起点如果你的老师曾经信誓旦旦地说“main函数是程序的开始”别怪他那是对程序员而言的。从你写代码的视角看程序确实从main起步但把视角切到系统底层故事完全不一样。我们直接看汇编代码就明白了。编译出来的程序真正的入口函数根本不是main而是_start。在Linux上是_start在Windows上则叫_crt_start。这个入口函数才是程序被加载进内存后CPU执行的第一条指令所在的地方。_start内部的核心逻辑简化一下大概长这样_start { int ret 0; int arg_count 0; arg_count 3; // 这个值是通过扫描 main 函数签名动态确定的 if (arg_count 0) ret main(); else if (arg_count 2) ret main(argc, argv); else ret main(argc, argv, env); }有没有发现什么_start会先扫描你写的那个main函数通过词法分析和语法分析搞清楚你的main到底定义了几个参数。然后根据参数个数的不同选择不同的调用方式无参的调无参版带两个参数的调main(argc, argv)带三个参数的调main(argc, argv, env)。所以main从来不是程序真正的起点。它是_start在完成一系列准备之后回头来“请”你登台的。真正的幕后推手是那个你几乎从没注意过的_start。0.1.2 main函数为什么也可以接收参数初学C/C的时候我们写的main几乎都是光秃秃的一个参数都不带。但到了Linux系统级编程这层你才会发现真正的main其实可以大大方方地接收参数而且这些参数还是操作系统在你程序启动时自动递过来的。最常见的声明长这样int main(int argc, char *argv[]);两个参数各司其职argc一个整型记录命令行参数的个数。argv一个字符指针数组存着每一个具体的命令行参数字符串。你敲的那一长串命令早在程序跑起来之前就被操作系统拆好、装进这两个参数里恭恭敬敬地交给了main。还有一种写法最好也混个脸熟int main(int argc, char *argv[], char *env[]) { (void)argc; // 调用一下变量消除“未使用”警告 (void)argv; (void)env; }(void)argc这行很多人第一次见会愣一下。它存在的意义很单纯当你声明了参数却暂时没用到时编译器会好心地提醒你“这变量你设置了怎么不用啊”甚至报出警告。加一行(void)变量名就是明确告诉编译器“我知道它在那儿我故意不用别唠叨了。”这种小技巧在系统编程里很常见写代码时顺手就能用上。0.2 命令行参数的组织形式——argc、argv与参数表结构当你在终端里敲下一串命令比如ls -a -b -c然后按下回车第一个接触这串字符的不是你的程序而是终端解释器bash。它会先把这一整行输入原封不动地拿到手然后以空格为刀把这个长字符串切成一个个独立的子字符串。切好之后这些字符串被一张一张填进一张表里。拿./code a b c为例系统在底层构建出的指针数组结构是这样的char *argv[]: --- ---------------- | 0 | --| ./code | --- ---------------- | 1 | --| a | --- ---------------- | 2 | --| b | --- ---------------- | 3 | --| c | --- ---------------- | 4 | --| NULL | --- ----------------三个参数a、b、c加上程序路径./code 本身总共四个有效字符串。但注意数组末尾bash在构建这张表时会强制追加一个NULL指针作为整张表的结束哨兵。所以argc的值是 4而数组的实际长度是5。这个NULL尾巴不是多余的装饰品。它让任何遍历这张表的代码都能在不知道argc的情况下安全地一路走到尽头碰到NULL就知道该停了。这种“结尾置空”的设计在系统级编程里几乎处处可见后面我们看环境变量的时候还会跟它再次碰面。0.3 命令行参数的实际应用演示为什么要费这么大劲把命令行参数一路递给main函数根本目的就一个让同一个程序能靠不同的参数干不同的活。你日常敲的ls -a、ls -l本质上都是把-a、-l这些选项传给ls程序再由它解析参数表走向不同的功能分支。下面这段C代码把这个过程拆得明明白白。#include iostream #include string using namespace std; // 演示如何遍历打印所有命令行参数 void Test01(int _argc, char* _argv[]) { for (int i 0; i _argc; i) { cout _argv[i] endl; } } // 演示不同功能是怎么通过命令行参数选项实现的 void Test02(int _argc, char* _argv[]) { if (_argc ! 2) { cout 输入错误!请重新输入 endl; } else { string cmd _argv[1]; if (cmd a) cout 执行一号功能 endl; if (cmd b) cout 执行二号功能 endl; if (cmd c) cout 执行三号功能 endl; } } int main(int argc, char *argv[]) { // Test01(argc, argv); // 想遍历参数就把这行打开 Test02(argc, argv); return 0; }这段代码里有两个关键点。第一./code本身也是参数。它稳稳躺在_argv[0]上是这张参数表里的头号成员。很多人第一次写参数解析逻辑时会忘掉这个“第0个参数”的存在直接从_argv[1]开始取业务参数这段代码里_argv[1]才是用户真正传入的选项。第二程序的“多面手”能力全靠判断_argv[1]的字符串内容来实现。传a走一号功能传b走二号功能传c走三号功能。同一个可执行文件配上不同的参数就能表现出完全不同的行为。这就是命令行参数的全部意义一份代码多个入口切换自如。你每天在终端里用的那些自带各种选项的命令内部逻辑跟这段演示代码如出一辙。一、初识环境变量——程序运行背后的隐藏配置1.1 环境变量的基本概念1.1.1 什么是环境变量环境变量说白了就是操作系统里用来描述“运行环境”的一堆参数。它们通常以名称内容的形式成对出现比如PATH/usr/bin、HOME/home/user。举个最直观的例子我们在编译C/C代码时从来没手动告诉过链接器“动态库和静态库放在哪个目录”但链接照样能成功程序照样能跑。你以为编译器是靠自己聪明才智找到库的别天真了。真正在背后指路的就是那些环境变量。它们帮编译器圈定了搜索范围让链接器不费吹灰之力就能锁定目标。环境变量的用途其实远不止找库这么简单。它们往往肩负着某些特殊使命配置系统行为、记录用户偏好、传递进程间信息都是它们的主场。而且环境变量在系统中天然具有全局特性一旦设置这个进程和它往下的所有子进程都能读到像空气一样弥漫在整个运行环境里。后面你会越来越频繁地感受到它的存在感。1.1.2 Linux中常见的环境变量环境变量队伍庞大但刚入门时先认识几个最常见的就够用了PATH指定命令的搜索路径。你敲ls、gcc这些命令时系统就是顺着PATH里记录的目录挨个去找可执行文件的。没有它每条命令都得写全路径。HOME指定当前用户的主工作目录。你登录Linux后终端一开始待的地方就是HOME指向的那个家。SHELL记录当前正在使用的Shell值通常是/bin/bash。1.1.3 如何查看当前系统环境变量查看环境变量的方式有好几种各有各的主场命令作用echo $VAR快速精准地查看某一个环境变量的值。比如echo $PATH直接输出PATH的内容。env列出所有环境变量。内容通常很长一般配grep过滤使用比如env | grep PATH。set查看所有本地变量范围比环境变量更广还包括一些Shell内部变量。export不带参数时也能查看所有环境变量列表。1.1.4 环境变量与本地变量的区别及使用在Linux Shell里变量主要分两大阵营本地变量和环境变量。很多初学者搞不清它们的边界其实关键就一条作用域不同。1.1.4.1 核心概念对比——变量作用域与生命周期变量类型作用域特点本地变量仅在当前Shell进程内有效子进程无法继承出了这个Shell就没人认得它。环境变量当前Shell及它派生的所有子进程全局有效常用于传递系统级配置。本地变量主要是给Shell脚本内部使用的。Shell脚本本身是个大话题我们这里不展开讲但既然提到了就简单带两句让你知道它长什么样。Shell脚本的本质就是用文件装下一堆命令。文件开头写上#!/bin/bash之后每一行都是一条Shell命令。执行这个脚本就等于把这些命令按顺序跑一遍#!/bin/bash touch file mv file myfile i100 echo $i这段脚本干了四件事创建一个文件、给它改名、定义一个变量i并赋值为100、把i的值打印出来。脚本里的i100定义的就是一个本地变量只在脚本运行期间有效脚本一结束这个变量就蒸发了。理解本地变量和环境变量的区别是掌握Shell编程的一块地基。简单记本地变量是“自己家里的事”环境变量是“家业传下去的事”。一个关起门来用一个要被子孙后代继承。后面我们讲export的时候你会更深刻地感受到这两者之间那层微妙的转换关系。1.1.4.2 环境变量相关操作命令变量这玩意儿光知道概念不够得会动手操作。三个命令覆盖了变量的生、升、灭。① 定义本地变量语法简单到只有一个等号变量名值。但有个致命细节等号两边绝对不能有空格。i10是对的i 10就是一场灾难Shell会把i当成一条命令去执行然后报个莫名其妙的错。如果值里面本身带空格用双引号把整个值包起来就行。my_varhello world i10② export把本地变量提拔为环境变量想让一个本地变量被它的子子孙孙继承就得给它办个“升级手续”。export就是那个升级通道。两种写法效果一样先定义后导出或者定义时一步到位。var1ubuntu export var1 # 或者直接 export var2centos③ unset删除变量变量不用了别让它赖在环境里占地方。unset能清理任何变量不管它是本地籍还是环境籍。unset var2 # 验证一下输出为空就说明删干净了 echo $var21.2 main 函数的第三个参数——环境变量入口你以为main函数只有两个参数别急着下结论。它的完整形态其实可以塞下第三个参数int main(int argc, char *argv[], char *env[]);第三个参数char *env[]就是环境变量表。它和命令行参数表是同一副骨架同样是一个字符指针数组每个元素指向一个形如NAMEvalue 的环境变量字符串末尾同样挂着一个NULL 指针当结束哨兵。两张表结构一模一样像一对双胞胎。这时候我们可以先抛出一个重要结论一个进程内部藏着两张表。一张是命令行参数表argv记录着它是怎么被启动的另一张是环境变量表env记录着它运行时所处的环境。两张表都是一个进程从娘胎里带出来的家当。那这两张家当是谁给的答案是父进程。当父进程fork出子进程时会把自己手里的这两张表一并传下去。所以子进程一出生就天然继承了父进程的全部命令行参数信息和环境配置这也正是环境变量“全局特性”在进程层面的真正来源。1.3 为什么执行自己的程序需要加./想在Linux里跑一个可执行程序核心前提其实就一条系统必须先找到它。当你敲下ls并回车时系统为什么能瞬间响应因为ls这个二进制文件规规矩矩地待在/usr/bin/目录下那是系统放核心指令的地方。换句话说它住在一个“系统点名册”上登记过的位置。那问题来了凭什么偏偏是/usr/bin/而不是别处答案就藏在一个叫PATH的环境变量里。这个全局变量专门用来记录系统搜索可执行程序的默认路径集合。我们随时可以把它打印出来看看[abcVM-0-9-opencloudos ~]$ echo $PATH /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbinPATH里挂着一串用冒号隔开的目录。当你输入一个命令时命令行解释器bash就会顺着这串路径从左往右一个一个目录地翻找跟你输入的同名二进制文件。翻到了就执行翻遍了也没有就报command not found。顺带说一句这个“找命令”的活正是bash干的。它不仅是你的输入翻译官还兼着搜索引擎的职责。现在回到我们的问题。为什么自己编译出来的程序不加./就跑不起来原因一目了然你的程序所在的那个目录压根不在PATH里。系统翻遍了它认识的所有地方就是不知道你的程序藏在哪。那怎么办用./显式地告诉系统“别翻你的点名册了就直接在我当前所在的这个路径下找。”./代表当前目录./code的意思就是“当前目录下的code”路径给得明明白白系统自然就找到了。所以./不是多余的仪式感它是在给系统指一条它原本不知道的路。什么时候你的程序也搬进了 /usr/bin/你也能像敲ls 一样光凭名字就把它叫出来。二、环境变量的底层结构与存储机制上面我们反复提到“环境变量表”你可能到现在还觉得雾里看花。别急这一节我们就集中火力把这张表的真面目扒个干净。2.1 环境变量表的组织形式当你登录Linux系统时系统会为你启动一个bash进程。如果有10个用户同时登录内核里就有10个互不相干的bash进程每个进程都揣着自己的一套环境变量表。这张表正是我们前面说的那个字符指针数组。它长这个样子char *env[]: --- ------------------------------------ | 0 | --| XDG_SESSION_ID14400 | --- ------------------------------------ | 1 | --| HOSTNAMEbite-alicloud | --- ------------------------------------ | 2 | --| TERMxterm | --- ------------------------------------ | 3 | --| SHELL/bin/bash | --- ------------------------------------ |...| --| ... | --- ------------------------------------ | n | --| PATH/usr/local/bin:/usr/bin:... | --- ------------------------------------ |n1| --| NULL | --- ------------------------------------每一个数组元素都是一个指向字符串的指针。字符串的格式清一色是NAMEvalue等号左边是变量名右边是它的值。这张表从下标0一路排到n内容五花八门有会话ID有主机名有终端类型有Shell路径有命令搜索路径……最末尾照例是一个NULL指针压阵。这个NULL尾巴可不是摆设它给遍历者提供了一个天然的刹车点。你写代码遍历这张表的时候不用先数清楚里面有多少个变量只要判断当前元素是不是NULL就能知道什么时候该停下来。这种设计跟命令行参数表的套路一模一样干净又省心。2.2 环境变量的修改机制——内存级修改与磁盘级保存环境变量能不能改当然能。但这玩意儿改起来一不留神就把自己带沟里。比如你在终端里随手敲一句[abcVM-0-9-opencloudos ~]$ PATH # 强行把 PATH 清空 [abcVM-0-9-opencloudos ~]$ ls -bash: ls: No such file or directory完蛋ls直接罢工了。为什么因为你这一手是覆盖式修改把PATH原来的值一股脑清空了。系统顺着空空如也的PATH去找命令自然什么都找不到。这时候再执行别的命令会发现大面积失能只有少数内建命令还能勉强喘气内建命令后面讲。当然也有温和一点的做法——追加式修改[abcbite-alicloud lesson15]$ PATH$PATH:/home/abc/code/code/merge_class/lesson15 [abcbite-alicloud lesson15]$ echo $PATH /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/abc/.local/bin:/home/abc/bin:/home/abc/code/code/merge_class/lesson15这次没把原来的值抹掉而是在后面“接”了一段新路径。原来的命令照常用新增的目录也被纳入了搜索范围。但注意上面这两种改法都是内存级的。只要你关掉这个终端重新连一下之前所有修改瞬间蒸发因为你的改动只存在于当前这个bash进程的内存里从来没落到磁盘上。那问题就来了如果我想永久性地增加一个环境变量或者永久修改 PATH该怎么搞先说结论不建议随便改。改坏了轻则命令失效重则登录不进系统。但为了把底层原理吃透我们还是要聊聊配置文件。每次你重新登录Linuxbash进程被拉起来的那一瞬间它会跑到你的家目录~底下去读几个隐藏的脚本文件老老实实执行一遍然后在内存里动态构建出那张初始的环境变量表。bash本身也是个进程这张表就是它内部维护的那一套。最核心的两个文件是~/.bash_profile~/.bashrc这还不算完。~/.bashrc还会接着去调用系统级的/etc/bashrc。这些文件本质上就是Shell脚本里面早就写好了默认的环境变量导出指令比如export PATH...。它们一环扣一环.bash_profile调.bashrc.bashrc再调系统的bashrc逐层展开最终拼出你看到的那张完整环境变量表。所以每次登录时你看到的环境变量都是从这些磁盘上的脚本里“现读现建”的。如果你在这些文件里追加一条export再重新登录一次改动才会真正持久化。这就是内存级与磁盘级修改的本质区别前者是临时易逝的后者是刻进配置里的。2.3 Windows系统中的环境变量管理三、获取环境变量的核心方法3.1 方法一通过main函数第三个参数获取环境变量第一种方法用的就是前面提到的main函数完全体。直接遍历那个以NULL结尾的环境变量指针数组把每一项原原本本打印出来#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; }循环条件写得很清楚碰到NULL就收手。一行一行吐出来的就是当前进程完整的NAMEvalue环境变量表。不过说句实在话现实中几乎没人这么写。这种方法又原始又死板只能无差别地遍历整张表想按名字精准揪出某个变量还得自己额外写字符串比对逻辑。所以它更多是作为一种教学手段让你直观地看见“环境变量表原来长这样”。真正工程里我们有更体面的拿法接着往下看。3.2 方法二通过getenv接口获取指定环境变量方法一虽然能一网打尽整张环境变量表但你要的往往只是里面某一个特定的值比如当前用户名是谁、家目录在哪。每次都把整张表扫一遍未免太粗暴了。这时候就该C标准库提供的getenv接口登场了#include stdio.h #include stdlib.h int main() { char *user getenv(USER); if (user ! NULL) printf(Current user is: %s\n, user); return 0; }getenv的用法干净利落传进去一个变量名它还你一个指向对应值的字符串指针。想拿哪个就拿哪个不用自己手写遍历逻辑也不用关心那张NULL结尾的表到底怎么组织。底层那些查找工作库函数全替你包了。而且它自带一层安全性如果那个变量不存在getenv会返回NULL你只需要像上面那样加个判空就能避免空指针崩溃。定向、精准、省心这是工程里用得最频繁的获取方式。3.3 方法三通过environ全局指针遍历环境变量表既不想改main的参数列表又想完整遍历一遍环境变量表怎么办C语言早就给你留了后门一个叫environ的全局指针。它声明在unistd.h里但使用前需要手动做一次extern声明把它的身份亮出来#include stdio.h extern char **environ; int main() { for (int i 0; environ[i] ! NULL; i) printf(%s\n, environ[i]); return 0; }为什么这个全局指针能拿到整张表关键在于它的类型environ是一个二级指针也就是char**。它并不直接保存字符串内容而是保存了环境变量表首元素的地址。于是environ就相当于一张表的“抓手”。你拿着它就可以像遍历数组一样从下标0开始一路往后走直到撞上NULL收尾。跟方法一殊途同归但不依赖main函数的参数声明用起来更灵活。当你在某个函数深处突然想访问环境变量时不用费劲把env参数一层层传下来直接掏出environ就能用。这也是它在很多源码里频频现身的原因。3.4 方法四子进程继承父进程环境变量前三种方法都是在“当前进程”这个框框里打转研究怎么把环境变量读出来。而方法四把视角拔高了一层它关注的是环境变量从哪来、怎么传。在Linux系统里环境变量天生带着全局属性。这种“全局性”不是靠什么魔法维持的而是靠一条非常朴素的机制实现的子进程默认会继承父进程的环境变量表。抓住两个核心结论环境变量具有全局属性。一个bash里设置的环境变量从这个bash派生的所有子进程都能拿到。环境变量可以被继承。不只是父进程原有的变量就连后来“倒给”它的新变量也会一并传给子进程。那“全局”到底怎么理解看一张图就全明白了。所有的进程往上追根溯源最终都会追到那个登录时启动的bash进程。bash手里攥着那张完整的环境变量表它每fork一个子进程就把这张表原封不动地拷贝一份传下去。子进程再传给它的子进程代代相传。于是整棵进程树上任意一个节点都能拿到“祖爷爷”bash当初定下的那套环境配置。所以“全局”不是指某个变量存在一个公共的空间里让所有进程去共享而是通过父子链继承把同一套表一代一代传递下去。理解了这一点你才算真正摸到了环境变量传递机制的根。3.4.1 环境变量继承的核心原理当我们用fork()创建子进程时子进程会复制父进程的绝大部分数据环境变量表自然也在其中。注意这里触发的正是那个老朋友写时拷贝子进程刚被创建出来的瞬间它的environ指针指向的内存空间跟父进程是完全一致的。也就是说父子俩在出生那一刻看的是同一份环境变量表一字不差。3.4.2 示例代码分析下面这段代码让父进程先设置一个自定义环境变量再fork出子进程。子进程什么都不用做直接就能读到这个变量#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main() { // 1. 父进程在自己的空间内设置一个自定义环境变量 // setenv 参数键, 值, 是否覆盖1 表示覆盖 setenv(MY_TEST_VAR, Hello_Linux_Process, 1); printf([父进程] 成功设置环境变量 MY_TEST_VAR\n); // 2. 创建子进程 pid_t id fork(); if (id 0) { perror(fork failed); return 1; } else if (id 0) { // 3. 子进程逻辑 printf([子进程] 开始尝试获取父进程留下的遗产...\n); // 子进程直接调用 getenv 获取父进程在 fork 前设置的变量 char *val getenv(MY_TEST_VAR); if (val ! NULL) { printf([子进程] 成功继承! 获取到的值为: %s\n, val); } else { printf([子进程] 未找到该环境变量。\n); } exit(0); } else { // 父进程等待子进程退出 wait(NULL); } return 0; }父进程用setenv把MY_TEST_VAR塞进自己的环境变量表然后fork。子进程一出生连招呼都不用打直接getenv(MY_TEST_VAR)就能把父进程设好的值原原本本读出来。这就是环境变量继承的完整实证一张表父传子代代相传全局属性由此而来。四、深入理解环境变量的实际应用4.1 Linux中常见环境变量解析下面把当前系统里能见到的环境变量基本都列了出来。其中加粗的几个是日常打交道最频繁、最该先混个脸熟的“常用款”。环境变量名变量值 / 含义说明SHELL/bin/bash —— 当前使用的 Shell 解析器。HISTCONTROLignoredups —— 忽略连续重复的命令历史记录。HISTSIZE1000 —— 历史命令保存的最大条数。HOSTNAMEVM-0-9-opencloudos —— 主机名。PWD/home/zbc —— 当前工作目录随cd切换实时变化。LOGNAMEabc —— 登录的用户名。XDG_SESSION_TYPEtty —— 当前会话类型为 TTY 终端。MOTD_SHOWNpam —— 已通过 PAM 模块展示过每日欢迎信息。HOME/home/abc —— 当前用户的家目录。LANGen_US.UTF-8 —— 系统的语言与编码环境。LS_COLORSrs0:di01;34:ln01;36:... —— 定义各类型文件的显示颜色长度过长省略。SSH_CONNECTIONSSH连接的客户端IP、端口及服务端IP、端口。XDG_SESSION_CLASSuser —— 会话类别为普通用户。TERMxterm —— 终端仿真器类型。LESSOPEN|/usr/bin/lesspipe.sh %s —— 配合 less 命令做文件预处理。USERabc —— 当前用户名。GOPROXYhttps://mirrors.tencent.com/go,direct —— Go语言的腾讯云代理源设置。SHLVL1 —— Shell 的嵌套层级。XDG_SESSION_ID15098 —— 当前 XDG 会话 ID。XDG_RUNTIME_DIR/run/user/1001 —— 当前用户运行时的临时数据目录。SSH_CLIENTSSH客户端的IP、端口和服务器端口。which_declaredeclare -f —— 内部变量定义which命令的函数方式。PATH/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin —— 系统查找可执行命令的路径集合。DBUS_SESSION_BUS_ADDRESSunix:path/run/user/1001/bus —— D-Bus系统会话总线地址。MAIL/var/spool/mail/zbc —— 当前用户的系统邮件信箱路径。SSH_TTY/dev/pts/0 —— 当前SSH会话绑定的虚拟终端设备。BASH_FUNC_which%%导出的 Bash 自定义 which 函数。_/usr/bin/env —— 上一个执行的命令或当前拉起进程的工具路径。OLDPWD/home/student/project —— 上一次所在的工作目录支撑cd - 快速回跳。4.1.1 USER与LOGNAME 的区别用最省事的方式讲清它俩的差别USER当前实际生效的用户是谁。你su成谁它就是谁。LOGNAME你最初登录系统时用的那个账号是谁。大多数时候这两个值长得一模一样你几乎感觉不到它们的存在。但有一种情况会让它俩分道扬镳当你用su命令切换了用户身份之后。切换完USER会立刻变成新用户而LOGNAME还死守着最初登录时的那个名字。一个管“现在是谁”一个管“一开始是谁”审计登录记录、排查用户切换问题时这个区别就派上用场了。4.1.2 历史命令记录与HISTSIZEbash这个“管家”有个很贴心的习惯你敲过的每一条命令它都会默默记在小本子上。等你想翻旧账的时候用下面几种方式就能把历史命令揪出来。Ctrl R反向搜索。敲一两个关键字它立刻帮你翻出最近一条匹配的历史命令再按一次接着往前翻。!v快捷执行。感叹号后面跟一个字符直接重跑最近一条以该字符开头的命令。比如 !v 就执行最近的vim xxx。上下方向键最原始也最顺手按一下回退一条一条条翻。history一次性列出当前保存的所有历史命令。history | wc -l数一数当前系统里到底存了多少条历史命令。这个条数上限就是环境变量HISTSIZE在管着默认通常1000。下面我一条一条演示给你看4.2 环境变量与程序控制、权限管理在Linux环境编程里环境变量可不只是用来查路径、找库的。它还有一个很实用的进阶玩法权限绑定。说白了就是通过读取进程的环境变量或者拿捏住进程的用户身份标识比如USER、LOGNAME、UID来判断“当前是谁在跑这个程序”然后据此决定开放哪些功能、拦截哪些操作。比如一个后台服务只允许特定用户启动或者一个工具检测到USERroot才放开危险操作。环境变量就这样悄悄变成了权限控制的第一道闸门。4.2.1 环境变量控制的核心逻辑与安全问题写这类程序光靠一句getenv(USER) 来判断身份是远远不够安全的。原因有两层环境变量可以伪造。任何用户尤其是有权限的root随手敲一句export USER你的用户名就能临时把环境变量改了。你代码里那个简单的字符串比对瞬间就被绕过去了。UID 才是硬通货。Linux里真正绑死身份的是UID用户 ID。root的UID永远为 0这个数字改不了。通过getuid()系统调用拿到的才是进程背后那个真实用户的ID。所以最稳的方案是双管齐下既要核对手里的USER环境变量又要把root的UID挡在门外或者直接限定成你自己的专属UID。两道门一起把安全性才立得住。4.2.2 示例代码分析下面是一段完整的C示例。用之前把代码里的your_username换成你自己的Linux用户名。#include iostream #include cstdlib // getenv 在这里 #include unistd.h // getuid 在这里 #include string int main() { // 1. 设定唯一有资格跑这个程序的用户名 const std::string AUTHORIZED_USER your_username; // 2. 抓取环境变量里的 USER const char* env_user std::getenv(USER); std::string current_user env_user ? env_user : ; // 3. 获取进程真正归属的 UID uid_t current_uid getuid(); // 4. 核心安全检查 // - 环境变量 USER 必须对得上号 // - 当前 UID 不能是 0root 永远别想混进来 if (current_user AUTHORIZED_USER current_uid ! 0) { // 验证通过放行 std::cout [验证通过] 欢迎回来 AUTHORIZED_USER std::endl; std::cout 执行核心绝密逻辑... 正确答案是42 std::endl; } else { // 其他人包括 root或者环境变量被动手脚了一律挡下 std::cout [错误] 权限不足该程序已被锁定仅限指定用户执行。 std::endl; // 这里可以故意给点误导信息或者干脆静默退出 return 1; } return 0; }4.2.3 基于自定义环境变量实现程序控制除了系统预置的那些环境变量我们其实还能自己造变量然后用它来指挥程序的行为。这招很实用比如把环境变量当成一个简易的软件激活凭证或者干脆当个Debug开关用。4.2.3.1 示例代码说到底这就是利用进程间的数据传递来控制子进程的执行逻辑。直接看代码#include iostream #include cstdlib // getenv 在这里 #include string int main() { // 1. 读取自定义的 FLAG 环境变量 const char* env_flag std::getenv(FLAG); std::string flag_val env_flag ? env_flag : ; // 2. 控制逻辑FLAG 必须精确等于 1 才放行 if (flag_val 1) { std::cout [SUCCESS] FLAG 验证成功程序开始正常运行 std::endl; // 这里放你的核心业务逻辑... } else { std::cout [ERROR] 缺失关键环境变量或参数不正确程序终止。 std::endl; return 1; } return 0; }用法很简单跑程序之前先export FLAG1程序就能进正常分支不设这个变量或者值不对程序直接撂挑子退出。一个环境变量就成了程序的门禁卡。实际场景里这种自定义变量常被拿来干这些事区分开发环境和生产环境、控制是否打印调试日志、甚至做一个粗糙但有效的授权校验。比起改代码重新编译改一个环境变量就能切换程序行为这种体外控制的思路在运维和调试时出奇地好用。结尾从main函数身后那个隐形的_start到环境变量表末尾那枚安静的NULL指针从PATH为什么逼着你加./到子进程如何继承父进程的整张家当这一路下来环境变量的里里外外我们已经翻了个底朝天。几个核心认知值得在关掉页面前再刻一遍main 不是程序的起点_start才是。_start会先扫描你的main签名再决定怎么调用它。你写了几参数就给你塞几个参数。命令行参数表和环境变量表都是在这时候一并交到main手上的。一个进程两张表末尾都是NULL。命令行参数表记着“怎么来的”环境变量表记着“活在什么环境里”。两张表都靠NULL压阵遍历时碰到NULL就收手这是系统级编程里最常见的设计语言。环境变量的全局性不是共享是继承。没有哪个公共空间存着所有进程共用的环境变量。真相是bash建好表fork时整张传给子进程子进程再传给它的子进程。代代相传才有了“全局”的假象。查环境变量getenv是首选遍历用environ。main第三个参数教学意义大于实用价值真正工程里精准取值用getenv全面遍历用extern char **environ。环境变量能控制程序行为也能当门禁。但别全信getenv(USER)环境变量可伪造UID才硬核。核心场景下双保险才靠谱。环境变量是进程与系统环境之间的一扇窗。窗外的风景我们已经看清了下一篇我们继续往进程的内存深处走进程虚拟地址空间。代码段、数据段、堆和栈到底怎么排布页表又是怎么把虚拟地址翻译成物理地址的这些问题下一篇一并拿下。如果这篇文章对你有帮助欢迎点赞、收藏、关注三连支持。你的每一个正反馈都是我继续肝下一篇的最大动力。我们下篇见。