一、命令行参数
我们先看一段代码:
int main(int argc, char* argv[]) {
int i = 0;
for (i; i < argc; i++) {
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
运行结果如下:

上面的 main 函数的两个参数就是命令行参数,平时我们使用 main 函数时默认都是没有使用命令行参数的,但其实它是真实存在的。argv 是一个字符指针数组(命令行参数表),它指向一个一个的字符串,argc 是字符指针数组的元素个数。(arg 是参数英文 arguments 的缩写,c 表示 count,v 表示 vector)
那这两个参数起什么作用呢?我们看运行结果可以猜个大概,我们输入的一串指令本质就是字符串,整体字符串会被分为由空格隔开的一个个字串,每个子串会被存放在 argv 数组中。
为什么有命令行参数?
我们再来看一段代码:
int main(int argc, char* argv[]) {
if (argc != 2) {
// 如果不符合命令格式
printf("Usage: %s: -a/-b/-c\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-a") == 0)
printf("这是功能 1\n");
else if (strcmp(argv[1], "-b") == 0)
printf("这是功能 2\n");
else if (strcmp(argv[1], "-c") == 0)
printf("这是功能 3\n");
else
printf("功能不支持\n");
}
运行结果:

我们之前学习过,命令本质是程序(大部分是 C 语言写的),上面代码编译形成的可执行程序 myls 可以把它理解成一种命令,-a -b -c 可以理解成命令的选项。所以我们现在明白了,原来所谓选项本质就是字符串,它可以以一定方式传递给命令程序内部的 main 函数,在命令程序内部实现的时候,就可以根据不同的选项,实现类似功能的不同表现形式。
命令行参数是由谁做的,命令字符串先被谁拿到?
这里不解释太多,我们之前已经知道命令行启动的所有进程都是 bash 的子进程,那么命令程序自然也是 bash 的子进程,所以命令字符串会先被 bash 拿到,然后由父进程 bash 进行一系列操作把命令字符串打散传给子进程(命令程序),然后子进程就拿到了命令行个数和命令行。具体过程在后面讲程序替换时再细讲。
总结:linux 系统中,命令行参数被 shell 获取时,通常会被父进程(bash)维护成一个指针数组(argv),并提取该数组中有多少有效元素(argc),然后再把 argv、argc 传递给对应子进程的 main 函数,子进程拿到后就可以用来实现不同功能了。
补充:argv 数组最后一个元素必须以 null 结尾。下面是示例代码:
int main(int argc, char* argv[]) {
int i = 0;
for (; argv[i]; i++) {
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
运行结果:

命令行参数有以下几个特点:
- 命令行参数至少有一个,因为程序要运行至少要有./程序名或者指令名。
- 进程对应的程序名一般是 argv[0]。
- 命令字符串有几个由空格隔开的字串,argc 就是几。
二、环境变量
环境变量虽然我们没有真正了解过,但是它无处不在,下面我们一起来揭开它的神秘面纱吧。
一个现象引入环境变量
我们之前在学习指令时知道系统级指令可以在任意路径下运行,因为系统级指令可以不加./直接用指令名运行,而我们自己写的可执行程序不做任何修改的情况下只能在可执行程序所在路径下运行,因为系统规定执行自己的可执行程序需要在程序名前加./运行,比如下面:

当我们直接 a.out 时系统会报 command not found,这个报错背后意味着系统要找我要执行的程序,我们之前的说法是系统会去 /usr/bin/路径下找,当我们把自己的程序拷到 /usr/bin/路径下后,就可以不加./直接运行了。这里我们就可以详细说一说它背后的原理,/usr/bin/路径其实被包含在系统内的一个环境变量 PATH 中,操作系统本质会去环境变量 PATH 中查找可执行命令。
要证明上面说法我们可以用下面的指令查看环境变量的内容:

果然 /usr/bin/在 PATH 路径中,当一个指令在 PATH 路径中找到了就可以直接运行,如果指令不在 PATH 路径中我们直接运行系统就会报 command not found,我们自己创建的可执行程序所在的目录(当前目录)默认是不会被 PATH 路径包含的,所以我们自己的程序默认需要加./运行。
可是 linux 为什么要这样设计呢?这主要是处于安全考虑,如果当前目录被加入 PATH,当目录中存在伪装成系统命令的恶意程序(比如命名为 ls 的恶意脚本)时,在系统任意路径下执行 ls 会优先运行当前目录的恶意程序,而非系统真正的 ls 命令,存在安全风险。
指令中会用到 PATH: 我们之前介绍的 which 指令本质就是在 PATH 里找对应的命令:

修改环境变量
修改环境变量的操作和我们在学习语言时的赋值操作类似,我们下面会分别操作将 PATH 置为空和将我们自己的路径加入环境变量。
将 PATH 置为空:

我们可以看到将 PATH 置为空后在 /usr/bin/路径下的外部指令就无法使用了,而入 cd、pwd 等内部指令还能正常使用。但是不用怕,我们退出 Xshell 然后重新登陆环境变量 PATH 就正常了,原理我们后面说。
将当前路径加入 PATH:

将当前路径加入 PATH 后我们的 a.out 就可以像系统命令一样在任意位置直接运行了。
配环境的本质
在平时我们配 java 环境、python 环境本质就是先将对应的可执行程序下载到电脑里,然后将可执行程序的路径添加到环境变量里,这样可执行程序就可以不局限于可执行程序的路径,而可以在任意路径下运行。
查看环境变量
env 指令可以查看系统在当前用户下的所有环境变量:

环境变量本质
环境变量有的让用户处于默认家目录(cd ~ == cd $HOME),有的用来记录当前用户是谁(whoami == echo $USER),有的用来记录当前主机名是什么等等,这些环境变量是系统级的全局变量,不同的变量具有不同的用途,它们会在我们用户登陆系统时被天然初始化,我们在任意路径下都可以随时访问。
如何通过代码获取环境变量
1、main 函数获取
这里要补充一点,环境变量是 main 函数的第三个参数,前两个 main 函数参数是命令行参数,我们已经介绍过了,这三个 main 函数参数都是可以省略的,如果要显示使用第三个参数的话前两个参数也要带上,这是严格要求的。 main 函数第三个参数本质也是一个字符指针数组(环境变量表),也是指向一个一个的字符串,这里的字符串是环境变量的 KV 键值对,如下图所示:(左边小方块 environ 后面解释)

我们下面拿代码来验证一下:
// 环境变量参数
int main(int argc, char* argv[], char* env[]) {
int n = 0;
for (; env[n]; n++) {
printf("env[%d]: %s\n", n, env[n]);
}
return 0;
}
运行结果:

2、通过函数获取单个环境变量
getenv 函数可以获取单个环境变量,使用该函数需要包含 stdlib.h 头文件。

getenv 函数参数传环境变量名(key),返回值是指向环境变量的具体数据(value)的指针,若环境名参数不存在则返回 NULL。示例如下:
#include <stdlib.h>
int main() {
char* path = getenv("PATH");
printf("PATH = %s", path);
}
运行结果:

3、通过 environ 变量获取
C 语言为我们提供了一个全局指针变量 environ,在 linux 系统下使用不需要包含 unistd.h 头文件,只需要声明一下 extern char** environ 即可使用。

它是一个二级指针,指向了字符指针数组(环境变量表)的首元素地址,既然它是一个二级指针,我们又可以通过 environ[]访问整个字符指针数组,示例如下:
int main() {
extern char** environ;
int n = 0;
for (; environ[n]; n++) {
printf("environ[%d], %s\n", n, environ[n]);
}
}
运行结果:

环境变量的来源
上面我们介绍了如何用代码获取当前进程的环境变量,这只是方法,我们还需要知道环境变量从哪来。首先我们要知道环境变量本质就是一些数据,环境变量可以类比命令行参数,我们当前的子进程环境变量其实是从父进程(bash)来的,我们之前学习过,子进程会继承父进程的代码和数据。
有了前面的知识,我们可以做个总结: 父进程 bash 在命令行解析运行程序时会维护两张表:命令行参数表和环境变量表,命令行参数表一直在变,因为命令行一直在输入指令,而环境变量表相对比较稳定,父进程 bash 在 fork 子进程后子进程会继承父进程的代码和数据,子进程自然而然就可以看到并获取环境变量了。
看到这里我想大家一定还有问题,父进程 bash 是如何获取环境变量数据的呢?我们知道命令行参数数据是由用户输入的,父进程 bash 可以直接获取,那环境变量呢?其实 bash 是从系统的配置文件中获取的环境变量,系统中有一些配置文件会记录当前环境变量的名字和内容,当我们登陆 Xshell 时系统会创建并运行 bash,bash 就会用读文件操作读取这些配置文件中的内容,然后在 bash 进程内部动态申请指针数组(环境变量表),接着解析读取到的环境变量内容依次放入环境变量表中。
所以我们介绍的命令行参数表和环境变量表这两张表本质是内存级的,这里我们就能理解为什么前面我们把 PATH 清空后再重启 Xshell 后环境变量 PATH 又复原了,原因就是重启 Xshell 后系统重新读取配置文件把环境变量又加载到内存中了。
这些配置文件一般位于普通用户的家目录中,只不过它们是隐藏文件,所以要查看它们需要加-a,我们重点关注用方框框起来的两个文件:

我们来看看这两个文件中的内容,里面大部分都是 shell 脚本,简单来说父进程 bash 会执行这些脚本把对应的环境变量导到它的地址空间里或者说程序里。


验证: 下面我们来验证一下,我们修改.bash_profile 文件中的 PATH 路径,把我们自己的路径添加进去,然后 echo 一句话:

我们退出 Xshell 然后重新登陆:

我们看到输出的一句话代表配置文件确实被执行了,被添加进 PATH 的路径里的可执行文件也可以像指令一样在任意位置运行了。
但是并不是所有环境变量都是从配置文件来,还有少部分是 bash 启动之后动态获取或创建的,比如 PWD,它是从 bash 调用系统调用接口 getcwd() 获取的,具体过程如下:当 bash 启动时,会调用 getcwd() 获取初始 cwd,并存入 PWD 环境变量。当用户执行 cd 命令切换目录时,bash 会先通过 chdir() 系统调用修改自身的 cwd,然后立即调用 getcwd() 获取新的 cwd 路径,更新到 PWD 环境变量中。
getcwd()文档:

getcwd()可以获取当前进程的工作路径,哪个进程调用 getcwd() 就返回哪个进程的当前工作路径,它的第一个参数是缓冲区,第二个参数是缓冲区的大小,返回值是当前工作路径字符串的首元素地址。 下面是一段示意代码,不仅是 bash 可以调用 getcwd,子进程也可以通过 getcwd 获取子进程的 pwd。
int main() {
char pwdbuf[128];
char* pwd = getcwd(pwdbuf, sizeof(pwdbuf));
printf("%s\n", pwd);
return 0;
}
运行结果:

环境变量的作用
我们前面介绍了许多环境变量的周边概念,那环境变量在实际开发中有哪些作用呢?下面小编编写一个只有自己才能运行的程序,root 来了都不行。
int main() {
char* who = getenv("USER");
if (strcmp(who, "fdb") != 0) {
printf("我不让你执行:%s\n", who);
return 1;
}
// 自己才能运行的程序:
printf("这个程序只有我能执行,root 也没招!\n");
return 0;
}
运行结果:


借助这个例子想告诉大家不同的环境变量会有不同的用途,它是全局的,如果你想获取可以随时获取。系统在某些场景下会自动获取环境变量,比如 (cd -) (cd ~) pwd 等等。
本地变量和相关指令
在 linux 系统中,除了环境变量,还有本地变量的概念,本地变量只在父进程(bash)内部有效,它不会进环境变量表,不会像环境变量一样能继承给子进程。下面是几个相关的指令介绍: echo:显⽰某个环境变量值 export:设置⼀个新的环境变量

env:显⽰所有环境变量 unset:清除环境变量

set:显⽰所有变量,包括本地变量和环境变量
环境变量的全局性
下面我们来聊聊环境变量为什么是全局变量,我们还是先看一段示例代码:
int main() {
char* myenv = getenv("MYENV");
if (myenv == NULL) {
printf("MYENV 不存在!\n");
} else {
printf("MYENV=%s\n", myenv);
}
return 0;
}

上面的运行结果是'MYENV 不存在!':是因为我们创建的 MYNEV 是本地变量,本地变量只有 bash 能拿到,/a.out 创建的子进程拿不到这个本地变量。

当我们把 MYENV 导成环境变量后,子进程就能拿到这个环境变量了,同一份代码结果也发生了变化。
正因为 bash 的环境变量会把环境变量继承给子进程,而子进程也会创建子进程也可以继承到环境变量,所以以 bash 为发起点,它的子进程、孙子进程…都能继承到 bash 的环境变量,所以我们说环境变量是全局变量,具有全局属性。

内建命令的引出
我们先看下面的示例:

为什么这里的由 bash 创建的 echo 命令子进程能拿到本地变量 MYENV 并打印呢?这里需要引入一个概念:内建命令 在 linux 中,大部分命令是可执行程序,也就是/usr/bin 路径下的可执行文件,需要通过 bash 创建的子进程执行,还有一部分命令因为执行的时候没有风险,需要 bash 进程自己执行,我们把这些命令称作内建命令。 (但是有些内建命令比较特殊,也会在/usr/bin 路径下存在,但是它运行时不会创建子进程,这样做是为了在脚本模式中能执行 echo 命令。)


