本篇文章是Linux_进程概念详解的续篇,请先阅读Linux_进程概念详解再来阅读本篇。
命令行参数
在C / C++中,每个程序都必须有一个main函数,该函数有很多的版本,我们最常用的就是不带参数的版本,也就是下面第一条语句
int main()
int main(int argc, char *argv[])
int main(int argc, char *argv[], char *env[]) //env后面会讲
由语句可知main函数可以接收两个参数:argc和argv。这两个参数允许程序接收来自命令行的输入参数。
- argc(Argument Count)是一个整数,表示传递给程序的参数数量,包括程序本身的名称。
- argv(Argument Vector)是一个字符串数组,包含了每一个传递给程序的参数。
现在我们通过一个简单的例子来理解argc和argv的用法。
int main(int argc, char* argv[])
{
printf("argc: %d\n",argc); // 打印argc
for(int i = 0;i < argc;i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
在这个程序中,我们首先打印出argc的值,即参数的数量。然后,我们遍历argv数组并打印出每个参数。 下面是输入不同参数打印的不同结果
argc记录了每次输入的参数个数,而argv(指针数组)则是将输入的每一个参数当作一个字符串打印出来。
注意:
argv[0]始终是执行程序的命令或程序名。
argc至少为1,因为它包括了程序本身的名称。
如果没有传递任何额外的参数,argc将为1,argv数组只包含一个元素,即argv[0]。
为什么要有命令行参数
我们再来看一段代码
int main(int argc,char* argv[])
{
if(argc != 2){
printf("Usage: code 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;
}
运行该代码
这是在干什么呀,输入的参数不同,输出的功能不同,可能大家现在觉得有点怪,别急。现在大家在来想一想,我们在Linux上输入的ls命令
大家知道,Linux上的命令基本上都是用C语言写的,而这些命令的各种选项是怎么实现的呢?其实就是用命令行参数实现的!!! 下面我来回答一下标题的问题,同一个程序,可以根据命令行参数,根据选项不同,表现出不同的功能。比如:指令中选项的实现!
环境变量
现在我们来谈谈开篇提到的 env,env其实就是环境变量。
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。这些变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
见一见环境变量本身_来看代码:
// 获取环境变量
int main(int argc,char* argv[], char* env[])
{
for(int i = 0;env[i];i++)
{
printf("env[%d]:%s\n",i ,env[i]);
}
}
上述代码结果也可以直接在命令行输入env直接查看!
常见的环境变量
环境变量,最开始都是在系统的配置文件中的
PATH : 指定命令的搜索路径
我们在Linux下创建一个code.c文件,在文件里写入如下代码
#include <stdio.h>
int main()
{
printf("hello world!\n");
return 0;
}
然后./code运行该代码,然后就会输出hello world!,很好,非常正确。不过我的问题是为什么要./code运行代码而不是直接code运行呢?像我们的pwd、ls等命令直接输入就可以运行,为什么要输入./呢?其实就是环境变量PATH的原因,PATH告诉了shell,你应该去那一条路径下查指令!下面是Linux下PATH的路径集合(系统可执行文件的搜索集合)
[lyx@VM-16-7-centos]$ echo $PATH // 查询环境变量PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/lyx/.local/bin:/home/lyx/bin
为什么叫做路径集合呢?很简单,就是因为它包含多条路径,每条路径之间以:作为分割符。当shell在运行任何一条命令行命令时,都会首先去该路径集合里面去查,如果找到了就会直接执行,否则就会输出command not found。
现在知道为什么要输入./code才可以运行了叭,因为code不在系统可执行文件的搜索集合里面,所以要./指明路径。如果我们想让自己的程序直接运行,可以将我们自己程序的路径拷贝到系统目录里面;也可以把自己程序的路径添加到PATH中。两种方法都可以。
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
任何一个用户都有自己的主工作目录,HOME
保存的就是当前用户的主工作目录。
PWD : 保存当前进程所在的工作路径
获取环境变量我们可以使用系统调用getenv(),从而模拟出pwd命令的功能
// envtest.c
// 模拟pwd功能
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n",getenv("PWD"));
return 0;
}
从而我们实现了一个pwd。
USER:当前正在使用系统的用户
如果我们想要区分用户是谁,只需要看USER就可以了。使用场景:我可以让我的程序,通过USER环境变量识别用户身份,只让指定的用户访问。请看下面代码
int main()
{
const char* who = getenv("USER");
if(strcmp(who, "lyx") == 0){
printf("程序正常执行命令!\n");
return 0;
}
else{
printf("无权访问!\n");
return 1;
}
}
理解环境变量
int main()
{
char * env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
运行上述代码发现没有结果,说明MYENV环境变量不存在。但是如果在命令行输入export MYENV="hello world" ,再次运行代码 ,发现有结果了。为什么?
要想解释清楚上述问题,我们来谈谈本地变量的概念,我们在命令行输入a=10,然后通过echo $a,命令行就会输出10,a就是本地变量。本地变量和环境变量都是由shell维护的,但是env只会查找环境变量,想要查找环境变量和本地变量就要使用set命令。我们可以通过export a命令将本地变量变为环境变量。也可以直接export b=20,将b导为环境变量。
我们首次登陆时OS会为我们创建一个属于当前用户bash进程,用来进行命令行解释,bash进程会根据我们的配置文件形成一张环境变量表(char* env[],该表的指针会指向每一个环境变量)和一张命令行参数表(char* argv[]),除此之外还会形成一张本地变量表(也是一个指针数组),所以当在命令行输入a=10时,bash识别a=10不是一条命令,就会把a=10当作一个字符串连接到本地变量表中。而所谓的export就是把a=10从本地变量表迁移到环境变量表中!
所以上述问题,就是将MYENV导成了环境变量,而环境变量可以被所有的bash之后的进程全部看到(可被继承)!!自然而然的就可以打印出结果,因此,环境变量具有“全局属性”。
环境变量为什么需要具有“全局属性”呢?
- 系统的配置信息,尤其是具有“指导性”的配置信息,它是系统配置起效的一种表现。
- 进程具有独立性!环境变量可以用来在进程之间传递数据(只读数据)。
获取环境变量的方法
- 通过main()第三个参数获取
- 使用系统调用getenv()获取
- 使用全局变量environ()获取
// 使用environ获取环境变量
extern char** environ;
int main()
{
for(int i = 0;environ[i];i++){
printf("%s\n",environ[i]);
}
return 0;
}
进程地址空间
老样子,先上代码
int gval = 100;
int main()
{
printf("我是一个进程, pid: %d, ppid: %d\n",getpid(),getppid());
pid_t id = fork();
if(id == 0){
// 子进程
while(1)
{
printf("我是子进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n",getpid(),getppid(),gval,&gval);
gval++;
sleep(1);
}
}
else{
// 父进程
while(1)
{
printf("我是父进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n",getpid(),getppid(),gval,&gval);
sleep(1);
}
}
}
根据运行结果我们发现一个有违我们认知的事情:父子进程,输出地址是一致的,但是变量内容不一样!既然变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,也不对啊,它们的地址是相同的啊!所以我们可以大胆推断该地址绝对不是物理地址!其实在Linux地址下,这种地址叫做虚拟地址。
1.理解地址空间
其实所谓的进程虚拟地址空间就是操作系统给进程画的大饼,操作系统(4G内存)让每个进程都以为自己有4G大小的内存空间,而实际上是让所有的进程共享这4个G的内存空间。
操作系统让每一个进程都认为自己是独占系统物理内存大小,进程之间彼此不知道,不关心对方的存在,从而实现一定程度的隔离。操作系统在管理所谓的进程虚拟地址空间时,本质是管理一个内核数据结构对象(类似PCB) mm_struct(大饼)。
2.理解区域划分
之前我们在C语言阶段接触到的内存区域划分,又是怎样划分的呢?其实在mm_struct结构体中有很多的开始、结束地址。比如栈的起始地址、栈的结束地址;堆的起始地址、堆的结束地址等等,都是以整型变量作为划分的。
3.理解地址空间上的地址
a. 地址本质上就是一个数字,可以被保存在unsigned long中。b. 空间范围内的地址,我们可以随便用,暂时不需要记录它的地址
有了上面三点理解,我们再来谈谈虚拟地址空间
每一个进程(task_struct)中都含有一个struct mm_struct* mm指针,该指针指向一个进程虚拟地址空间,创建一个进程不仅仅要创建该进程的PCB,还要为该进程创建虚拟地址空间。不管进程中间怎么玩,最终的代码和数据还是要加载到物理内存的。
当一个程序从磁盘加载到内存,该内存就会有自己的物理内存地址;在虚拟地址空间该程序也会有自己的虚拟地址。而操作系统会在虚拟地址空间和物理内存之间创建一个名为页表的东西,这个页表就记录着虚拟地址与物理地址的映射关系。
假设我们在编译器上定义gval=200;当我们取gval的地址时,我们所看到的地址就是虚拟地址!!而当我们将gval修改为100后,它对应的物理内存会发生变化,但是虚拟地址不会发生变化,只是页表中的映射关系变了!!
补充
- 关于变量和地址
变量名实际上就是地址,所以我们看到的虚拟地址是一样的,这也证明了fork的返回值id既大于0也等于0,因为我们用的页表不同。
- 重新理解进程和进程独立性
进程 = 内核数据结构(task_struct / mm_struct / 页表) + 自己的代码和数据。
a. 内核数据结构各自私有一份
b. 代码和数据也是独立的
综上,上面代码结果出现的地址相同,变量值却相同的现象,就得到了合理的解释。
我们把虚拟地址空间 + 页表叫做虚拟内存管理方案 。我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。OS负责将 虚拟地址 转化成 物理地址 。
页表
上面罗里吧嗦的说了那么多关于页表的东西,现在我们来简单了解一下页表。
权限标记位——rwx
我们在之前学习C语言的时候,只知道代码区是只读的,可是为什么是只读的呢?举个例子:
char* str = "hello world"; // 字符串常量
*str = 'H';
上述代码中的char类型的指针str指向常量区,而我们要想执行第二句代码修改常量区的值时,程序编译能过,但一运行就会报错。为什么?
能编过就说明不是编译器的问题,而这个问题是在运行时发现的问题,当代码按区域加载到内存之后,经过映射填充我们的页表,之后就会把权限带上。因为常量区的代码是只读的!所以你进程想要修改,对不起,你没有写权限就不让你修改,甚至我(OS)会将你杀死。这也是我们程序为什么会崩溃的原因。
所以上层编译器为了解决上述问题,就引入了const关键字。如果你不想内容被修改,你就加上const,这样编译器就能检查出来问题了
检查目标内容是否在内存中的标记位——isexists
假如,有一个G大小的代码文件,程序在运行的时候,内存是一次性读取全部读取呢?还是只读取被调用的代码呢?答案是只读取一部分。内存中的资源是非常宝贵的,既然你这些代码在当前这个时间里面没用,我读取你做什么,那不就是浪费资源吗。所以isexists标记位的存在就是为了分批加载、挂起等操作设定的
为什么要存在地址空间
1. 虚拟内存 + 页表可以有效保护内存。当进程经过页表访问物理内存时,如果该进程的权限不对或者访问页表时,页表中没有该进程的映射关系,那么系统就会阻止其访问,甚至将该进程杀掉!
野指针就是一个很好的例子:野指针用到的实际上是虚拟地址,当一个指针指向一块错误的虚拟地址时,该指针就变为了野指针;野指针的出现导致的程序崩溃,就是因为野指针在转化时,权限不对或者地址不存在映射关系,被系统直接杀掉了。
2. 进程(task_struct)管理和内存(mm_struct)管理在系统层面进行了解耦合。如果没有地址空间和页表,对应进程的相关数据就必须直接在物理内存中加载出来,只有物理地址的话,要申请内存,就必须malloc或者new物理地址,从而使物理内存与进程的耦合度特别高。ps. 不了解 解耦合 与 耦合度的建议百度一下。
3. 让进程以统一的视角看待物理内存,代码和数据可以加载到物理内存的任意地方,从而让“无序”变“有序”。“无序”变“有序”的意思就是,可以在物理内存随便加载代码和数据,这样虽然会导致代码和数据是无序的,但是经过页表的映射关系,在进程看来这些代码和数据,甚至是栈区、堆区都是在一起并且按照一定规则呈现的。
总结
简单总结一下
地址空间的本质就是struct mm_struct,上述所有内容都是由OS系统自动完成的。我们知道在进程PCB中有一个指针指向mm_struct,所以只要把PCB管理好了,mm_struct就管理好了。最后再问大家一个问题:为什么全局变量、字符常量等具有全局性,在整个程序运行期间都会有效?原因就是这些数据在地址空间中,随着进程一直存在!而其地址,可以被大家一直看到。