【Linux】环境变量
【Linux】环境变量
- 命令行参数
- 为什么
- 谁做的
- 环境变量
- 直接看现象
- 将程序移到/usr/bin
- 添加环境变量
- 更多的环境变量
- 结合代码与程序
- 内建命令与本地变量
命令行参数
我们平时写的函数通常都可以传参,如 int add (int a, int b)。而我们用 C/C++ 写的 main 函数也是一个函数,好像没有传过参数,那它可不可以传参呢?
main 函数也可以传参,我们平时没有这样做过,那是因为 main 函数的参数可传可不传。它的其中两个参数为:
- int argc,它代表了 argv 的有多少元素
- char* argv[],字符指针数组,字符指针一般会指向字符或者字符串,这里是字符串
所以,argv 是一个数组,里面放着若干字符串的地址;argc 则是 argv 的元素个数
我们可以写一个代码,把 argv 中的字符串打印出来看看
#include <stdio.h>int main(int argc, char* argv[])
{for (int i = 0; i < argc; i++){printf("argv[%d] -> %s\n", i, argv[i]);}
}
编译得到我们的程序

然后运行

我们发现,argv 中只存在一个字符串,而且恰恰和我们刚刚输入的命令相同。于是我们猜想,argv 中存的会不会是我们执行程序时向命令行输入的字符串呢?我们可以在执行程序时,输入更多选项,看看会不会被打印出来

可以看到,确实和我们猜想的一样,argv 中存的是命令行字符串。那 argv 中是怎么存数据的呢?从图上我们可以看出:
- argv 是一个变长数组
- argv[0] 是固定的,存的是程序的路径+名称
- 其余位置存的是与程序相对应的选项
- argv 末尾位置存的是NULL
argv 以 null 结尾,我们可以把之前的代码稍微修改一下就可以验证了
for (int i = 0; argv[i]; i++)
这样,如果 argv 是以 NULL 结尾的话,程序就会自动结束;反之不会结束


到这里,我们可以知道:操作系统中存在某程序,会把我们在命令行中输入的命令字符串截断为若干字符串,存入 argv 表中

