Linux进程——环境变量
文章目录
环境变量
本篇文章,我们将来介绍关于环境变量的内容。
环境变量的概念
首先,我们先直接给出环境变量的相关概念:
环境变量(environment variables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
这些概念,我们现在来看当然是看不懂的,但是我们先放在这里。我们先不对概念进行理解。等到后面将环境变量的相关知识讲完了我们再回过头来看这些概念。
其实环境变量,就是一些运行系统进程的时候所需要的内容,一旦系统内有了这些环境变量,哪怕我们运行一些需要库或者需要特定内容的进程的时候,就不需要进行手动操作,只需要让系统去环境变量里面找到即可。
就好比我们在下载python的时候,并不是单纯的下载软件,下载完后还需要配置一些环境变量,Windows下其实也是有环境变量的:

我们会发现,在我们的Windows系统下,也是存在着很多环境变量的。正是有了这些环境变量,我们在运行某些进程的时候就不需要手动的在代码中添加了。
接下来,我们将一起来讲一下环境变量究竟是什么。
命令行参数(补充)
本部分将补充命令行参数的相关内容。一方面为后续获取环境变量做好铺垫,同时也会详细讲解指令的选项参数等关键要素。
命令行参数的使用和解释
首先我们来思考一个问题,即c/c++中的main函数有参数吗?
其实main函数的参数是可以有,也可以没有的。我们通常写的代码大部分情况下main函数都是不带参数的,但其实如果有仔细了解过main函数的形式的话,会发现是有参数的:
int main(int argc, char* argv[], char* env[]);
我们也可能看过有的人写代码的时候会带上这三个参数。这里还需要注意的是:
这三个参数可以按照语法标准来减少,但是不能随意减少参数:
int main(int argc, char* argv[]);//合法
int main(int argc);//不合法
我们之前普遍的认为,main函数是程序的入口处。而且我们在调试代码的时候,确实是从main函数作为入口进行调试的。
但是我们会思考一个问题,就是main函数本身也是一个函数,它也有返回值,它的返回值是返回给谁的呢?
其实这里就要打破我们以往的认知,其实main函数并不是一个程序的入口处。在Linux下,其实是由一个叫做_start的函数去调用main函数的,但是在Windows下是mainCRTStartup函数。这些函数负责调用main函数。
在打破了我们以往的“main函数为一个程序的入口处的“认知后,我们现在就很很好奇,这些个参数是用来干什么的?怎么用?
我们先讲解前面两个参数,即int argc 、 char* agrv[],最后一个env数组我们等到后面正式讲解环境变量的时候再来详细讲解。而且main函数的参数允许只有前两个。
先进行使用,然后再来讲解:
//这里用一个代码来进行测试:CmdLineArg.c
#include<stdio.h>
#include<unistd.h>
int main(int argc, char* argv[]){
for(int i = 0; i < argc; ++i){
printf("argv[%d] : %s\n", i, argv[i]);
}
return 0;
}
我们来看一个运行结果:

使用两条指令进行测试:
./CMD_LINE_ARG
./CMD_LINE_ARG -a -b -c
我们会发现,最终我们在命令行中输入的字符串(指令),会被空格符进行分割。分割出来的字符串数量就是argc的数量,argv这个数组从下标位置0开始存放被按顺序分割的字符串。
而且我们这里还需要注意的是:
argv数组的最后一个位置(没有存放字符串)必然是NULL。
我们看我们输入的第二个测试指令,其实是有一点似曾相识的感觉的:

这不是和我们使用带选项的指令是一样的吗?我们常用的一些指令,如ls:

我们使用ldd指令查询ls指令以来的夫,我们会发现很多是libc开头的库,我们介绍过动静态库,我们知道这是关于c语言的库。其实ls指令本身也就是个写好的可执行程序而已。那ls内也会有main函数,那么也会分割外界传入的指令选项,然后根据不同的选项进行功能实现。
只不过是现在我们的代码并没有根据不同的选项进行功能分割而已,接下来,我们将写一份代码来进行测试自带选项的指令:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char* argv[]){
if(argc == 2){
if(strcmp("-a", argv[1]) == 0){
printf("This is function 1\n");
}
else if(strcmp("-b", argv[1]) == 0){
printf("This is function 2\n");
}
else if(strcmp("-c", argv[1]) == 0){
printf("This is function 3\n");
}
else{
printf("please use ./CMD_LINE_ARG [-a or -b or -c]\n");
}
}
else{
printf("please use ./CMD_LINE_ARG [-a or -b or -c]\n");
}
return 0;
}
这个代码的功能是:
如果输入在命令行的指令是带有一个选项(argc == 2,第一个必然是进程指令本身),那么就展示对应的功能。其余情况都需要提示用户这个指令的使用方式。

