8.进程概念(四)
一、环境变量
1.基本概念
环境变量(environment variables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数。如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
2.main函数实例
测试代码如下:
#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; }
main命令行参数是实现程序不同子功能的方法。(指令选项的实现原理)
3. 一个例子,一个环境变量(重点)
1)要执行一个程序,必须先找到它!
2)系统中存在环境变量,帮助找到二进制文件。
谁找?bash通过PATH找。
PATH->系统中搜索指令的默认搜索路径。
env->查看环境变量
名称=内容
注:PATH是一个内存级的环境变量,修改PATH方法:
1)直接赋值
2)添加环境变量,采用字符串拼接的方式
1.如何理解环境变量?(存储的角度)
bash形成一张表,环境变量表
2.环境变量,最开始从哪里来的?
系统的相关配置文件中来的。
在用户的家目录下,有两个配置文件,.bashrc和.bash_profile
.bashrc依赖于/etc/bashrc的内容.bash_profile依赖于.bashrc的路径,bash_profile是拼接PATH的文件。
修改bash_profile中的PATH,重新登录时,可以看到之前添加的路径被加载到PATH中了。
4.认识更多的环境变量
HOME:当前用户的家目录
SHELL:外壳程序路径
USER:当前用户
LOGNAME:登录名称,登录时更新
HITSTSIZE:历史命令记录的最大条数(history)
HOSTNAME:当前登录的主机名
PWD:当前工作路径
OLDPWD:上一次工作路径(cd -路径切换原理)
5.获取环境变量的方法(重点)
环境变量通常具有某种特殊用途,在系统中通常具有全局属性。
1)操作
export key=value 导入和修改环境变量
unset key 删除环境变量
env 查看环境变量
echo $key 查看环境变量对应内容
2)代码
int main(int agrc, char *agrv[], char *env[]);
其中env是父进程传递的,env是存放环境变量字符串的指针数组。
如何知道main函数有几个参数的?
编译器对main函数的带的参数做语法分析,然后将参数个数给_start函数的arg_count,里面做判断,调用对应参数个数的main函数。
_start {int ret = 0;int arg_count = 3;if(arg_count==0)ret = main();else if(arg_count==2)ret = main(argc,agrv);elseret = main(argc,argv,env); }
方法1:main函数参数获取环境变量->父进程传递的环境变量->环境变量可以被子进程继承。
方法2:getenv,setenv,<stdlib.h>,根据环境变量的名称key,获取对应的内容value,获取失败返回NULL。(可以根据这个做身份的识别)
方法3:extern char **environ; <unistd.h> 全局的环境变量表指针。
6.理解环境变量的特性
环境变量具有全局特性。
bash会记录两套变量:1.环境 2.本地
两个概念:
a.变量名=value->本地变量->不会被子进程继承,只能在bash内部被使用
操作:a=10
b.我们的环境变量在谁里面?bash
操作:export a ->可以通过env看到
set 显示环境变量和本地变量
但因为a变量不会被子进程继承,那么export->内建命令(built-command)->不需要创建子进程,而是让bash自己执行->bash自己调用函数或者系统调用完成。
原因:环境变量需在当前 Shell 及其子进程中生效。若 export 是外部命令,子进程无法直接修改父进程的环境变量表(因进程间内存隔离),导致变量无法正确传递。
PATH没了,pwd,export,cd能用原因:这些命令是内建命令,只要bash能跑就能跑。
二、程序地址空间
测试代码:
#include <stdio.h> #include <unistd.h> #include <stdlib.h>int g_unval; int g_val = 100;int main(int argc, char *argv[], char *env[]) {const char *str = "helloworld";printf("code addr: %p\n", main);printf("init global addr: %p\n", &g_val);printf("uninit global addr: %p\n", &g_unval);static int test = 10;char *heap_mem = (char*)malloc(10);char *heap_mem1 = (char*)malloc(10);char *heap_mem2 = (char*)malloc(10);char *heap_mem3 = (char*)malloc(10);printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1) printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)printf("read only string addr: %p\n", str);for(int i = 0 ;i < argc; i++){printf("argv[%d]: %p\n", i, argv[i]);}for(int i = 0; env[i]; i++){printf("env[%d]: %p\n", i, env[i]);}return 0; }
地址从低到高分为:用户空间(正文代码、初始化数据、未初始化数据、堆区、共享区、栈区、命令行参数和环境变量)和 内核空间
常量字符串是硬编码到代码里的,因为代码是只读的,所以常量字符串也是只读的。
static变量就是全局变量,具有全局属性,只不过作用域是局部的。
子进程修改,父进程变量不修改,但gval地址不变->说明这个地址不是物理内存地址->虚拟地址,C/C++指针用到的地址,全部都是虚拟地址。
原因:页表,gval虚拟地址在父子进程中虽然是一样的,但是在父子进程的页表中映射的物理内存地址不一样。
虚拟地址空间(重点)
1)一个进程,一个程序地址空间
2)一个进程,一套页表
3)页表是用来做物理内存和虚拟内存映射的。
写时拷贝的触发过程:子进程修改共享的变量,会触发写时拷贝,操作系统开辟一块物理内存,将修改前的值拷贝到物理内存,然后修改子进程页表中虚拟内存对应的物理内存为这个新开辟的物理内存地址,子进程才能修改数据。
虽然父子进程的虚拟地址一样,但是映射的物理内存不一样。
虚拟地址空间形象案例:
让每一个进程都人认为自己有4G物理内存,或者,每一个进程都认为自己独占物理内存。
饼要不要管理?
要管理->先描述,再组织。
虚拟地址空间:本质就是一个数据结构,struct mm_struct
如何管理进程地址空间?
区域划分,只需要确认区域的开始和结束即可!
内核中的mm_struct:
1)在虚拟空间中申请指定大小的空间(调整区域划分)
2)加载程序,申请物理空间
物理地址转化成虚拟地址->提供给上层用户使用。
mm_struct:
1.开辟空间
2.初始化的值在哪里?->加载磁盘数据到内存的时候,进行初始化。
为什么?(重点)
1.将地址从无序变成有序
2.地址转换的过程中,也可以对你的地址和操作进行合法性判断,进而保护物理内存
a.什么是野指针?
对应的虚拟地址,查不到页表,物理内存已经被释放了,对应的虚拟地址:物理内存键值对被删除了。
b.char *str="hello world"; *str='H';为什么在字符常量区写入,就会崩溃?
查找页表时,权限被拦截了,字符常量是被硬编码到代码区的,代码区是只读的。
注:
1)我们可以不加载数据,只有task_struct,mm_struct,页表
2)创建进程,先有task_struct,mm_struct,页表,再有代码和数据。
3)如何理解进程挂起?
将页表的物理内存数据换出到磁盘的swap分区。
缺页中断:当程序访问一个尚未分配物理内存空间的虚拟地址时,操作系统会检测到这个情况,并触发一个异常,即缺页中断。操作系统通过中断服务程序来处理缺页中断。
3.让进程管理和内存管理解耦合
因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载!物理内存的分配 和 进程的管理就可以做到没有关系。若直接指向物理内存,加载代码,数据时要修改PCB内指针,若申请内存,指针可能又要修改。知识点(重要)每个分区每次申请虚拟内存不一定是挨着放的,为什么?
mm_struct内有一个struct vm_area_struct的指针,用来维护一个进程中程序申请的所有虚拟内存的链表。记录了虚拟内存的开始和结束,以判断其分区。虚拟空间的组织方式有两种:1)当虚拟区较少时采取单链表,由mmap指针指向这个链表;2)当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树。linux内核使用 vm_area_struct 结构来表示⼀个独立的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使用多个 vm_area_struct 结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是 vm_area_struct 结构来连接各个VMA,方便进程快速访问。
虚拟内存管理形象图:
为什么要写时拷贝?1.减少创建子进程的时间2.减少内存浪费
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证!写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率。数据段只读->修改了,违背权限,触发写时拷贝->数据段可写。