那为什么要有命令行参数呢?又是谁实现的呢?
为什么
我们先来看看下面这个代码,运用一下命令行参数
#include <stdio.h>
#include <string.h>int main(int argc, char* argv[])
{if (argc != 2){// 用法printf("Usage: %s -[a, b, c]\n", argv[0]);}else if (strcmp(argv[1], "-a") == 0){// 功能1printf("This is function1\n");}else if (strcmp(argv[1], "-b") == 0){// 功能2printf("This is function2\n");}else if (strcmp(argv[1], "-c") == 0){// 功能3printf("This is function3\n");}else{// 错误选项printf("No this function!\n"); }
}
我们预想的是在运行此程序时,必须带一个选项,如果不带选项或者带了多个选项,就给出程序的用法;带了正确选项就运行相应的功能;如果选项带错了,就给出提示。以下是运行结果:

除了我们自己写的程序可以使用命令行参数,系统自带的命令本身也是用 C/C++ 写的程序,也可以使用命令行参数。如 ls、ps 等。所以说运行自己写的程序和运行系统命令没什么区别

综上,命令行参数本质是交给程序的不同选项,用来实现定制程序功能
谁做的
我们先来看一下以下代码,看看父子进程中的全局变量是怎样的
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int g_val = 1000; // 全局变量
int main()
{printf("I am father process, pid: %d, ppid: %d, g_val: %d\n", getpid(), getppid(), g_val);sleep(5);pid_t id = fork();if (id == 0){// childwhile(1){printf("I am child process, pid: %d, ppid: %d, g_val: %d\n", getpid(), getppid(), g_val);sleep(1);}}else{// fatherwhile(1){printf("I am father process, pid: %d, ppid: %d, g_val: %d\n", getpid(), getppid(), g_val);sleep(1);}}return 0;
}

可以看到,父子进程的 g_val 值相同,所以这时可以有一个结论:父进程的数据,默认情况下是可以被子进程看到并访问的。
这时我们再回到命令行参数的话题,再将程序改回我们的带选项的,再加一句打印进程信息
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>int main(int argc, char* argv[])
{// 输出进程信息printf("I am father process, pid: %d, ppid: %d\n", getpid(), getppid());if (argc != 2){// 用法printf("Usage: %s -[a, b, c]\n", argv[0]);}else if (strcmp(argv[1], "-a") == 0){// 功能1printf("This is function1\n");}else if (strcmp(argv[1], "-b") == 0){// 功能2printf("This is function2\n");}else if (strcmp(argv[1], "-c") == 0){// 功能3printf("This is function3\n");}else{// 错误选项printf("No this function!\n"); }
}
我们反复启动程序,可以发现它的 ppid,也就是父进程的 pid 是不变的

我们可以查一下这个进程是谁
ps axj | grep 进程id

这时我们查到,这个进程是bash。其实,我们在命令行中启动的程序会变为进程,而它们的父进程是bash
我们在命令行输入的命令字符串,默认都是给了 bash,然后 bash 将这个字符串拆分为若干字符串,存入argv 表。而我们在命令行启动的程序,会变为bash 的子进程,而我们上面也得到了结论:子进程默认可以看到并访问父进程的数据
所以,子进程在启动时可以得到父进程 bash 维护的 argv 表
环境变量
直接看现象
上面我们说过,命令的本质也是程序,所以运行自己写的程序和运行命令没什么区别。虽然这么说,但我们还是能感到区别的,例如:运行自己的程序,需要路径+程序名;而运行系统命令只需要命令名就可以。

系统的命令也是有路径的,存在于路径usr/bin下

那为什么执行命令不用加路径呢?没有路径,bash 是如何找到并加载命令的呢?
这是因为,Linux系统中存在一些全局的配置,会告诉 bash 执行系统命令时应该到哪些路径下去寻找要执行的命令,这些配置就是环境变量
而 PATH 就是环境变量中的一个,我们可以使用echo命令查看一下里面的内容

这样直接使用并不能正确查看环境变量,有点类似于指针,需要在环境变量名前加 $
echo $环境变量名

可以看到,里面有我们之前提到的 /usr/bin 路径,还有其他路径,这些路径都是用:分隔的。当 bash 执行命令时,会到这些路径下面逐个寻找,找到命令后就会加载。如果找不到命令,就会报command not found

将程序移到/usr/bin
明白这些之后,我们也想执行自己写的程序时不加路径,怎么实现呢?简单暴力一点,可以把我们写的程序丢到环境变量写的路径中,例如 /usr/bin 路径。涉及到系统的操作时,普通用户无权操作,需要提权为 root

这时我们直接输入程序的名字就可以执行了

也可以使用which命令查看命令的路径
which 命令名

这样虽然可以直接执行我们的程序,但是这种方法有点挫,而且直接将我们自己写的程序丢到 /usr/bin 中,可能会污染系统指令集。所以不建议这样做,所以我这里就删除了

另外,在这里我们可以简单地认为:Linux 中软件的安装卸载,就是在 /usr/bin 路径下复制删除程序。这只是简单认识,当然还会有其他操作
添加环境变量
既然 PATH 中存放的都是可执行程序的路径,那我们应该也是可以把自己程序的路径加到 PATH 中。如下

这时我们就可以直接执行我们自己的程序了

但是出现了问题,系统本身的命令有很大部分不可以用了,只有少部分还可以使用

这是因为我们添加环境变量时,直接将 PATH 原来的内容覆盖掉了,导致 bash 执行系统命令时找不到相应路径。这时我们将 PATH 改回去就可以了

如果之前没有记录 PATH 的内容,只需要重启 shell 即可,重新登陆 Linux系统后 PATH 就会复原。
正确添加环境变量的做法因该是这样的:
PATH=$PATH:要添加的路径

这样虽然可以成功添加,但是重启系统后 PATH 依然会复原。以下就是重启后的 PATH

为什么重启后 PATH 就会复原呢?
这是因为,系统中存在很多配置文件,在我们登录的时候就会被加载到 bash 进程中,bash 运行在内存中,也就是被加载到了内存中,其中就包括了环境变量。
因此只要我们不对配置文件进行修改,无论我们如何修改内存中的环境变量,重新登录时系统都会加载一遍配置文件,修改都不会有效

这些配置文件是位于用户家目录的 .bash_profile .bashrc,还有 /etc/bashrc

我们可以看看这些文件里的内容

可以看到,确实存在 PATH,当我们对这里的 PATH 进行了修改,那就是永久有效的。但是依旧不建议修改
更多的环境变量
PATH 只是众多环境变量中的一个,我们还可以认识一下其他环境变量
HOME
这个环境变量中存的是我们用户家目录的路径,bash可以通过这个定位用户的家目录,这样每次我们登录 Linux 就可以直接进入到对应的家目录

PWD
这个环境变量是动态变化的,记录了我们当前的工作目录

SHELL
有了这个环境变量,系统才知道要加载哪个 shell 到内存中

HISTSIZE
这个环境变量的意思是:系统中记录了多少条历史命令

此外,我们还可以使用env命令查看系统中的环境变量

如果我们想添加自定义的环境变量也是可以的
export 环境变量名=值
例如,添加一个环境变量 myVal=123,我们可以使用 echo 或者 env 命令查到

如果我们不想要这个环境变量了,也是可以取消的
unset 环境变量名

如果添加环境变量时,没有打 export,也是可以添加成功的。只不过和正常的环境变量有一些区别:在 env 中查不到,但是可以通过 echo 打出来。这种我们称之为本地变量

结合代码与程序
上面我们在命令行中了解了如何查看环境变量,下面我们来看一下如何用程序查看环境变量
系统中存在着一个全局变量 environ,用于存储系统的环境变量

我们在程序中使用 environ 时,需要声明一下。至于为什么它是一个二级指针,稍后我们就清楚了
extern char** environ;
我们直接上手使用,代码如下
#include <stdio.h>int main()
{extern char** environ;for (int i = 0; environ[i]; i++){printf("environ[%d]->%s\n", i, environ[i]);}return 0;
}
运行效果如下

可以看到,程序把环境变量都打印出来了,和我们直接在 shell 使用 env 命令的效果一样。而我们的程序运行起来形成的进程,是 bash 的子进程,也就是说这些环境变量是子进程从父进程bash拿到的
那么 bash 是如何组织这些变量的呢?这就让我们联想到上面说命令行参数表,bash 也会维护另一张表,env表,其中存储的是系统的环境变量
上文已经提过,系统的配置文件存在于磁盘中,bash 启动时会将这些数据加载到内存中,将系统的环境变量存入 env 表维护起来。而环境变量本质就是一个字符串,例如“/usr/local/bin…”,可以用一个字符指针来表示,所以 env 表就是一个指针数组,并且也是以 NULL 结尾,与命令行参数表相同。而全局变量 char**environ 则是一个指向 env 表的指针,也就是 env 表首元素的地址,所以是一个二级指针

那既然 env 表和命令行参数表类似,那我们可不可以将 env 表作为参数传给 main 函数呢?
是可以的。我们可以把程序改为这样
#include <stdio.h>int main(int argc, char* argv[], char* env[])
{for (int i = 0; env[i]; i++){printf("env[%d]->%s\n", i, env[i]);}return 0;
}
运行结果:

同样可以将环境变量打出来。这里再顺便介绍一个函数,可以取出指定的环境变量
getenv("环境变量名")

测试代码:
#include <stdio.h>
#include <stdlib.h>int main(int argc, char* argv[], char* env[])
{printf("PATH: %s\n", getenv("PATH"));return 0;
}

所以到现在,我们知道 bash 会给子进程维护两张表:
- 命令行参数表,数据是从命令行得来,是用户输入的
- env 表,数据是从系统配置文件得来的
bash 会通过各种方法将它们交给子进程,而子进程又可以创建子进程,又因为子进程可以看到并访问父进程的数据,所以环境变量是可以一直被继承下去的,所以说环境变量具有全局属性
内建命令与本地变量
内建命令
当我们使用 export 导出命令时,例如 export myval=123,是可以在 bash 中查到的

可是这很奇怪:当我们运行 export 时,不应该创建子进程吗?既然是子进程,那么子进程做出的修改就不应该被父进程 bash 看到(详情见[地址空间]相关知识)
所以这里要说的是,像export echo这样的命令很特殊,叫做内建命令。什么是内建命令?在 Linux 系统中,大部分的命令都是由 bash 创建子进程来执行的,少部分是由 bash 亲自执行的,由 bash 亲自执行的命令就是内建命令
所以,当我们使用 export 导出环境变量时,是由 bash 亲自执行,修改 bash 维护的 env 表的。所以 bash 可以看到做出的修改
如何验证内建命令:将 PATH 置空后,仍然还可以使用的命令就是内建命令

本地变量
上面我们也提到,如果导出环境变量时不加 export,也是可以导出的。虽然可以被 echo 打印出来,但是在 env 表中查不到,这就是本地变量。

这时候我们启动子进程,看看用程序能不能查到

从运行结果来看,子进程无法查到本地变量

其实,本地变量是在 bash 中存放的,但是无法被子进程继承,只在 bash 内部有效。另外,我们可以直接用 export 本地变量名,将已经存在的本地变量直接导出到 env 表,这样就可以被子进程继承了

以上,我们可以知道本地变量只在 bash 内部有效,不可以被子进程继承。而 echo 可以查看到本地变量,进一步说明了 echo 不是子进程,是由 bash 亲自执行的
结束,再见 😄
