目录
1 PATH环境变量:
我们这里以PATH环境变量来引入环境变量的概念,先来看一个事例:
我们用file指令查看文件类型时,会发现Linux系统中的指令和我们自己写的程序都是可执行程序,那为什么我们执行我们自己写的程序时要添加 ./ 路径,而执行系统的可执行程序就不需要 ./ 路径呢?
原因是:我们要执行一个程序(指令)就需要将该程序加载进内存,要将其加载到内存,就要先找到这个程序,./ 表示当前路径,它可以帮我们找到程序。而系统里面的指令,系统会默认去对应的位置帮我们找它们。
结论:我们如何在执行我们自己写的程序时,也不使用 ./ 就可以执行我们写的程序呢?
方法一:将我们自己写的程序拷贝到 /usr/bin/中
命令:sudo cp 可执行程序 /usr/bin/
但是,我们一般不这么做,因为我们自己写的程序没有经过测试,将该程序拷贝到该目录下,相当于把该程序安装到系统里,会污染系统里的指令池 。
方法二:
原理:系统之所以能够找到这些指令,是因为系统里面存在一个环境变量PATH,操作系统启动的时候会在shell的上下文当中定义一个PATH变量,它在系统里是全局有效的,要看到它需要在其前面加$。
命令:echo $ PATH
系统在执行指令(我们自己写的程序其实也是指令)时,它会默认在每个 : 作为间隔的路径里面去搜索对应的指令,如果指令存在就找到并执行它,当搜索完所有上面这些路径后,如果没有找到该指令,就会报错。
所以,我们可以这样做:
解决方法:将我们当前的路径添加到环境变量PATH中。
命令:export PATH=$PATH:当前路径
这样,我们自己写的程序就可以不加 ./ 了。
然后,我们再来了解一些知识:
在Linux系统中,命令行(bash/shell)上是可以定义变量的【我们暂时可以这样理解:因为shell也是一个进程,而且我们曾经也使用过malloc或new,它是在我们程序运行期间申请空间的,所以说,一个程序在运行期间申请空间就意味着:一个进程在运行期间是可以对它的空间进行动态调整的,有了空间就可以保存数据,所以,环境变量本质上就是数据,而且它其实就是一个字符串。它既然是字符串,那么,我们命令行式的一个进程,我们在启动的时候,导入所谓的环境变量的时候,本质上就是把环境变量这样一个字符串放到我们进程的对应空间当中,这个字符串要么全局定义,要么malloc或new把数据拷过来,我们在命令行上去定义也是类似的效果,定义出来的变量也是字符串,所以把它保存起来就可以了】。
在Linux中,环境变量也属于命令行上定义的变量中的一种。但是这些环境变量我们自己又没有定义,那么它们是从哪里来的呢?
我们执行如下命令:
当我们打开上面三个文件时:
我们会发现这三个文件里面都有一些shell脚本,我们不管它具体什么含义,反正就是我们自己在登录Linux系统时,操作系统会默认让我们当前的shell进程执行一次 .bash_profile 文件里面的内容,把环境变量导入到当前的shell当中,环境变量的配置就是通过 .bash_profile 在启动的时候,被加载到了bash当中,所以只要这个文件被成功启动,操作系统就会在内存里为我们维护一个叫做$PATH的变量,它是内存级的,如果被覆盖掉了,我们只需要关闭系统重新登录,bash就会重新去读取配置文件,然后把环境变量导入内存。
因此,我们得到环境变量的概念:
2 环境变量的概念:
我们把变量由操作系统提供,具有全局属性,往往具有特殊功能的变量叫做环境变量(环境变量是操作系统为了满足不同的应用场景(比如查找指令、确认登录用户是谁、确认主机名等)而预先在系统内设置的一大批的全局变量,它具有全局性,从bash之后的所有进程都可以访问它)。
3 查看环境变量的方法:
echo $NAME
NAME指环境变量的名称。
4 和环境变量相关的命令:
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
具体的使用,后面再说。
5 通过系统调用获取或设置环境变量:
- getenv , 获取环境变量(这里讲)
- putenv , 设置环境变量(以后讲)
我们除了可以通过系统指令获取环境变量(echo $…),还可以通过系统调用来获取环境变量。
我们写了如下程序来获取当前登录用户:
Makefile文件:
我们编译运行该程序后:
我们会发现当前登录用户为xc,我们此时使用su -让root重新登录,(由于root重新登录会默认进入自己的家目录中,所以要重新进入原来的目录内)然后再执行一次程序。
注意:这里切换用户要用 su - 而不能用su,su -是让root用户重新登录,也就是说,如果当前用户正在登录(xc),su - 就是让root这个用户重新登录,重新登录之后,它就会以root的身份重新加载很多东西,把root相关的环境变量全部加载进来。su其实就是切换一下身份,对应的当前路径并没有变,里面的环境变量也没有变,自然USER环境变量也不会变,依然是xc。
上述例子告诉我们USER环境变量的最大意义是标识当前的正在使用Linux的用户。
我们这里回到xc用户,再编写一下代码。
在该代码中,不同用户会执行不同的代码。
通过上述代码,我们就可以解释为什么我们在执行一些指令时会报错【Permission denied】(权限被拒绝),原因是因为我们在执行指令时(比如访问某个文件),系统会去获取文件的属性,查看该文件的拥有者和所属组,当要访问时,它会通过USER环境变量确认当前登录用户,然后确认该用户是否在拥有者或所属组里,看他是否具有该权限。
我们上面举的一些例子主要是为了说明:环境变量会在不同的引用场景当中被使用的,我们需要在不同的场景中使用不同的环境变量。
6 相关命令的具体使用:
6.1 使用export命令理解环境变量的全局性:
为了更好的理解什么是环境变量的全局性,我们这里再举一个例子。首先,查看操作系统内拥有的环境变量:
然后,我们自己设置一个环境变量:
我们使用 echo 命令查看我们自己设置的环境变量 myval 会发现这个环境变量已经被创建好了。
但是,我们使用 env 命令查看所有环境变量时,会发现,在里面没有我们自己设置的 myval 环境变量。
这是因为,我们自己创建的这种变量其实是在shell中的本地变量(可以暂时理解为C语言中的局部变量)。那为了更好的理解环境变量的全局性,以及理解这里的本地变量是什么意思。我们这里再举一个例子。
我们在上面已经说了,我们可以获取系统的环境变量(getenv),那么,肯定也可以获取我们自定义的环境变量。
我们使用 getenv 查看是否存在 myval 这样一个环境变量。
编译并运行该程序后:
可以看出该变量还没有成为环境变量。
在这里我们就需要使用 export 指令了,它可以对环境变量做定义,并且,如果一个本地变量已经存在,它也可以把它导成环境变量。
我们此时,会发现本地变量myval变成了环境变量。
这上面的都是现象,下面就是来得出结论了。
当我们在命令行上运行 mycmd 程序时,mycmd 就会变成一个进程,而 bash 也是一个进程(系统进程),mycmd 是 bash 的子进程。而环境变量具有全局属性的根本原因是因为它会被子进程集成下去。那为什么会被继承下去呢?主要是为了不同的应用场景。比如:使用PATH找到指令路径,使用USER进行身份认证等。所以说,本地变量只会在当前进程(bash)内有效,不会被继承。
6.2 set命令:
如果我们定义了一个变量(本地变量),我们不想将它设为环境变量,但是又想查看它怎么办?
这里就需要使用 set 命令了。(set命令会打印很多东西出来,既包括本地变量也包括环境变量以及其它东西)
6.3 unset命令:
unset 可以清除环境变量,但是清除的时候不是立即清除,会等一会儿。
同样地,unset 也可以清除本地变量。
7 PWD环境变量:
我们上面说过,当我们要执行自己的命令(程序)时是需要带路径的,但是我们执行 ls 命令时,后面却不需要带路径,这是为什么呢?
按理来说不应该这样吗?
我们的系统是怎么知道我们当前的文件在哪里呢?可能有些小伙伴会说,它不就在我们的当前路径下吗?但是 ls 怎么知道当前路径在哪里呢? 我们知道找一个文件必须使用绝对路径或相对路径,那么 ls 是如何做到不使用的呢?
那是因为存在一个环境变量PWD,而环境变量是在bash内维护的,bash维护了当前所处的路径,每当路径发生变化时,shell会自动的调整环境变量的值。而当我们运行 ls 指令时,其实是在创建子进程,而环境变量的信息可以被子进程继承下去,被 ls 拿到,ls 就知道自己当前在哪个路径下。所以,显示文件时,我们不带路径,ls 也知道我们当前在哪里。
所以,我们自己也可以实现一个类似pwd的指令,只需要获取到PWD环境变量并将其打印出来即可。然后将该程序拷贝到 /usr/bin 目录下,此时就不需要带路径了。
8 命令行参数:
8.1 定义:
命令行参数是指在命令行界面中与命令一起提供的信息,用于控制命令的行为。这些信息通常以空格分隔,并紧跟在可执行文件名或脚本名之后。
8.2 作用:
我们在这里不具体讲命令行参数到底是什么,我们只管它到底有什么用。
在C/C++中,main函数()内的参数就是命令行参数,我们有的时候看书或写程序时,main函数()内有的时候会出现参数。
通过上面的代码,我们可以看出:main函数包含两个参数,分别为argc与argv,其中argv是char*类型的指针数组,它在这里指的是字符串。argc 指的是这个指针数组的元素个数。我们在这里验证一下这里的指针数组到底是什么。
编写如下代码:
在这里,编译可能会报错:
这是因为我们的 gcc 版本太低了:
我们在这里需要在 gcc 命令后添加 -std=c99 :
此时再编译运行我们写的程序:
出现了如上结果。
如果我们再添加一些选项,此时再编译运行:
我们会发现:当我们命令行输入的内容变多时,我们的指针数组的内容会自动变多。
同样地,我们可以拿 ls 命令做参照:
其中,ls 是程序名,-a , -l 是选项。
所以说,我们所谓的命令行参数,本质是把程序名和选项依次传给argv,一共有几个程序名+选项,就需要定义多大的argv数组, argc的大小也就相应的为多少。
其实,当我们在进行命令行解析时,我们上面输入的内容,整体是一个长字符串,当我们在进行命令行解析时,该字符串会被以空格为单位分成一个个子串。这些操作是由 shell 与 操作系统 共同来做的。
那么,我们传这些命令行参数有什么用呢?
我们编写如下代码:
编译运行后:
我们可以发现,当我们输入不同的命令行参数,我们可以通过不同的选项,在同一个程序内执行不同的功能,这就是命令行参数最大的意义。
通过命令行选项,显示不同的内容:
8.3 获取环境变量的三种方式:
其实,main函数还有一个参数env,它也是指针数组。它指向一个个的环境变量。我们上面也说过,其实,环境变量就是一个个字符串。所以,当我们main被调用时,如果我们带了这三个命令行参数,系统是会为我们传两张表的(命令行参数表argv,环境变量表env),所以,环境变量就可以被进程给拿到,拿到了之后,这个进程就可以使用环境变量了,所以说,环境变量是可以被其它进程拿到的。
为了验证 env 是以NULL作为结尾的,我们编写如下代码。程序结果应为:当遍历到最后一位时,由于是NULL,而NULL又是0,所以,程序会退出循环。
结果如我们所料。
当我们导入自己的环境变量,然后再运行程序:
此时,环境变量myval就被子进程继承了,(因为环境变量是导给shell(bash)的,而环境变量可以被继承,我们的 mycmd 是 bash 的子进程),环境变量是通过命令行参数传给子进程的。
实际上,环境变量也可以不通过命令行参数传给子进程。
比如:直接用getenv来获取环境变量。
或者使用全局的二级指针environ。
char** environ可以理解为系统为我们提供的指针。我们原来的 char* env[] 这个表是一个一级指针数组表。实际上,当我们C语言在做初始化加载进这个进程时,对应的这个表就已经形成了,即使我们没有写命令行参数,这个表也会有。C语言会为我们定义一个environ这样一个全局的环境变量指针,这个指针指向这个表,因为这个表里的元素是 char* 类型的,那要指向它就需要是 char** 类型的指针。其实,这就相当于把environ传入了我们的程序。
我们要用它需要使用 extern 关键字来声明一下,告诉系统我们要用这个 environ 。
编译运行后:
总结:
在我的进程的上下文中,获取环境变量的三种方式:
- char* env[ ]
- getenv()
- extern char** environ
我们最好使用getenv(),因为它可以根据我们的需要获取对应的环境变量,而不是全拿进来。