Linux --环境变量,虚拟地址空间
环境变量
平时我们在写函数的时候都会有参数列表,main函数也是一个函数,那么它也有自己对应的参数可以传递,只是在我们一般写代码中不需要传递,只有在命令行中才可能需要传递使用。main函数一般有三个参数可以传递
int main(int argc,char* argv[],char* env)
那么设置main函数参数的意义是什么,同一个程序中可以根据命令行参数,根据选项的不同表现出不同的功能,一些命令指令就是最好的体现,比如 ls -a mkdir -p这些后面都会带参数,实现不同的功能。argc是代码数组中argv char类型指针数组的参数个数,而env是环境变量,一会我们会讲解。这里用一个进程来说明程序传不同的参数实现不同功能。
int main(int argc,char* argv[])
{if(argc == 1)return 1;else {if(strcmp(argv[1],"-opt1") == 0)cout<<"功能1开启"<<endl;if(strcmp(argv[1],"-opt2") == 0)cout<<"功能2开启"<<endl;if(strcmp(argv[1],"-opt3") == 0)cout<<"功能3开启"<<endl;}return 0;
}
当我们在命令行输入以后,shell会将我们输入的字符串打乱成为一张表,这个表就是 argv数组,其中每个元素都是字符串,第一个是进程本身,最后一个是NULL,然后再再统计个数传递参数给我们的进程,也就是shell的子进程,然后在这个进程中就很明显的说明了根据不同的命令参数我们可以实现进程不同的功能。第三个参数env也是一个指针数组,它里面存的是环境变量,这些环境变量是"全局变量",会被继承到每一个子进程中,所以处了shell的每个进程都会有环境变量。我们也可以在代码中查看这些环境是什么,只需要循环env数组打印直到最后一个元素是空即可。 我们也可以在命令行中输入 env查看环境变量
for(int i = 0;env[i];i++){cout<<env[i]<<endl;}
我们进程每次启动都需要输入一个./+进程名,而系统自带的指令如ls rm等就不需要,只是因为环境变量中的PATH指定了搜索路径去查找进程名,而我们可以自己去修改PATH达到只需要输入进程名就可以启动的效果。我们可以用echo $PATH查看环境变量,用$PATH=$PATH:(你需要添加的路径)
可以看到我已经将path路径修改了并且code执行也不需要加./,不过现在的环境变量是一个内存级的,也就是说重启shell也会就会恢复原来的环境变量。那么原来的环境变量shell是怎么获取的?在我们的家目录下有两个系统的配置信息,shell启动时会从这里读取并加载环境变量。
之所以说环境变量是"全局变量",因为shell在创建进程的时候会把env也就是环境变量这个数组"继承"下去,子进程以及一些文件的属性就是从环境变量中获取的,其中USER变量对应着创建者,也就是说一个文件创建就知道是谁创建了它。
我们在命令行中也可以创建变量,例如a=100,这种是本地变量,是不会加载到子进程中的,而可以通过export设置一个新的环境变量。我们可以通过set查看本地变量和环境变量,unset可以清楚环境变量。
虚拟地址空间
这是一张空间布局,为什么我不是说的内存空间布局,因为这确实不是,这是Linux中一种虚拟的进程空间,每个进程都会有一个和内存一样大的虚拟空间。通过以下代码我们就可以得出结论
pid_t id = fork();if(id == 0){gav+=10;cout<<"gav = "<<gav<<endl;cout<<"子进程 pid: "<<getpid()<<" gav的地址是: "<<&gav<<endl;}else {cout<<"gav = "<<gav<<endl;cout<<"父进程 pid: "<<getpid()<<" gav的地址是: "<<&gav<<endl;}
这里就出现了一个奇怪的现象,变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量 但地址值是⼀样的,说明,该地址绝对不是物理地址! 在Linux地址下,这种地址叫做 虚拟地址 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀管理,OS必须负责将 虚拟地址 转化成 物理地址 ,所以就出现了同一个地址但是出现了两个值。以下这张图标能说明同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
这个虚拟的地址空间其实是一个叫做mm_struct结构体,这个结构体之中存在在一些long类型的start和end变量,代表栈,堆等这些数据的虚拟地址范围,然后页表中的虚拟地址会对应物理地址。当一个子进程被创建时会拷贝父进程的这个结构体,父子进程代码段都是储存在同一个物理空间的,数据内容也是,只有当子进程进行修改时才会开辟一个新的物理空间储存这个数据,页表中对应的物理地址也会更改,这是一种写时拷贝的方法。
mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:
内核中的代码:
struct mm_struct{/*...*/struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */struct rb_root mm_rb; /* red_black树 */unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*//*...*/// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;/*...*/
}
虚拟地址空间也是一个结构体,那么它自然也需要初始化,它初始化会根据可执行程序中带有的信息来进行。可执行程序在生成的时候不只有编译信息,还包含着属性和分段信息,它代表了数据代码等所属于的范围以及读写执行权限 。
页表
页表中不仅仅有虚拟地址和物理地址,还有一些特殊的标志位,常见的有权限位和isexist。页表中的权限位代表了该地址的读写执行权限,如果是只读地址的文件用户进行写入那么进程就会被杀死,注意这里说的是用户,因为这里涉及到写时拷贝。当一个子进程在加载到内存中时,它的虚拟进程空间和页表会拷贝父进程的,此时它们代码和数据都是指向同一个地方的,此时数据和代码都是只读的。当子进程或者父进程需要修改数据进行写入时,会触发缺页异常,系统会判断该地址是否是数据或者还是只读区域的代码,然后将数据的只读属性改为可读写,再给需要写入的数据分配新的物理空间,修改数据。所以进程之间数据在修改之前也是共享的!!!但是如果用户对只读地址进行写入时就会报错,例如char *str = "hello";初始化字符串以后是不允许再去修改的。
页表中的另一个标志位isexist的作用是告诉系统该虚拟地址的物理地址是否存在,在页表中有的虚拟地址是还没有对应的物理空间的,就例如Malloc以后的空间在不使用之前其实还是没有的开辟空间,只有真正需要使用的时候才会申请空间,这个设计就大大提高了内存的使用率。
虚拟内存管理的原因
为什么需要使用一个额外的虚拟地址空间,这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的, 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。 那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存 ⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分 出110M分配给程序B。这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。
安全⻛险 :每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
地址不确定 :众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷⻉ 的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了。
效率低下 :如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内 存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷⻉时间太⻓,效率较低。
有了虚拟地址空间和分⻚机制就能解决了上诉问题
1.为了保护系统,防止用户对物理地址空间的滥用和随意使用导致系统的崩溃,就像系统调用一样封装了对硬件调用的接口但是不允许用户直接访问,地址空间和⻚表是OS创建并维护的,也就意味着,凡是想使⽤地址空间和⻚表进⾏映射,也⼀定要在OS的监管之下来进⾏访问!!也顺便 ,包括各个进程以及内核的相关有效数据!
2.因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置的加载!物理内存的分配和进程的管理就可以做到没有关系, 进程管理模块和内存管理模块就完成了解耦合
3.因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问的时候,才执⾏内存的相关管理算法,帮你申请内存,构建⻚表映射关系(延迟分配),这是由操作系统⾃动完成,⽤⼾包括进程完全0感知!!
4.因为⻚表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射,在进程视⻆所有的内存分布都可以是有序的。