目录
[1. 命令行参数](#1. 命令行参数)
[1.1 概念](#1.1 概念)
[1.2 指令如何实现不同功能](#1.2 指令如何实现不同功能)
[1.3 命令行进程](#1.3 命令行进程)
[2. 环境变量](#2. 环境变量)
[2.1 main函数参数](#2.1 main函数参数)
[2.2 认识各种环境变量](#2.2 认识各种环境变量)
[2.3 char **environ](#2.3 char **environ)
[3 本地变量](#3 本地变量)
[3.1 认识本地变量](#3.1 认识本地变量)
[3.2 转换成环境变量](#3.2 转换成环境变量)
[3. 与环境变量比较](#3. 与环境变量比较)
1. 命令行参数
1.1 概念
在终端上,一般使用命令行进行交互。如上ls指令可以查看文件或者目录的属性,在其后面加上一些选项-l或者-a,可以告知操作系统执行更细化的功能。 在命令行上,不管是指令还是选项,叫做命令行参数,它们一般都以空格隔开。指令其实是一个可执行程序。
cpp
#include <stdio.h>
int main(int argc, char* argv[])
{
for(int i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
我们使用C语言写代码时,都会使用到main函数,但是不会给main函数设置参数。其实main函数有自己的参数,可以向上面一样写参数类型。第一个参数是整型变量,第二个参数是字符指针数组,用于存储字符串的。并使用for循环打印字符指针数组中指向的字符串。
执行myproc程序结果如上。当命令行中只输入该程序,argv数组只存储了"./myproc",argc变量为1。如果后面使用空格隔开输入一些字符串选项,argc变量就是命令行中以空格隔开字符串的个数,argv数组就会存储这是字符串的指针。
因此,我们可以得出结论,main函数第一个参数是命令行参数的个数,第二个参数存储命令行各个参数的字符串数组。main函数前两个参数叫做参数列表
1.2 指令如何实现不同功能
cpp
#include <stdio.h>
#include <string.h>
//./myproc -opt1 -opt2 -opt3
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("Usage: myproc -opt\n");
return 1;
}
if (strcmp(argv[1], "-opt1") == 0)
{
printf("功能1\n");
}
else if(strcmp(argv[1], "-opt2") == 0)
{
printf("功能2\n");
}
else if(strcmp(argv[1], "-opt3") == 0)
{
printf("功能3\n");
}
else
{
printf("默认功能\n");
}
return 0;
}
上面的代码中,我们让用命令行运行该程序的人,在该程序后面必须加一个选项。argc变量记录命令行参数个数,可以通过argc来判断是否命令行中后加上一个参数。如果在命令行中加上第二个参数,再判断该参数是否跟"-opt1"、"-opt2"、"-opt3" 三个字符串中的任意一个相等,相等的话,就会实现某种功能,不过这里使用打印来替代。
当我们在命令行中运行该程序,如果不加上选项,会打印提示消息。如果选项是-opt1,就会打印功能1。其实就类似于"ls -l"这行指令,判断ls指令后面跟的选项是否为代码中已实现功能的标记符号。这就是linux指令后面代选项能完成相应的功能的部分原理。
1.3 命令行进程
在终端中,你输入的命令行参数都会被shell拿到。shell是一个命令行界面软件,linux操作系统中一般是bash进程。shell拿到命令行的参数,会按照空格打散,形成一张字符串列表,并记录参数个数,就是我们所说的argv和argc变量。而一般命令行启动的程序的父进程就是bash进程。
子进程一般会拷贝父进程的数据,尤其是只读的数据。因此,子进程就可看到传进来的参数列表。
2. 环境变量
2.1 main函数参数
cpp
#include <stdio.h>
#include <string.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;
}
main函数的第三个参数也是一个字符串指针数组,这个数组就存储了环境变量。使用for循环进行打印,其中for循环的判断部分直接写个env[i],因为最后一个元素是NUll空指针,就会退出循环。
运行结果如上,我们会发现打印出许多的信息。这些就是环境变量,环境变量以key=value形式一行一行的展现。有些占许多行是因为字符太多,一个屏幕放不下才会折行。
其中有我们熟悉的bash进程路径,还有pwd当前路径,还有home家路径。
其实在linux操作系统中,输入env指令,就可以查看跟刚刚相同的环境变量。
2.2 认识各种环境变量
(1)PATH
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("I am a process, pid: %d\n", getpid());
return 0;
}
我们运行一个普通程序,需要在前面加上./,这与可执行程序名组成相对路径。相当于告诉操作系统该可执行程序的位置在哪,系统才会执行。
但当我们将该可执行程序拷贝到/usr/bin目录下,可以不加上点斜杠就能运行myproc程序。因为shell拿到命令行中的指令,会先去/usr/bin目录下寻找与指令匹配的可执行程序,如果找到了,就运行它。所以只要将我们的程序拷贝到该目录下,就不用加路径了。
那这是怎么做到的呢?这是因为linux操作系统会启动bash进程,而该进程会加载环境变量。我们可以通过env指令后面加上"$",再加上具体环境变量名称,就可以获取该环境变量的内容。环境变量中有个PATH变量,记录着许多路径。它是系统可执行文件的搜索集合。其中就包含有/usr/bin路径。
既然系统的进程会通过PATH环境变量下的路径去寻找匹配的可执行文件,那么我们可以将myproc程序的路径添加到PATH变量中。你要在命令行输入你要改变的环境变量名再加上个"="符号,其中"$PATH"表示原有的环境变量,再加上冒号,后面跟上添加的路径。
使用env查看PATH变量,添加路径成功,直接输入myproc,运行程序成功。我们就可以通过改变环境变量直接运行可执行文件,不需要添加路径。但是如果我们关掉shell重启,我们刚添加的路径还在吗?
当我们重启shell,输入myproc运行,会报找不到该指令的错误。这是因为重启shell时,系统会从家目录读取两个配置文件.bashrc和.profile,加载到内存中,让bash进程读取。而我们刚刚只是修改bash进程里的PATH变量,对配置文件没有影响。除非手动添加到配置文件中,不然重启之后无效。
(2)USER
USER变量会记录shell启动后的用户名。
进程内部有个uid变量,该变量记录的是哪个用户启动的进程。那么你在启动进程的时候,系统怎么知道你是谁?并且把你的uid写到进程的pcb中?
这是因为启动shell进程,系统会读取用户和系统相关环境变量的配置文件,形成自己的环境变量表。而所有在命令行启动的程序,都是shell进程的子进程。子进程会继承父进程的许多属性,环境变量表中的大部分属性会被拷贝。因此,子进程就会读取继承下来的环境变量表中的USER变量。
HOME变量是记录shell启动后的家目录。普通用户的家目录一般是"/home"加上用户名,超级用户root的家目录就是/root。那为什么我们启动shell后,自动除于家目录下?
这是因为当我们登录时,系统会启动shell进程。linux环境下,一般是bash进程。bash进程会读取环境变量相关的配置文件,并设置好PATH和HOME等变量。而bash也是个进程,有自己的cwd,即当前工作目录。通过chdir这种系统调用函数改变cwd为HOME变量中的家目录。至此,我们启动shell程序,就出在家目录下。
由此引深开来,所有在命令行启动的程序都是bash进程的子进程,那么这些子进程会继承bash进程许多属性,那么cwd就会以bash进程为基础。
(3)SHELL
shell变量是记录你登录时启动的哪种版本的shell进程,linux下是bash。
pwd变量是保存当前进程所在的工作路径。那为什么存在pwd环境变量呢?我们下面通过代码获取环境变量。
- 第一种方法可以是使用main函数的参数列表中的第三个字符串数组变量,判断数组中字符串前三个字符是否为PWD,再对后面的字符串进行切割,获取工作路径。
- 第二种方法是使用系统封装的getenv函数,来获取相应的环境变量。
(4)PWD
getenv函数可以获取环境变量,只需要传入变量名字符串,就会返回该变量的内容。如果没有与传入字符串匹配的变量,会返回空指针。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PWD"));
return 0;
}
我们运行envtest程序可以获取当前工作路径,pwd指令也可以获取当前工作路径。此时,我们完成了对pwd指令的实现。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
const char *who = getenv("USER");
if (strcmp(who, "Greg_122") == 0)
{
printf("正常执行命令!\n");
return 0;
}
else
{
printf("无权访问!\n");
return 1;
}
return 0;
}
我们通过getenv函数,可以获取启动当前进程的用户,再做个判断,看启动该进程的用户是不是指定的用户。
这里可以看到,当用户为Greg_122时,可以正常访问。即使是超级用户root,也无法运行该程序内部的正常内容。这就实现了权限的设置。
(5)OLDPWD
oldpwd会记录上次所在的工作路径。而"cd -"指令就可以回到上次所在的工作路径,说明这条指令是通过获取OLDPWD环境变量实现的。
2.3 char **environ
获取环境变量还可以通过environc二级字符类型指针,这是一个全局的指针变量,指向的是环境变量表的第一个元素。
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
for(int i = 0; __environ[i]; i++)
{
printf("%s\n", __environ[i]);
}
return 0;
}
写一个for循环遍历获取环境变量,运行结果如下。
3 本地变量
3.1 认识本地变量
linux操作系统中,shell启动后不仅要环境变量,还有本地变量。可以通过"变量名"加上"=",再加上"变量内容"来定义。定义一个本地变量后可以使用echo指令加上符号,再跟上变量名,来打印内容。其中符号类似一种指针的用法。定义了一个本地变量,还可以使用到指令中。
通过env指令查看环境变量,发现没有刚刚定义的本地变量。那么本地变量存放在哪里呢?shell会将本地变量使用一个指针数组维护起来,形成一张本地变量表。不仅有本地变量表,还会有环境变量表,命令行参数表,都是指针数组。
3.2 转换成环境变量
我们可以通过set指令读取到所有的本地变量和环境变量。那本地变量与环境变量有什么关系呢?
我们可以通过export指令,将本地变量i导出到环境变量表中。还可以使用export后面加上变量定义,导出到环境变量表中。如下图所示,操作系统会先创建一个bash进程来管理命令行启动的程序。bash进程中会维护环境变量表env,命令行参数表,还有本地变量表。
新增本地变量相当于bash进程识别到"变量名=内容"的字符串格式,在内部开辟一段空间,拷贝该字符串,然后将该字符串的起始地址链入到本地变量表中。export指令,就相当于将本地变量表中指向变量的指针删除,环境变量表链入该变量指针。
但是一旦退出,重新登陆shell,你会发现不管你定义的本地变量,还是导入到环境变量新增变量,都不存在了。所新增的变量只作用于当前启动的shell进程,一旦会话结束就会销毁新增变量。
3. 与环境变量比较
与本地变量不同,环境变量会被传递给子进程。本地变量默认情况下不会传递给子进程。我们下面可以写一个demo展示一下。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
const *isrunning = getenv("ISRUNNING");
if (isrunning == NULL)
{
while(1)
{
sleep(1);
printf("当前进程首次启动!\n");
}
}
else
{
printf("当前进程已经启动,不要再启动了!\n");
}
return 0;
}
我们定义一个指针变量,用来接受getenv函数返回的指针,如果该进程有ISRUNNING环境变量,那么isrunning指针就不为空,会打印else下的语句。如果不存在,就会一直while循环打印"当前进程首次启动"这句话。
我们添加一个ISRUNNING本地变量,运行该程序,发现会一直循环打印"当前进程首次启动"这句话。当我们添加到环境变量中,运行该程序,只会打印一句话,表明ISRUNNING变量被子进程获取,而本地变量却不能被子进程获取。
由此得出一个结论,环境变量具有"全局属性",可以被bash的所有子进程获取。那为什么环境变量要具有这种性质呢?
- 系统配置的一致性:环境变量的内容通常来源于系统的配置文件,这些文件定义了系统范围内的参数和设置。由于这些配置文件具有指导性,环境变量的全局性质确保了所有进程都能够访问这些关键的配置信息,从而保持系统行为的一致性。
- 进程间信息的共享 :每个进程是具有独立性的!环境变量可以用来给进程间传递数据(只读)。
创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!