我们会发现,确实是能够这样做。所以我们现在就知道,main函数中前两个参数是用来干什么的了:
argc代表着输入命令和选项被分割出的字符串的个数
argv内是argc个按顺序分割出来的字符串,最后一个是NULL
正是有了这两个参数,我们可以对我们写的代码做功能分区,也就是可以实现指令的选项。
也就是说,我们可以通过main函数的参数实现程序的子功能!
bash进程切分命令
我们现在会有一个问题,即为什么我们在命令行输入的指令,会被按照空格为间隔符来分割呢,这个操作是谁做的呢?
答案是:bash进程做的。
我们知道,每个用户登陆的时候,系统会自动分配给每个用户一个bash进程,这个bash进程是我们用来管理我们自己写的进程代码和数据的父进程。
当我们在命令行终端输入指令和选项时候,如./CMD_LINE_ARG -a -b -c的时候,bash进程会按照空格符进行分割,在bash进程内部形成一张表,如下表所示:
| 序号 | 字符串 |
|---|---|
| 0 | “./CMD_LINE_ARG” |
| 1 | ”-a“ |
| 2 | ”-b“ |
| 3 | “-c” |
这张表其实就是输入指令按照空格符分割出来的字符串,也就是argv表。这个表是可以作为参数被传入到main函数中被使用的,所以这也就是为什么系统可以识别出指令的选项的原因。
有的人可能会问,运行ls -a -n -l 和 ls -aln效果其实是一样的,这是为什么呢。对于bash进程而言,第一步永远都是先按照空格符切分后填入表内。所以,对于ls -aln而言,argv表就是"ls" 和 "-aln"两个字符串。但是,bash会对这个组合进行进一步地语法分析,是有办法可以解析出 "-aln"是由三个独立的选项组合而成的。这点不用担心。当然具体的实现方法这里就不讲解了,感兴趣的可以了解一下。
环境变量的具体讲解
本节将以PATH变量为例讲解环境变量的概念,同时也会介绍其他常见环境变量。
查看环境变量
我们可以使用指令env查看当前登陆时系统中的环境变量,env就是environment的简写:

我们发现,env所打印出来的环境变量,其实都是由固定格式的:
即名字 = 内容。
当然,有时候其实是知道变量名,但是忘记了变量名所对应的环境变量的内容,我们就单纯的想查看某个环境变量,可以使用echo $环境变量名打印出来。
这里一定要注意的$的使用,如果直接把环境变量名放在echo后面,echo会直接把这个变量名当作成字符串打印出来:

加上$就是为了把环境变量的内容提取出来的,我们在Makefile里面见过。
PATH变量
系统中有很多的环境变量,但是,这里我们就以PATH变量作为例子进行讲解。
程序执行——是否带路径(PATH变量的引入)
我们回看命令行参数部分我们自己写的代码,我们会发现运行起来和系统内的一些指令是有一些不一样的:

最大的区别就是:
系统内的指令是不需要使用./的,而我们自己写的是需要带上的。‘
我们曾经讲过,Linux下一切皆文件,所谓的指令,本质就是写好的可执行文件。系统的指令是放在目录usr/bin下的,文件有可执行的权限,所以可以使用指令的路径代替文件:

所以,对于系统来说,其实我们要运行某个可执行文件,首先需要找到它。
之所以系统的指令不需要带上路径使用,是因为它们都存放在了usr/bin下,我们可以理解为安装好了这个软件在系统中。但是我们自己写的并没有,所以我们就猜测,如果把我们自己写的安装到该目录下,是否就可以不用带路径使用了呢?

所谓的安装,其实就是把软件复制到该目录下,这里注意是需要提权的。因为该目录的拥有者和所属组都是root,普通用户无权向目录内写入,需要提权。

然后我们就发现,我们自己写的程序就可以不带路径使用了。
但是,强烈不建议自己写的程序安装到该目录下!因为我们自己写的程序没有经过验证,可能会有很多bug,会污染系统的指令池。
上面我们是知道了,为什么系统的指令可以不带路径,而我们自己的需要。但是还是存在一个问题,为什么/usr/bin目录下的指令不需要带路径系统可以自己找到呢?
不废话,直接给结论:
这是因为系统内的环境变量PATH中存在这个路径。当输入指令的时候(假设不带路径),系统会优先到PATH变量下的目录去找,如果找不到就会提示找不到。反之调用指令。

