【Linux】环境变量
命令行参数
我们平时写的函数通常都可以传参,如 int add (int a, int b)。而我们用 C/C++ 写的 main 函数也是一个函数,好像没有传过参数,那它可不可以传参呢?
main 函数也可以传参,我们平时没有这样做过,那是因为 main 函数的参数可传可不传。它的其中两个参数为:
- int argc,它代表了 argv 的有多少元素
- char* argv[],字符指针数组,字符指针一般会指向字符或者字符串,这里是字符串
所以,argv 是一个数组,里面放着若干字符串的地址;argc 则是 argv 的元素个数
我们可以写一个代码,把 argv 中的字符串打印出来看看
c
#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 结尾,我们可以把之前的代码稍微修改一下就可以验证了
c
for (int i = 0; argv[i]; i++)
这样,如果 argv 是以 NULL 结尾的话,程序就会自动结束;反之不会结束
到这里,我们可以知道:操作系统中存在某程序,会把我们在命令行中输入的命令字符串截断为若干字符串,存入 argv 表中
那为什么要有命令行参数呢?又是谁实现的呢?
为什么
我们先来看看下面这个代码,运用一下命令行参数
c
#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)
{
// 功能1
printf("This is function1\n");
}
else if (strcmp(argv[1], "-b") == 0)
{
// 功能2
printf("This is function2\n");
}
else if (strcmp(argv[1], "-c") == 0)
{
// 功能3
printf("This is function3\n");
}
else
{
// 错误选项
printf("No this function!\n");
}
}
我们预想的是在运行此程序时,必须带一个选项,如果不带选项或者带了多个选项,就给出程序的用法;带了正确选项就运行相应的功能;如果选项带错了,就给出提示。以下是运行结果:
除了我们自己写的程序可以使用命令行参数,系统自带的命令本身也是用 C/C++ 写的程序 ,也可以使用命令行参数。如 ls、ps 等。所以说运行自己写的程序和运行系统命令没什么区别
综上,命令行参数本质是交给程序的不同选项,用来实现定制程序功能
谁做的
我们先来看一下以下代码,看看父子进程中的全局变量是怎样的
c
#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)
{
// child
while(1)
{
printf("I am child process, pid: %d, ppid: %d, g_val: %d\n", getpid(), getppid(), g_val);
sleep(1);
}
}
else
{
// father
while(1)
{
printf("I am father process, pid: %d, ppid: %d, g_val: %d\n", getpid(), getppid(), g_val);
sleep(1);
}
}
return 0;
}
可以看到,父子进程的 g_val 值相同,所以这时可以有一个结论:父进程的数据,默认情况下是可以被子进程看到并访问的。
这时我们再回到命令行参数的话题,再将程序改回我们的带选项的,再加一句打印进程信息
c
#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)
{
// 功能1
printf("This is function1\n");
}
else if (strcmp(argv[1], "-b") == 0)
{
// 功能2
printf("This is function2\n");
}
else if (strcmp(argv[1], "-c") == 0)
{
// 功能3
printf("This is function3\n");
}
else
{
// 错误选项
printf("No this function!\n");
}
}
我们反复启动程序,可以发现它的 ppid,也就是父进程的 pid 是不变的
我们可以查一下这个进程是谁
shell
ps axj | grep 进程id
这时我们查到,这个进程是bash 。其实,我们在命令行中启动的程序会变为进程,而它们的父进程是bash
我们在命令行输入的命令字符串 ,默认都是给了 bash,然后 bash 将这个字符串拆分为若干字符串,存入argv 表 。而我们在命令行启动的程序,会变为bash 的子进程 ,而我们上面也得到了结论:子进程默认可以看到并访问父进程的数据
所以,子进程在启动时可以得到父进程 bash 维护的 argv 表
环境变量
直接看现象
上面我们说过,命令的本质也是程序,所以运行自己写的程序和运行命令没什么区别 。虽然这么说,但我们还是能感到区别的,例如:运行自己的程序,需要路径+程序名 ;而运行系统命令只需要命令名就可以。
系统的命令也是有路径的,存在于路径usr/bin下
那为什么执行命令不用加路径呢?没有路径,bash 是如何找到并加载命令的呢?
这是因为,Linux系统中存在一些全局的配置 ,会告诉 bash 执行系统命令时应该到哪些路径下去寻找要执行的命令,这些配置就是环境变量
而 PATH 就是环境变量中的一个,我们可以使用echo
命令查看一下里面的内容
这样直接使用并不能正确查看环境变量,有点类似于指针,需要在环境变量名前加 $
shell
echo $环境变量名
可以看到,里面有我们之前提到的 /usr/bin 路径,还有其他路径,这些路径都是用:
分隔的。当 bash 执行命令时,会到这些路径下面逐个寻找 ,找到命令后就会加载。如果找不到命令,就会报command not found
将程序移到/usr/bin
明白这些之后,我们也想执行自己写的程序时不加路径,怎么实现呢?简单暴力一点,可以把我们写的程序丢到环境变量写的路径中,例如 /usr/bin 路径。涉及到系统的操作时,普通用户无权操作,需要提权为 root
这时我们直接输入程序的名字就可以执行了
也可以使用which
命令查看命令的路径
shell
which 命令名
这样虽然可以直接执行我们的程序,但是这种方法有点挫,而且直接将我们自己写的程序丢到 /usr/bin 中,可能会污染系统指令集。所以不建议这样做,所以我这里就删除了
另外,在这里我们可以简单地认为:Linux 中软件的安装卸载,就是在 /usr/bin 路径下复制删除程序。这只是简单认识,当然还会有其他操作
添加环境变量
既然 PATH 中存放的都是可执行程序的路径,那我们应该也是可以把自己程序的路径加到 PATH 中。如下
这时我们就可以直接执行我们自己的程序了
但是出现了问题,系统本身的命令有很大部分不可以用了,只有少部分还可以使用
这是因为我们添加环境变量时,直接将 PATH 原来的内容覆盖掉了,导致 bash 执行系统命令时找不到相应路径。这时我们将 PATH 改回去就可以了
如果之前没有记录 PATH 的内容,只需要重启 shell 即可,重新登陆 Linux系统后 PATH 就会复原。
正确添加环境变量的做法因该是这样的:
shell
PATH=$PATH:要添加的路径
这样虽然可以成功添加,但是重启系统后 PATH 依然会复原。以下就是重启后的 PATH
为什么重启后 PATH 就会复原呢?
这是因为,系统中存在很多配置文件 ,在我们登录的时候就会被加载到 bash 进程中,bash 运行在内存中,也就是被加载到了内存中,其中就包括了环境变量。
因此只要我们不对配置文件进行修改,无论我们如何修改内存中的环境变量,重新登录时系统都会加载一遍配置文件,修改都不会有效
这些配置文件是位于用户家目录 的 .bash_profile
.bashrc
,还有 /etc/bashrc
我们可以看看这些文件里的内容
可以看到,确实存在 PATH,当我们对这里的 PATH 进行了修改,那就是永久有效的。但是依旧不建议修改
更多的环境变量
PATH 只是众多环境变量中的一个,我们还可以认识一下其他环境变量
HOME
这个环境变量中存的是我们用户家目录的路径,bash可以通过这个定位用户的家目录,这样每次我们登录 Linux 就可以直接进入到对应的家目录
PWD
这个环境变量是动态变化的,记录了我们当前的工作目录
SHELL
有了这个环境变量,系统才知道要加载哪个 shell 到内存中
HISTSIZE
这个环境变量的意思是:系统中记录了多少条历史命令
此外,我们还可以使用env
命令查看系统中的环境变量
如果我们想添加自定义的环境变量也是可以的
shell
export 环境变量名=值
例如,添加一个环境变量 myVal=123,我们可以使用 echo 或者 env 命令查到
如果我们不想要这个环境变量了,也是可以取消的
shell
unset 环境变量名
如果添加环境变量时,没有打 export,也是可以添加成功的。只不过和正常的环境变量有一些区别:在 env 中查不到,但是可以通过 echo 打出来。这种我们称之为本地变量
结合代码与程序
上面我们在命令行中了解了如何查看环境变量,下面我们来看一下如何用程序查看环境变量
系统中存在着一个全局变量 environ,用于存储系统的环境变量
我们在程序中使用 environ 时,需要声明一下。至于为什么它是一个二级指针,稍后我们就清楚了
c
extern char** environ;
我们直接上手使用,代码如下
c
#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 函数呢?
是可以的。我们可以把程序改为这样
c
#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;
}
运行结果:
同样可以将环境变量打出来。这里再顺便介绍一个函数,可以取出指定的环境变量
c
getenv("环境变量名")
测试代码:
c
#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 亲自执行的
结束,再见 😄