我们在学校学习某些编程语言比如Java、python,一开始在配置环境的时候基本上都会做一件事情就是配置环境变量。
那我们当时往往都是按照老师的指导或者跟着网上的一些教程直接就把它配置了,但是,我们可能并不明白配置这个环境变量到底是干啥的,它到底有什么作用?
那这篇文章,我们就来谈一谈环境变量到底是个什么东西?
首先我们可以来看一下环境变量的概念:
1. 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
2. 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
单凭这段文字,大家肯定还不能理解到底什么是环境变量,那下面我们通过几个问题来帮助大家理解
我们之前在Linux上写过C程序,并且我们知道如何编译链接让它生成可执行程序,然后运行它。
比如:
我写了这样一个test.c
并且写好了Makefile。
然后我执行make命令就生成了对应的可执行文件
然后我运行一下
没什么问题。
但是呢,我们想问大家一个问题:为什么我们运行这样的可执行文件要加上./
呢?
那我问大家,我们写的.C的文件生成的可执行程序,我们可以像指令那样去执行它来完成相应的任务。
那这个可执行程序可以叫做指令嘛?
🆗,我记得在刚开始我们学习Linux基本命令的时候就有说过:
指令其实也是文件,可执行文件
我们可以file查看一下我们平时常用的命令比如——ls:
我们看到它其实就是一个可执行(executable)程序啊
那我们这里自己生成的可执行程序myproc呢?
我们看到它们其实就是一样的,没什么区别。
只不过人家写的ls这些指令被纳入了Linux的基本指令里面,而我们写的只能自己玩玩。但他们本质上都是可执行程序。
但是呢,问题就来了:为什么我们运行ls这些指令可以直接敲对应的指令直接执行,而我们自己生成的可执行程序运行要加./
呢?
为什么呢?
如果我想让我们自己的可执行程序也可以不加./
直接运行,能做到吗?如何实现呢?
./
我们知道它是啥东西,.
代表当前目录嘛,/
是路径分隔符嘛。
那我们这里的可执行文件myproc就是当前目录下的一个文件
所以,执行我们自己的可执行程序好像必须要定位到它所在的路径。
那上面./
的定位方式其实是相对路径,那用绝对路径是不是也可以执行这个可执行文件?
这当然也是可以的。
但是它为什么就不能像ls哪些基本命令那样无需指明路径直接执行呢?
那原因呢其实就在于像ls这些基本指令,系统中原本就存在与之相关的环境变量。我们执行这些指令的时候,系统会自动根据环境变量去相对应的路径下查找这些指令,能够找到就可以直接执行,而无需指明完整路径。
所以,执行一条指令的前提是得先找到它。
那么Linux中就存在这样一个环境变量——PATH:
PATH :用于指定命令的搜索路径
我们可以先查看一下它
echo $NAME
//NAME:环境变量名称
那么上面说了这么多:为什么ls哪些基本直接无需指明路径就可以直接执行,而我们自己的可执行程序不可以呢?
🆗,那原因就在于我们在命令行执行ls这些基本指令的时候,系统会自动去环境变量PATH中指定的这些路径里面查找,如果能找到对应的指令,就可以执行,无需我们自己指明具体路径。
而我们自己的可执行程序
所在的路径是没有包含在PATH环境变量定义的路径里面的。
所以运行的时候我们必须自己指明路径,如果没有指明,那么系统就找不到该命令,无法去执行
那这里我们提到的PATH其实就是一种环境变量:
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作/家目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash
那到这里,大家再去回看最开始环境变量的概念以及后面跟的例子:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
相信大家可能就有一点感觉了。
那如果我想让我们自己的可执行程序像ls这些基本命令那般可以直接执行而无需指明路径,应该怎么实现呢?
那经过上面的学习,我们知道为啥我们自己的可执行程序不能直接运行而需要指明路径啊?不就是因为我们自己的可执行程序没有在PATH环境变量定义的路径里面嘛。
那所以呢?
如果我们想要我们自己的可执行程序可以像ls这些基本命令那样直接执行,是不是把我们自己的可执行程序所在的路径添加到PATH环境变量里面就行了。
那如何添加呢?
export PATH=$PATH:要添加的路径
(修改PATH环境变量添加一个新的路径)
export可新增,修改或删除环境变量
我们来试一下:
我们把myproc的路径添加到PATH里面
那现在我们再来执行我们自己的可执行程序myproc
我们发现就可以直接执行而无需再指明路径了。
另外需要注意的是,如果这样写的话:
这样就不是添加而是修改PATH里面的路径了,这样修改后PATH里面就只有这一条路径了。
那你的ls这些基本命令可能就无法执行了。
但是如果这样做了也没关系,关闭你的Xshell重新登录就恢复了。
当然同样的,我们添加之后,后面重新登录的话,它其实也会恢复。
那除此之外,大家想一下还有没有其它的方法可以使我们自己的可执行程序能够不带路径直接执行呢?
🆗,我们如果把我们自己的可执行程序放在PATH环境变量里面已包含的路径里面,按理说应该也可以啊。
我们试一下:
我新打开一个渠道
现在执行myproc是不行的。
那我们现在把myproc拷贝到PATH里面已有的路径比如usr/bin/
下面
拷贝好了。
那我现在把当前目录下面的这个myproc删掉
然后我现在像执行ls那样直接执行myproc
是可以的,并且我们which命令也可以查到。
🆗,那像我们上面这样:
在Linux中,把可执行程序拷贝到系统环境变量默认路径下,让我们可以直接访问的方式——其实就相当于Linux下软件的安装。
那如果我不想要它了,把他从对应路径里面删掉:
就用不了了,那这其实就相当于卸载。
我们在Linux上用不同用户登录的时候,系统中也会有对应的环境变量来记录当前登录的用户是谁
这个环境变量呢就叫做——USER
我们可以来查看一下它
我当前登录的用户是yhq,那我查看这个环境变量就是yhq
如果我切换成root
查看到就是root
然后大家再来思考一个问题:
前面我们讲文件权限的时候,我们要判断一个用户对某个文件是否拥有某些权限的时候,一般首先我们要定位一下该用户的角色,它是这个文件拥有者,还是所属组,或者other。
那么系统是如何知道当前用户是什么角色呢?
那就是因为有环境变量的存在。
当你登录的时候,环境变量就记录了你是哪个用户
所以当我们访问某个文件的时候,那系统就可以拿着你当前的用户名和文件的拥有者、所属组或other进行对比,从而就能判断出来你是什么角色,然后拿对应的权限去套你,以此来确定你对某个文件拥有哪些权限。
所以这种情况也是依靠环境变量来处理的。
那如果我想查看我这个用户当前系统上所有的环境变量都有哪些,要如何查看呢?
这里用到的命令叫做——
env
:列出所有环境变量及其赋值
它就列出了当前系统上我这个用户所有的环境变量,这些环境变量基本上都是我们登录的时候就设定好的。
比如:
挺多的,大家感兴趣可以自己查询了解一下。
上面我们提到过一个环境变量——HOME
HOME : 指定用户的主工作/家目录(即用户登陆到Linux系统中时,默认的目录)
那当前我是普通用户,我们查看HOME环境变量的话
显示的值就是我的家目录
那如果我切换成root
那我们再查看就是root的家目录。
所以对于同一个环境变量,如果对应的用户不同,那它的值可能就是不一样的。
上面我们提到,系统启动的时候,就已经存在大量的环境变量,那如果我们想获取到这些环境变量要怎么做呢?
那我们其实是可以自己写一个程序来获取的
那首先来问大家一个问题:大家之前肯定都写过C/C++的程序,那我想问大家的是main函数可以带参数吗?如果可以的话最多可以带几个呢?
我们平时自己写C/C++代码一般写的main函数都是无参的
但是呢相信大家可能会在网上或者一些书籍上见过带参数的main函数,比如这样的
最常见的就是这种两个参数的,如果这两个参数你不知道是啥,没关系,我们后面也会给大家介绍。
但是呢,main函数其实是可以有3个参数的:
那我们先来讨论一下这个第三个参数——envp
,通常我们把它叫做环境变量表。
我们再来看第三个参数,大家说char* envp[]
这是个啥啊?
🆗,这不是一个字符指针数组嘛。
每个元素都是一个char*的指针,那这些指针都指向什么东西呢?
我们学过C语言,对于一个字符指针来说,它指向的内容无非就两种:
- 指向一个字符(即存储一个字符变量的地址)
- 指向一个字符串(即存储的是一个字符串的首字符地址)
那在这里我明确的告诉大家它指向的就是一个字符串,并且:
char* envp[]
这个字符数组的最后一个元素里面一定存的是NULL
,当然其实不一定总是最后一个元素,应该说第一个无效元素存的是NULL。
比如该数组大小为10,只有前5个元素都指向字符串,那么它第六个元素就指向NULL。
那它指向的字符串是什么呢?
不知道没关系,那我们可以遍历
envp
数组打印出来看一下
我们来运行一下看看这个表里面存的到底是啥?
大家看看,这打印出来的是啥啊!
这不是跟我们之前用env
命令列出的环境变量一样嘛,只不过我们在前面加了下标这些信息。
其实不用打印我们也能猜出来,我们说了它是环境变量表嘛,所以它里面放的就是一个一个的环境变量以及它们对应的值组成的字符串。
那么除了上面的方法,我们还可以通过一个全局变量来获取环境变量:
这个全局变量叫做——
environ
我们可以来查看一下
我们看到,它的类型是char**
,是一个二级指针类型
那char**
的话其实跟上面的char* envp[]
不是一样嘛!
因为
char* envp[]
是在main函数的参数列表里面,那传参的话传一个数组传过去的真正是啥,是不是数组首元素地址啊。
char* envp[]
首元素是char*
,那首元素的地址不就是char**
的二级指针嘛。
所以同样的,environ
也指向环境变量表。
那我们就来用environ打印一下环境变量表:
那其实跟上面用
char* envp[]
的方式是一样的
我们来运行一下
效果是一样的。
libc(Linux下的ANSI C的函数库)中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
那么由此,我们也引出——环境变量的组织方式
每个程序都会收到一张环境变量表,环境变量表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串(即环境变量名及其值组成的字符串)
上面呢我们已经介绍了两种通过代码获取环境变量的方式,但是:
我们以后如果要获取某个环境变量比如PATH的时候,难道要像上面那样遍历指针数组(环境变量表),再通过字符串匹配去一个个找吗?
可以是可以,但是好像有点麻烦,那有没有更简便的方法呢?
🆗,当然是有的,除了上面介绍的两种方式,我们还可以通过函数(系统调用)直接获取指定的环境变量。
那我们下面就来学习一下:
首先,用来获取特定系统变量的函数——
getenv
那它是如何使用的呢?
我们可以打开man手册来看一下:
man getenv
需要包含的头文件是stlib.h
该函数的返回值是char*
,一个参数const char *name
所以我们调用时传入指定的环境变量名,就可以获取到该环境变量
如果没有匹配到
返回NULL
那我们可以来写个代码试一下:
比如我们获取一下环境变量USER
我们来运行一下
🆗,就成功获取到了我们当前系统上环境变量USER 的值。
那其他的也是一样,我们调用getenv传对应的环境变量名称就行了。
那讲到这里,我们再回过头来理解一下到底什么是环境变量:
那在上面的学习中,我们了解了环境变量的概念,也进行了一些实操。
比如我们把自己写的可执行程序的路径添加到了PATH环境变量中,使得我们运行自己的可执行程序时可以像基本命令ls那样无需指定完整路径,直接可以运行。
但是呢?我们同时也注意到一个问题,就是我们如果重新登录的话,环境变量PATH里面的路径就被重置了,我们再去直接运行我们的可执行程序就不行了,除非你再次添加。
那这说明了什么呢?
上面我们获取了环境变量,我们知道它其实就是一张表嘛。表里面包含了所有的环境变量以及它们对应的值(键值对集合)。
那上面我们提到的现象又说明了什么呢?
🆗,它说明了环境变量其实本质是一张内存级的表,在用户登录的时候,就会由系统去特定用户形成属于自己的环境变量表。
而表中的每一个环境变量,都有自己特定的应用场景,比如有的是指定命令搜索路径的,有的是进行身份验证的等等。
表中的每一个环境变量都是KV的键值对形式。
那再来思考一个问题:
我们说环境变量是一张内存级的表,用户登录时由系统形成。那么表中的数据都是从哪来的呢?
🆗,表中的环境变量信息呢其实都是从系统的相关配置文件中读取进来的。
那这些配置文件又在哪里呢?
我们进入用户的家目录,在家目录下面呢我们能找到这样两个文件
它们其实是两个shell脚本。
那打开的话其实我们现在也看不太懂
但是它们里面其实就是对环境变量进行设置啥的,这样一些操作。
当我们每次登录成功的时候,系统会重新读取配置文件,把这些配置文件中的脚本执行,然后就自动形成对应的环境变量,加载到内存中。
上面我们提到环境变量其实是一张内存级的表:
那这张表其实是在shell内部由shell来维护的,那我们知道Linux上的shell一般是bash,我们在命令行启动的所有程序通常都是bash的子进程。
然后呢在上面我们学过一个命令——export:
export可以新增,修改或删除环境变量。
上面我们使用export对PATH环境变量进行了修改。
那我现在想新增一个环境变量,怎么做呢?
export VARIABLE_NAME="value"
其中,VARIABLE_NAME是要新增的环境变量的名称,value是环境变量的值
我们来试一下
我新增了一个环境变量为hello="youcanseeme"
那我们知道所有的环境变量都在环境变量表里面存放,那我们在环境变量表里面是不是能查看到我们新增的这个环境变量呢?
那我们使用env
命令列出所有环境变量及其赋值
那在显示出来的环境变量表中我们就看到了刚才我们自己添加进行的环境变量表。
上面我们说到:
环境变量表是在bash中由bash维护的,所以我们执行
export VARIABLE_NAME="value"
命令之后。
那bash就会把这个环境变量及其赋值作为一个字符串添加到环境变量表的指针数组中。
然后呢想告诉大家的是:
经过之前的学习我们知道命令行启动的程序都是bash的子进程,而环境变量——环境变量通常具有全局属性,可以被子进程继承下去
那我们能不能证明一下呢,你说环境变量可以被子进程继承,那就真的是这样吗?
那我们来写这样一个程序:
我们刚才不是新增了一个环境变量嘛,那我们现在来获取一下它,如果获取到了,打印一下。
来试一下
我们成功获取到了刚才新增的那个环境变量。
那这能证明什么呢?
这个结果不就证明了环境变量被子进程继承下来了嘛。
因为我们在命令行启动的这个程序是bash的子进程啊,而我们上面新增的子进程是在bash里面新增的,而现在子进程获取到了它,那也就证明环境变量被子进程继承了下来。
我们来做一个实验:
我们知道
export VARIABLE_NAME="value"
可以新增环境变量到环境变量表里面。
那如果我不加export呢?
比如:
另外我们发现这样的话这个hello1也可以像环境变量那样打印
那它也可以通过子进程获取到嘛,我们来试一下:
现在我们用getenv来获取hello1
我们来运行一下
我们发现啥也没有,那就是没有获取到,返回的是NULL
那所以呢,带export和不带export有什么区别呢?
我们知道,带export的话,它就会把后面的变量及其赋值当作环境变量导入环境变量表中,我们在环境变量表中可以查到,并且它可以被子进程继承。
那不带export呢?
通过上面的实验我们知道它不会被子进程继承,那就说明它不是环境变量,但是我们可以像查看环境变量那样查看到它,那就说明它也被bash记录下来了,但是它没有被添加到环境变量表中,成为环境变量。
所以,不带export的这种变量我们把它叫做shell的本地变量或者叫普通变量,它就不具有全局属性,而是局部有效,只在shell内部有效。
但是如果我后续就是想让这个hello1这个普通变量能够被子进程继承怎么办呢?
那也很简单,你就再用export把它导进去环境变量表就行了
然后,我们看到就可以了。
但是,这里好像还有一点问题值得我们思考:
这样不加export他就是一个普通变量,也会被bash记录下来。
我们也可以用echo $变量名
打印它的值,只是它没有被添加到环境变量表里面,子进程不会继承,获取不了。
但是,你不觉得奇怪吗?
我们执行echo $hello2
,这也是一个指令啊,那按理说他也是bash的子进程啊,但是本地变量不是不会被子进程继承吗?可是为什么这里我们能够打印出来呢?
🆗,那这个问题我们先放一放,后面再说,大家有兴趣可以自己先去研究思考一下。
那么以上就是环境变量的全部内容…
不过还有一个东西也值得我们来说一下:
上面我们提到过:
main函数呢其实是可以有3个参数的,前面我们重点介绍了第三个参数
char* envp[]
。
那么下面,我们来介绍一下前两个命令行参数int argc, char* argv[]
:
它们叫做命令行参数
那它们有什么用呢?
首先呢,这个argv[],我们看到它的类型和我们上面提到的环境变量表的类型是一模一样的,是一个char*
的指针数组。
argv[]
也是一张表,只不过内容肯定和环境变量表是不一样的。
那么第一个参数argc
又是啥呢?
🆗,他其实就是argv这个指针数组的大小。
那他里面到底放的是啥呢?我们可以直接遍历这个数组打印看一下:
我们来运行几次,大家看一下现象:
大家看看,现在是否对命令行参数这几个字有一点感觉了?
🆗,我们在调用这个程序的时候,在命令行输入的这些内容
在shell看来,就是一个字符串,那么按照空格将其分割成子串
它们分别对应:
那说到参数选项,相信大家应该不陌生,我们之前学习基本命令的时候,很多命令后面都可以跟对应的选项
那此时我们再来讨论argv这个表里面存的是什么:
那其实存的就是我们在命令行输入的字符串以空格分隔出来的一个个子串
bash通过命令行输入的字符串生成了这张表,我们子进程里面可以用这个表。
如果我们每次把argc也都打印出来的话:
再来运行几次
我们发现argc就是子串的个数,也是指针数组argv的大小。
🆗,那这就是命令行参数,那它有什么用呢?
那下面我们来做一个实验:
我们来尝试写这样一个程序:
就是你调用我这个程序的时候,必须带选项,如果你第一次调用不知道的话,没有带选项,就打印提示;然后根据提示,你带不同的选项,就会打印不同的语句代表完成不同的任务。(就像我们的基本命令后面跟不同选项一样)
所以,我来写这样一个代码:
我们来运行一下看看效果
那大家看,这就是命令行参数的意义。
现在我们再来想我们之前学习基本命令的时候,为什么我们跟不同的选项,就对应不同的功能,那其实就是通过命令行参数来实现的。