打印PATH变量的内容,我们可以发现,确实是存在一个路径/usr/bin。而且也不止一个路径,多个路径之间用:进行连接。
上述说明:当运行一个不带路径的指令的时候,系统会优先到环境变量PATH中的几个路径去找,找不到才会报出错误。
环境变量的修改
也就是说,加入想要自己写的程序能够不带路径执行,其实是有两种方案:
1.在环境变量下的任意一个目录内添加自己写的程序,如前面展示的复制到路径/usr/bin
2.修改环境变量PATH中的内容,添加指定路径(程序所在的路径)
第一种方法已展示过,就不再展示了。我们重点来看第二种方法:

我们把PATH变量的内容直接进行修改了,修改成当前程序CMD_LINE_ARG所在的路径,我们会发现,自己写的程序确实是可以不带路径执行了,但是系统的命令很多都用不了了,如ls,clear等。
其实这是因为,我们直接给PATH变量赋值一个路径,其实是覆盖了原来的变量内容的。所以导致当前PATH目录下只剩下刚刚赋值的路径了:

但是,我们会发现,echo也是指令,pwd也是指令,为什么它们还能继续运行呢?
这是因为,系统内部的一些指令其实是 内建命令 !这些指令是不需要到环境变量的路径去查找的。这是很大的区别!这一点我们先放在这,一会儿再回头来看。
我们现在需要把环境变量PATH的内容修改回去,我们可以重新赋值,把原来的路径赋值回去。这是一种方法。
但是,对环境变量的赋值其实是短暂性的!即只在本次登录生效。所以我们可以退出登录试试看,然后在重新打印:

上面的一些登录信息其实是被裁剪过的,所以看起来会有一些奇怪。
如果我们是真的需要正确修改PATH变量以达到不带路径执行自己写的程序目的,我们可以这样修改:

我们只需要PATH=$PATH:自己添加的目录,就可以在原有基础上添加一个新的目录。这样子就可以正常使用了:

不过这个方法是暂时性的,一旦我们重启就失效了。
当然是有办法使用一些指令达到永久生效的,但是目前的知识不足以理解,所以在这里就不进行讲解,我们将采用别的办法来达到该目的。
环境变量配置文件
前面我们证明了,我们直接对PATH修改,是暂时性的。一旦重启就失效了。
这就说明,其实环境变量是从一个地方来的。每次登陆的时候,系统都会从这个地方把环境变量的内容重新导入进来,要不然这无法解释为什么每次重新登录后我们添加到环境变量的内容都消失了。
其实上面的说法是正确地,这背后蕴含着一个问题,环境变量存在哪里的?
答案:环境变量存在系统的配置文件内。
配置文件,其实就是记录的一些必要信息的文件。那么,环境变量的配置文件在哪呢?

存在于家目录下的隐藏文件当中:.bash_profile和.bashrc
我们可以打开来看一下:


这些都是用shell脚本写的一些配置信息。shell脚本的书写在这里就不进行讲解,不是重点。
系统在登陆的时候,会先加载.bash_profile中的内容到系统当中。然后再加载.bashrc,.bashrc又会从/etc/bashrc目录下导入信息。
但不管怎么样,我们可以尝试着修改一下PATH路径:


我们确实会发现,我们成功的把指定牡蛎添加到了PATH环境变量下,所以必然是可以不带路径运行自己写的程序的。
但是要注意的是,我们修改配置文件后,是需要重新启动系统才能生效的,因为要将修改后的配置文件内的信息重新导入。
环境变量的组织方式
前面我们通过PATH引入了环境变量的概念,同时理解了环境变量存放在哪里。但是,系统从配置文件中究竟是如何导入信息的,导入的信息又存放在哪里呢?
其实这点和命令行参数表argv是类似的,也是存放在bash进程中的一张表。用户登陆的时候后,系统会自动分配一个bash进程,这个bash进程内部会存放一个agrv表,即命令行参数分割表。这个前面部分讲解过就不再提及了。同时,针对于环境变量,bash进程内部其实也是有一张表的:

这些环境变量,其实存放在系统内部的时候也都是一些字符串,形式为”名字=内容“。

