Linux环境变量以及进程虚拟地址原理
目录
一、介绍进程优先级
1.什么是优先级
2.为什么会有优先级
3.Linux中的优先级是怎么确定的
1)查看Linux中的优先级
2)计算优先级和更改优先级
二、环境变量
1.什么是环境变量
2.环境变量有什么作用
3.环境变量怎么做到的
1)查看系统已有的环境变量
2)以PATH具体认识环境变量
引入
①PATH环境变量的作用
②查看PATH中的存储的地址
3)介绍一些其他的环境变量
4)set
①普通全局变量与环境变量的区别
4.如何通过代码获取环境变量
1)main函数的命令行参数
①int argc和 char *argv[ ]
②char *env[ ]
2)getenv接口获取
3)extern char ** environ
4)一些奇怪的问题
①操作系统是如何得知你是否有某文件权限的?
②我们说非环境变量不能被子程序获取,那shell里定义的全局变量为什么能被echo读取并打印?
三、进程地址空间
1.引出问题
2.逻辑地址、虚拟地址、物理地址的概念
3.内核数据结构mm_struct
4.页表:虚拟地址到物理地址的映射
5.写时拷贝
6.再一次理解虚拟地址:从编译到执行
7.总结进程地址空间
1)进程地址空间是什么
2)为什么会存在逻辑地址、虚拟地址、物理地址三个地址而不是一步到位?
3)虚拟地址是如何做到的
总结
一、介绍进程优先级
1.什么是优先级
简单来说优先级是一种约定,进程都有优先级,优先级决定一个进程何时运行和接收多少 CPU 时间——优先级高的先做,优先级低的后做。
往简单说,优先级就是进程PCB中存储的一个数字,越小的优先级越高(跟成绩排名一样)
注意:
2.为什么会有优先级
系统中运行的进程可由用户决定多少,但硬件的数目却是电脑出厂后就决定的。
简单来说就是内存中的进程数目远多于硬件等资源的数目,导致的资源不足。所以引入了优先级这个概念以确保计算机的顺利运行
3.Linux中的优先级是怎么确定的
1)查看Linux中的优先级
ps -la指令可以查看目前所有用户的进程的信息,如优先级等。
2)计算优先级和更改优先级
优先级= PRI + NI
系统中的优先级是看pri决定的,pri越小越先被执行,但用户可以通过更改ni的值来改变进程的优先级。
你可以输入任意大小的数字,但操作系统只允许ni的值位于[ -20,19],多出的部分舍弃。
所以Linux中进程的优先级共有40个等级:[ 60 ,99]
二、环境变量
1.什么是环境变量
具有全局属性,就是可以被子进程继承下去(有些类似于C中的局部变量与全局变量的关系)
每当用户登录时,操作系统便会默认初始化系统中的环境变量,环境变量本质上就是一串字符串。
2.环境变量有什么作用
如环境变量HOME,当用户登录上Linux后,系统便通过识别用户名以初始化HOME环境变量,进而导致不同身份的用户登录家目录不同:
3.环境变量怎么做到的
1)查看系统已有的环境变量
env指令:在屏幕上打印出系统已有的环境变量
节选的一些
2)以PATH具体认识环境变量
引入
通过file pwd等指令,再file 我们自己写的程序,发现都是x86_64可执行程序(是的,指令也就是写好的程序),可为什么我们的程序不能像系统指令一样执行,而是得加" ./ "呢?
①PATH环境变量的作用
②查看PATH中的存储的地址
echo $PATH
如果我们将自己写的程序的地址加入PATH后,是不是就可以像使用指令一样在任何目录都可以调用我们的程序?
注意:
猜想是正确的!
此时再查看PATH中记录的地址发现有myproc程序的地址:
当我们重新登入Linux后,发现PATH的值又恢复初始状态:
3)介绍一些其他的环境变量
4)set
这里的全局变量就是C语言中的局部变量,环境变量就是全局变量,区别是这个全局变量只能在创建它的程序中使用,如下图中的hello,而环境变量可以继承给子进程。
①普通全局变量与环境变量的区别
在shell中定义一个普通全局变量hello=10,在set中可以看到hello。
但在环境变量中查找不到hello
通过export将hello变为全局变量后即可查找
②删除环境变量或者普通全局变量
关于环境变量相关的一些指令总结:
① echo: 显示某个环境变量值,如echo $PATH
② export: 设置一个新的环境变量,如export hello(自定义的)
③ env: 显示所有环境变量
④ unset: 清除环境变量
⑤ set: 显示本地定义的shell变量和环境变量
4.如何通过代码获取环境变量
1)main函数的命令行参数
不知你是否曾在某些书本当中见过main函数是带参数的,如下:
现在让我们来详细了解这些参数
①int argc和 char *argv[ ]
int argc的作用是存储在调用该程序时的选项数目;
而char *argv存储的则是每一个额外选项。
比如我们常用的ls -al—>ls -a -l,在包括ls指令本身后argc=ls + a + l ==3,而char *argv[ ]则存储着这些选项。
我们用自定义的test_main.c来观察argc和argv[ ]。
若gcc编译不通过,则按提示在gcc后添加-std=c99:
在调用test_main时我们附加了-a -b -c三个选项,如上图。可以看到被成功打印出。
②char *env[ ]
在有了对argc和argv[ ]的理解后,再来理解env[ ],便容易的多。
首先要明确的是:每个程序在运行时都会收到操作系统给的一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。而char * env[ ],便是那张环境表。
2)getenv接口获取
作用:在程序内通过代码获取环境变量。
3)extern char ** environ
直接在代码中额外声明char ** environ环境变量表,依然可以打印出环境变量。
小结:
在程序中获取环境变量有三种方式
①getenv;
②char *env[ ];
③extern char ** environ;
4)一些奇怪的问题
①操作系统是如何得知你是否有某文件权限的?
②我们说非环境变量不能被子程序获取,那shell里定义的全局变量为什么能被echo读取并打印?
如上图中所示:
已知:myenv是个shell定义的普通全局变量,echo是一个指令(本质上也是一种程序),当在shell运行echo时本质创建了一个子进程,但按照环境变量的定义这应该是不被允许的。
echo能读取非环境变量的本质:
三、进程地址空间
1.引出问题
我们编写一段代码,定义一个全局变量glo_x=100,再通过fork调用生成一个子程序。
我们尝试在子进程中修改glo_x,而父进程什么都不做,两个再打印glo_x会有什么现象。
1 #include<stdio.h>2 #include<stdlib.h>3 #include<unistd.h>4 int glo_x=100;5 int main()6 {7 pid_t id=fork();8 int cnt=1;9 if(id<0)10 {11 printf("创建子进程失败\n");12 exit(1);13 }14 else if(id>0)15 {16 //父进程17 while(1)18 {19 printf("我是父进程,我的id是:%d,glo_x=%d,&glo_x=%p\n",getpid(),glo_x,&glo_x);20 sleep(1);21 }22 }23 else24 {25 //子进程 26 while(2)27 {28 printf("我是子进程,我的id是:%d,glo_x=%d,&glo_x=%p\n",getpid(),glo_x,&glo_x);29 cnt++;30 if(cnt==3)31 {32 glo_x=700;33 printf("子进程已经修改了glo_x的值…\n");34 }35 sleep(2);36 }37 }38 return 0;39 }
结果如图:
此时,同一个地址的glo_x变量居然有两个值!!
2.逻辑地址、虚拟地址、物理地址的概念
在学习C语言时,我们认为内存划分为这么几个区域:代码段、数据段、堆区、栈区,如下图所示
上面的理解是正确的但不完全正确,真正理解进程地址空间还需要理解:逻辑地址、虚拟地址、物理地址这三种地址之间的区别与联系。
1)逻辑地址
①逻辑地址是程序视角下的地址空间;
②编译器编译的时候是从0开始编址的;
③代码中的指针值就是就是逻辑地址;
④逻辑地址空间存在于进程当中,每个进程都有自己的逻辑地址空间。换言之,每个进程认为自己独享整个系统的资源。
2)虚拟地址
①虚拟地址是站在cpu的视角的地址空间
②经过分段机制转换后的地址,是分页机制的输入。
由于技术的进步,在现代操作系统中,逻辑地址几乎等同于虚拟地址。
3)物理地址
真实内存硬件上的地址,CPU 最终通过总线访问的地址,内存访问的最终目标。
所以不管是在编译器中调试看到的逻辑(虚拟)地址,还是物理地址,都是用32位二进制来描述这2^32个地址。
从程序到物理内存的地址转换分为两个阶段:
逻辑地址 → 虚拟地址 → 物理地址,由于现在的操作系统中逻辑地址几乎等同于虚拟地址,为方便理解,我们暂时认为是从虚拟地址→ 物理地址。
3.内核数据结构mm_struct
通过上面三种地址空间的描述,我们可以得出一个信息:就是进程都认为自己独占整个系统资源,并且编译器在编译时也是从地址0开始编译。但是内存中的进程不只一个,肯定不可能让一个进程独占,且物理地址只有一条,让所有进程都从0地址开始编址也显然不可能,并且各个进程中代码段需要多少内存,数据段需要多少内存,堆栈又各自需要多少内存又是不尽相同。
于是,操作系统为了方便管理,创建了另一种内核数据结构mm_struct。
操作系统管理的核心思路是:先整理,在管理。
先整理:mm_struct中的数据成员正是代码段、数据段、堆栈的起始地址与终止地址。
再管理:通过为每一个进程创建一个mm_struct对象,直接管理各个进程所需的内存。
所以,在代码中的malloc 或者 new其实就是mm_struct中的数据更改变动,本质上就是start 或者end的更改。
注意:
故下图其实描述的是进程的虚拟(逻辑)地址空间:
4.页表:虚拟地址到物理地址的映射
在已经知道虚拟地址的概念即如何通过mm_struct存储后,那么操作系统是如何通过虚拟地址精确找到物理地址的呢?
这里笔者画了个草图以帮助读者理解从虚拟地址——>物理地址的转换。
同样的从物理地址到虚拟地址也要经过页表的转换。
值得注意的是:
5.写时拷贝
现在让我们回到“引出问题”中观察到的现象——为什么同一个地址的glo_x却有两个值。
还记得fork()调用的作用吗,fork调用父子进程会共享之后的代码。这里再给出写时拷贝的概念:
6.再一次理解虚拟地址:从编译到执行
1)编译器从0地址开始编址逻辑(虚拟地址)地址,若是32位环境下则每一位地址由32位二进制表示。64位同理;
2)当编译好的程序加载到内存后又获得物理地址;
3)操作系统将进程的虚拟地址与物理地址填入页表中;
4)当调用该进程的执行代码时:
①cpu通过查看task_struct(PCB)中的mm_struc,得到虚拟地址,然后前往页表查询物理地址;
②得到物理地址后,再前往物理地址中寻找该进程的代码和数据;
③得到具体的可执行代码和数据后再带回cpu内计算。
7.总结进程地址空间
1)进程地址空间是什么
进程地址空间 是指一个进程在运行时所能“看到”和使用的所有内存地址的集合。其核心内容就是虚拟地址。操作系统通过让每个进程都认为自己独占整个内存资源,实际物理内存由操作系统动态分配和管理。
2)为什么会存在逻辑地址、虚拟地址、物理地址三个地址而不是一步到位?
如果只存在一个地址,也就是让进程直接访问物理内存会有诸多隐患:
①防止进程越界非法操作(野指针),保证操作系统的安全;
——使用页表进行映射能有效拦截进程的非法请求
②防止恶意程序扫描内存而导致的数据泄露;
③减少内存碎片化,如果进程必须占用连续的物理内存,会是内存碎片化加剧:
比如:若物理内存剩余 1GB 但碎片化为多个 100MB 块,则无法运行需要 500MB 的程序。
整体是以空间换安全的设计思想。
3)虚拟地址是如何做到的
操作系统通过页表映射,将进程的虚拟地址与物理地址联系起来,以实现每个进程的虚拟地址空间。
总结
本文先介绍了进程的优先级,以及Linux中的优先级是如何确定的,之后介绍了什么是环境变量及其作用,最后介绍了什么是地址进程地址空间、mm_struct,以及为什么要使用虚拟地址。
笔者水平浅薄,难免有疏忽大意的地方,若有错误还请读者多多指出。
文章读完可否给笔者一个免费的赞呢,笑(*^_^*)。