这张表的大致结果和上图类似,也和argv表类似。
当有n个用户登录系统时,系统会分配n个bash进程个这n个用户,每个用户的家目录下其实都会有配置文件,所以每个用户对应的bash进程就会从对应的配置文件内将环境变量导入到bash进程二段环境变量表内。同时也会存在命令行参数分割表argv,这是针对于用户的命令行输入的进行分割的存放表。
在Windows系统下,其实也是对应的进程内有一个环境变量的表,只不过这张表被图形化界面给封装了后展示了出来。展示出来的本质上也就是一张表。
其他环境变量
使用env指令我们可以将系统中的环境变量表给打印出来,系统中存在着有很多的环境变量,我们可以来了解几个比较常见的环境变量。
1.USER变量
环境变量中存在一个叫做USER的变量,表示的是当前登陆的用户名:

如果我们使用su指令切换为root用户:

我们会发现用户名没有改变。这是因为su只是在普通用户的基础上切换了超级用户root,系统认为这只是进行权限的提升,所以并没有改变用户名。

使用su -就不一样了,su -会重新登录系统,此时会进行切换,因为系统识别到这是需要切换用从而进行重新登陆。
2.HOME变量

其实HOME变量指向的就是家目录。当系统识别到进入目录~的时候,其实就是进入变量HOME指向的目录。
3.SHELL变量

SHELL变量其实指向的是当前系统的shell程序是哪一个。我们都知道,在Linux系统下,shell程序是bash进程。
4.PWD变量
我们知道,pwd指令是可以用来查看当前处在哪一个工作目录的,PWD其实也一样,只不过它是以环境变量的方式存在于环境变量表的:

这个变量会随着当前用户工作路径的变更而变更。当然,每次登陆的时候其实PWD都指向的是家目录,因为登录到的地方默认就是家目录。
5.OLDPWD变量
这个是用来记录上一次出现的目录的:

所以这就是为什么我们可以使用指令cd -进行最近两个访问的工作目录的切换,这是因为PWD和OLDPWD变量分别记录了当前的工作目录和上一次所处的工作目录:

6.HOSTNAME变量

此变量存储当前机器的主机名。在执行命令行输入前,需先打印系统基本信息,其中主机名是通过读取环境变量HOSTNAME获取的。
7.HISTSIZE变量

这个变量是用来规定:使用history指令查看过往使用指令时,一次性打印出的最大历史指令数。也就是说,在当前系统下,我们使用histroy指令,最多只能展示10000条指令:

当然,这个是最大的数据,实际情况可能是不足10000条,也可能第一条不是从序号1开始的。
系统中还存在着许多其它的环境变量,这些就不一一的进行讲解了。感兴趣的读者可以自行前往查看。
获取环境变量的方法
前面我们围绕着环境变量的一些概念、操作、查看、介绍等进行讲解。这个部分,我们将从以下几个方面进行环境变量的获取:
1.指令获取
2.main函数参数获取
3.系统调用
4.全局变量environ
其中,后面三个办法是通过代码获取的。
指令获取
查看环境变量很简单,可以直接使用env指令查看所有变量,这里就不过多解释了。
也可以是使用echo $变量名。
下面我们来介绍几个其它的方法:
比如我们想要设置一个新的环境变量到环境变量表里面,可以使用export KEY=VALUE:

但是要注意的是,VALUE中间不能有空格,否则会被截断。同时,KEY=VALUE的结构中,等号左右两边也不要留出空格,这需要记住。
如果想要删除刚刚的环境变量怎么办呢?
这简单,使用指令unset KEY即可:

删除的时候只需要根据KEY来删除就好了。
当然,这两个操作是暂时性的,不会导入到配置文件中。也就是说,就算我们加入了一百个新的环境变量,一旦重启,那么就会消失。
main函数参数获取
前文说过,main函数的参数其实最多是可以有三个的,但是,对于第三个参数我们并没有进行讲解,只讲解了前面两个参数int argc 和 char* agrv。
针对于第三个参数,是char* env[],从名字来看,我们其实很快就看出端倪了。因为env指令是查看系统的环境变量的。那么我们有理由猜测,env参数其实就是环境变量表。
实际上,确实是这样,调用main函数的时候,如果有env这个参数,那么系统会把环境变量表传给env这个参数。
所以我们来试着用一下:
#include<stdio.h>
#include<unistd.h>
//1.通过main函数参数获取
int main(int argc, char* argv[], char* env[]){
(void)argc;
(void)argv;
for(int i = 0; env[i]; ++i){
printf("env[%d] -> %s\n", i, env[i]);
}
return 0;
}
这里稍微解释一下代码(void)argc; (void)argv;:
这里就是单纯的使用一下两个参数而已。因为不使用参数其实是会爆出警告WARNING的,常用的方法就是使用void强转以下,没有什么别的意图。
这里的循环条件是env[i],这是因为env这张表的最后是一个NULL,所以一旦碰到了就停止循环,我们来看看运行结果:

我们会发现,系统会把环境变量表传给env这个变量。环境变量表中的内容其实都是字符串。所以env表的每个元素的类型是char*,在c语言中,该类型可指向常量字符串。
系统调用获取
系统中其实也存在一些系统调用的接口来获取环境变量的内容:
如接口getenv 和 putenv:


getenv,就是输入一个变量名KEY,如果存在改环境变量,就返回内容的字符串,反之返回一个NULL:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
//2.通过系统调用
int main(int argc, char* argv[]){
(void)argc;
(void)argv;
printf("USER=%s\n", getenv("USER"));
printf("PATH=%s\n", getenv("PATH"));
printf("SHELL=%s\n", getenv("SHELL"));
printf("HELLO=%s\n", getenv("HELLO"));
return 0;
}

至此,我们就可以通过输入指定的变量名来获取环境变量的内容。
对于putenv这个接口,我们先暂时不进行讲解。
environ全局变量获取
在系统中,存在这样一个全局变量:char** environ,这个变量指向环境变量表这个数组,所以是一个二级指针:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
//3.通过全局变量environ
extern char** environ;
int main(){
for(int i = 0; environ[i]; ++i){
printf("environ[i]=%s\n", environ[i]);
}
return 0;
}

运行的效果和直接使用env表是类似的,只不过使用这个全局变量的时候需要使用extren声明一下这个外部的变量,否则会报错。
环境变量获取总结
综上所述,获取环境变量的方法大致就这几种。但是其实更多时候,用的是系统调用的接口来获取,或者是直接通过指令。因为我们大部分情况下是不需要获取整个环境变量表的。
环境变量的特性
本部分,我们需要重点探讨一下关于环境变量的一些特性。
环境变量的全局性
我们知道,bash进程里面有两张表,一个是argv,另一个是环境变量表。这里就出现了问题了,环境变量表是导入到bash进程里面的,为什么我们自己写的代码可以用呢?
其实这就是环境变量的全局性质,也就是说它是可以被继承的,因为我们自己写的代码运行起来的时候,是一个进程,父进程就是bash。当然满足继承关系。
那么,如果是子进程的进程内是否也可以使用呢?
这当然可以,因为我们知道Linux系统下,进程的父子关系是1:n,其实就是一颗进程树。所以一旦bash进程结构到这张环境变量表,那么后续的子进程,子进程的子进程都可以继承到这一张表,就是因为环境变量的全局性。
在这里就不进行代码解释原理了,感兴趣的读者可以自行尝试一下。
后面两个部分,我们将补充一点概念,这是为了后续学习铺垫的。
本地变量和环境变量
我们试着用一个指令:MY=10:

我们发现,是可以把这个变量打印出来的。

但是我们使用env指令却查不到该变量,这是怎么回事?
其实这就是标题所说的,本地变量。查看本地变量需要使用指令set:

我们再往上翻一下,又发现了一些环境变量表里面的内容。
这就说明,set指令打印出来的是环境变量 + 本地变量的内容。bash进程内其实维护了两张变量的表:分别是环境变量表和本地变量表!
二者最大的区别就是:
它们都属于bash进程内部的变量,但是:
环境变量具有全局性!可以被继承。但是本地变量只是bash进程内部自己使用的变量,也就是只能在命令行终端使用,没办法继承到子进程中使用!
内建命令
前文提及过,有一些指令,是不需要去环境变量PATH对应的路径去查找的。
这就只能说明:这些指令是不需要bash进程来创建子进程运行的,这些命令是由bash进程亲自运行的,那么用到的一些变量在哪呢?只能是本地变量。
所以前文为什么要提及本地变量,就是为了引出内建命令这一概念。比如cd指令,export指令,echo指令,pwd指令,这些指令都不需要去PATH查找对应的文件,直接由bash进程进行。所有这就叫做内建命令。
更多推荐
所有评论(0)