【Linux】进程概念 —— 虚拟内存地址空间
目录
一.进程地址空间
1.进程地址空间分布图
2.验证上述进程地址空间
3.Linux vs Windows
二.了解虚拟内存地址空间
0.通过代码引出虚拟内存地址空间概念
1.什么是虚拟内存地址空间
2.虚拟内存地址空间的设计方式
三.深入虚拟内存地址空间
1.虚拟内存地址空间与物理内存之间的映射
2.一个程序经过编译器到进程运行时的过程(帮助理解虚拟与物理之间映射关系)
3.虚拟内存地址空间存在的价值
四.重新理解fork函数
1.为什么会有两个返回值
2.为什么会有一个变量同时接收了两个返回值
3.写时拷贝
4.父子进程之间的关系
5.fork如何实现的调用fork之后父子进程代码共享
一.进程地址空间
我们先不谈虚拟内存与物理内存,当一个进程运行起来时,它的地址空间分布是什么样的呢?
1.进程地址空间分布图
2.验证上述进程地址空间
在Linux环境下验证!
代码:
#include<stdio.h>
#include<stdlib.h>int g_val1 = 10;
int g_val2 = 20;int g_val3;
int g_val4;int main(int argc, char* argv[], char* env[])
{//任务:验证进程地址空间//代码区printf("code address:%p\n", main);//只读常量区const char* p1 = "hello";printf("only read:%p\n", p1);const char* p2 = "hello world";printf("only read:%p\n", p2);//全局区printf("init global g_val1:%p\ninit global g_val2:%p\n", &g_val1, &g_val2);printf("uninit global g_val3:%p\nuninit global g_val4:%p\n", &g_val3, &g_val4);//堆区char* tmp1 = (char*)malloc(10);char* tmp2 = (char*)malloc(10);char* tmp3 = (char*)malloc(10);char* tmp4 = (char*)malloc(10);printf("heap:%p\n", tmp1);printf("heap:%p\n", tmp2);printf("heap:%p\n", tmp3);printf("heap:%p\n", tmp4);//栈区printf("stack:%p\n", &tmp1);printf("stack:%p\n", &tmp2);printf("stack:%p\n", &tmp3);printf("stack:%p\n", &tmp4);//命令行参数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;
}
3.Linux vs Windows
1).以上对于进程地址空间的验证,是在linux环境下.
在windows下会跑出不一样的结果,因为windows基于安全考虑,做了特殊处理
二.了解虚拟内存地址空间
0.通过代码引出虚拟内存地址空间概念
代码:
代码描述:
1.先创建一个全局变量g_val,并且初始化为100
2.创建子进程后,在子进程内部将g_val改为200
3.观察父子进程的g_val值以及g_val地址
#include<stdio.h>
#include<unistd.h>int g_val = 100;//全局变量int main()
{pid_t id = fork();if(id == 0){//子进程g_val = 200;printf("I am child, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val);}else{//父进程printf("I am father, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val);}return 0;
}
结果:
结论:
我们惊奇的发现,父子进程所打印的g_val的地址是一样的,但是!!里面却存了不同的值!!!
(注意一点:我们这里所打印的地址,是进程运行起来之后,由进程内部的printf系统调用所打印出来的,所以本质打印的这个地址是动态的)
为什么同一块空间,里面存放的不同的值呢?
经过进一步思考与猜测,那就是这个地址不是真实的物理地址! 而是虚拟地址!
在后面会给出对于这一现象的详细解释
1.什么是虚拟内存地址空间
现象:
每一个进程,都拥有自己的虚拟内存地址空间
虚拟内存地址空间的本质,就是我们上面验证的那个进程地址空间,这俩是一个东西
每一个进程都不知道外部其他进程的存在,都认为自己独享物理内存,并且拥有4G的空间
但实际上,并不是真正拥有4G内存,而是按需分配物理内存!
类比:
其实,这本质不就是老板给员工画的一张饼吗?
我们假设的认为:
操作系统 --- 老板
进程 --- 员工
虚拟内存地址空间 --- 老板给员工画的饼
物理内存 --- 真正的好处(例如:真的升职了,真的加薪了)
可以看出,虚拟内存地址空间只是操作系统给进程许诺,你拥有了全部的4G内存,但这仅仅只是是许诺而已,并没有执行!
而实际情况则是按需所取,如果进程过分索取内存的话,操作系统有权利拒绝!
这便可以很形象的表现出这四者之间的关系!
2.虚拟内存地址空间的设计方式
我们都知道,管理的本质是先描述,再组织
例如我们学过的进程,进程是如何被管理起来的呢?
答案是:先描述,再组织. 先通过进程控制块(PCB)(在linux中的具体表现形式:task_struct)将进程描述起来,然后将这些PCB以某种数据结构的形式组织起来,这样操作系统便将所有的进程管理了起来!
那么,虚拟内存地址空间是如何设计的呢?
以及它在linux中的具体表现形式是什么?
它是如何被管理的?
1.虚拟内存地址空间是如何设计的
实际上,虚拟内存地址空间的本质就是一种数据结构,并且将来要和特定的进程关联起来.
这种数据结构在linux中表现为mm_struct
与特定的进程关联起来是某个进程PCB中有一个指针指向mm_struct
2.mm_struct的本质是什么(大概了解)
其内部会有区域划分(这只是一部分)
3.对于虚拟内存地址空间的管理
先描述,再组织
先描述:通过mm_struct描述起来,mm_struct就是一个结构体,这个结构体内部包含各种数据结构,各种边界判定
再组织:进程PCB有一个指针指向mm_struct,便将进程与其虚拟地址空间一一对应起来,同时mm_struct内部有很多数据结构也能够自行组织
三.深入虚拟内存地址空间
1.虚拟内存地址空间与物理内存之间的映射
虚拟内存与物理内存映射图
2.一个程序经过编译器到进程运行时的过程(帮助理解虚拟与物理之间映射关系)
1).我们写的一个程序,在编译器编译成功之后,形成了一个.exe可执行文件(这是一个二进制文件)并且存储在磁盘上
提问:虚拟地址空间中的地址从何而来?磁盘中的.exe文件有没有地址呢?
答:虚拟地址空间中的地址,是编译器给的! 编译器在编译的时候,就已经为每一行代码都编好了地址(这个编好的地址是每一行代码的地址,是代码区的,并不是堆栈,堆栈在进程运行时才产生),并且在形成.exe文件的时候,代码/地址,此时已经全部存在于.exe文件中.
2).当我们准备执行这个.exe文件时,点击执行的那一刹那,程序就正式成为了一个进程,此时操作系统会为这个进程创建虚拟内存地址空间与页表,并且加载部分数据到物理内存,然后通过页表将虚拟内存与物理内存映射起来
2).0).创建虚拟内存地址空间时,会先将.exe文件中编译器给编好的地址放到代码区和全局区,然后在执行过程中,在生成对应的堆区和栈区
2).1).cpu读取/分析/执行指令时,会先去找虚拟地址,然后通过页表映射关系,找到物理地址
2).2).一些运行时的报错:比如说对只读变量做了修改,或者对野指针进行访问的非法操作
这些报错是谁报出来的?硬件,比如内存:其实是随时都仅仅只是简单的读写操作,并没有监管这么一说,那实际的报错,其实是
操作系统的介入,在页表映射的时候会记录有映射到这个物理内存空间中的权限,从而操作系统就可以根据权限来做出判断
3).当实际访问的时候,发现对应的物理地址没有数据或者代码,就直接发生缺页中断,此时操作系统执行页面调入与内存管理算法,完成之后,进行填充页表,完成映射
重点提取:
0.程序编译好生成.exe文件存在磁盘上,此时.exe内已经有地址了
1.编译器给程序生成了虚拟地址(代码区/全局区)
2.cpu执行的是虚拟地址
3.物理内存只需要按需载入
4.物理内存很傻,只是随时随地可以读取,而是在页表映射中给相应的物理内存空间赋予权限
3.虚拟内存地址空间存在的价值
从安全的角度来看: 有效保护物理内存
如果直接使用物理内存,必然会发生进程A可以访问或修改进程B的数据,甚至可以影响内核程序
使用虚拟内存然后映射到物理内存这一机制,使得每个进程彼此互不干扰甚至不知道对方的存在
同时由于中间层(虚拟内存与页表)的介入,使得可以直接拒绝(通过OS)一些危险操作
从而OS可以杀掉一些危险进程,起到保护其他进程与OS内核的作用
从管理模式的角度来看: 完成了对于进程管理模块与内存管理模块的解耦合
什么是解耦合:
如果我将所有代码都写到main函数内,这就是高耦合
如果我将代码分功能分模块的写到不同函数内,然后在main函数中只是调用这些函数,这就是低耦合
所谓的耦合,就是关联性的意思
虚拟内存地址空间 --- 只负责进程管理
物理内存 --- 只负责内存管理
两者互不干扰
也就是说,代码和数据被加载到物理内存上时,并不用考虑区域的问题,而是随意加载物理内存的任意位置
无论加载到哪里,都可以让虚拟地址通过映射关系,找到所对应的所在物理内存上的位置
虚拟内存地址空间+页表 可以将在物理内存中乱序的指令有序化(因为虚拟地址空间负责进程管理,它是有序的)
从效率的角度来看: 避免了不必要的空间浪费
对于空间浪费的认识:
如果我说申请了多余的空间,毫无疑问这肯定是浪费了空间
而如果说,我申请了一块空间,但这块空间没有很及时的被使用,这依旧也造成了空间浪费
有了虚拟内存地址空间与页表的介入之后,如果我们申请了一块空间,这样的申请只能说是一种约定,相当于告诉物理内存
我之后要使用这块空间,物理内存知道了,但没有立刻开辟出来,而是当我们在使用这块空间时,发现实际并没有这块空间,此时
就会发生缺页中断,然后分配物理内存,建立映射关系,再继续执行
这种延时分配的策略,提高了整机的效率,使内存的利用率几乎是100%
以上这些逻辑是由操作系统自动完成,用户不会感知到,并且对于用户来说也无需担心!
从进程独立性的角度来看: 真正意义上的实现了进程之间互不干扰,互相独立
由于每个进程都拥有自己的虚拟内存地址空间与页表,并且当cpu在执行时,总是执行虚拟地址再找到物理地址,所以对于这个 进程来说,它是不知道外部世界的,因为cpu根本不会访问外部地址(物理地址),总是通过进程内的虚拟地址找到对应的物理地
址
从进程挂起的角度来看: 实现了对进程分批加载分批换入换出,进而提高硬件的上限
既然可以实现延时分配内存的策略,自然也就同样可以支持分批换入换出操作
(本质都是当实际映射到的物理内存空间不存在时,通过缺页中断,来执行页面调入和内存管理算法,填充页表,完成映射)
现实生活中的例子: 当前时代的游戏,随便就有几十G大小,而我们的内存只有4G/8G/16/32G...(内存的成本非常高)
如何让高达几十甚至几百G的游戏运行起来?
很显然将几十/几百G全部加载到内存,成本消耗巨大,普通人是承受不起的,这时发现我们8G的电脑都是可以运行并且
很流畅的,这就应用到了OS对进程分批加载,并且将需要的部分换入,不需要的部分换出.让8G的内存达到了几十G的效果
如果这个进程短时间内不会被执行,就叫做阻塞
进程的数据和代码被换出,就叫做挂起
所以进程阻塞之后,进程的代码和数据就会被挂起!!!!!!
四.重新理解fork函数
1.为什么会有两个返回值
fork是系统调用,fork函数内部会创建子进程,当fork函数准备return时,子进程便已经被创建出来了,子进程被创建出来之后的代码父子共享(原理在第5点会介绍),此时父进程需要return,而刚刚被创建出来的子进程也要执行return,给子进程返回0,给父进程返回子进程的pid
2.为什么会有一个变量同时接收了两个返回值
我们知道了为什么表面上同一个函数为什么会有两个返回值之后
那么同一个变量,是如何接收两个不同的返回值,并且还能同时作出判断的呢???
这在我们不知道虚拟内存地址空间之前,是无法理解的
现在我们便可以给出合理并且正确的解释:
1).当我们创建出子进程,子进程会拷贝(或继承)父进程的虚拟地址空间
(为什么会继承父进程的虚拟地址空间呢? 因为在编译器编译的时候,只编译了父进程,给父进程生成了.exe文件,并且给每一行代码都进行了编址,那么在父进程运行时,子进程凭空出现,自己成的虚拟地址又怎么样让它生成呢?直接简单粗暴,拷贝父进程的!)
2).假设接收返回值的变量命名为id,这时子进程和父进程对于id这个变量拥有相同的虚拟地址(因为子继承的父),并且通过映射指向的是同一块物理内存空间
3).当第二次进行对id的写入时,会通过虚拟地址然后通过映射机制找到物理内存,并且知道这一次写入的(第二次写入)是父还是子,发现第一次已经写入过一次了,所以第二次就会重新为其开辟新的空间并且建立新的映射关系,此时父子表面上对于id的虚拟地址是相同的,但是实际指向的物理内存却是两块不同的空间
画图解释:
3.写时拷贝
创建子进程时只需要将未来会写入的数据再重新拷贝一份
但是操作系统并不知道哪些数据将来会被写入,即便是知道了提前就开辟好一块新的空间并且拷贝,这样长时间占用内存却不使用的情况也会造成空间浪费
所以操作系统所采用的方法是写时拷贝,只有当数据被写入时,才会发生拷贝
这一理念在C++的深浅拷贝中也有所体现!
4.父子进程之间的关系
1).子进程被创建出来时,会拷贝父进程的虚拟地址空间
2).共享子进程被创建完之后的代码(fork之后),也就是父子都从那一刻同时执行之后的代码
3).父与子是两个独立的进程,具有独立性,不相互影响不相互依赖
4).代码是只读的,所以代码层面上的共享,对于他俩的独立性而言没有影响
5).数据是可以被修改的,所以必须分离
5.fork如何实现的调用fork之后父子进程代码共享
由于进程具有并发性,所以随时有可能中断,所以必须要有多个硬件(寄存器)来存放当前进程执行到的数据与状态(例如进程执行到的位置),以便将来用于继续执行还未执行完的进程,这种寄存器内,存储的数据称为:进程的上下文数据
其中EIP寄存器,就是用来存放cpu将要读取的下一条指令的地址
创建好子进程之后,子进程认为从EIP开始执行,所以就会从父进程的EIP记录处为起始,开始继续向下执行指令
【Linux】进程概念 —— 虚拟内存地址空间相关推荐
- 深入理解Linux进程概念
Linux进程概念 冯·诺依曼体系结构: 冯诺依曼体系结构作为现代计算机硬件体系结构,规定了现代计算机应该具有哪些硬件单元 输入设备 :键盘,鼠标,图像采集设备 ,声音采集设备 输出设备:显示器 ...
- 实验五 显示进程的虚拟内存地址空间分布信息
实验五 显示进程的虚拟内存地址空间分布信息 目录 实验五 显示进程的虚拟内存地址空间分布信息 实验环境 一.实验目的 二.实验内容 三.实验步骤 四.实验总结 实验环境 操作系统版本:ubuntu-1 ...
- 【Linux从青铜到王者】第五篇:Linux进程概念第一篇
系列文章目录 文章目录 系列文章目录 前言 一.冯诺依曼体系结构 二.操作系统 1.操作系统的概念 2.操作系统的目的 3.操作系统的定位 4.如何理解管理 5.操作系统总结 6.系统调用和库函数概念 ...
- Linux打怪通关攻略(16)Linux 进程概念
文章目录 Linux 进程概念 概念理解 进程的属性 进程的分类 进程的衍生 进程组与 Sessions 工作管理 Linux 进程概念 概念理解 首先程序与进程是什么?程序与进程又有什么区别? 程序 ...
- 【Linux进程概念】 (4)进程地址空间
我们在编写C代码时往往出现一个错误就是变量地址的非法访问和使用,如果此时的地址对应内存中真实的物理地址,那么我们造成的多数错误可能会导致系统崩溃.所以关于地址空间的分布不是我们想象的那么简单,接下来我 ...
- Linux进程概念4 程序地址空间
一. 程序地址空间 此时我们的研究是在kernel 2.6.32和32位平台进行说明 1. 程序地址空间图 我们在学习C/C++时候,应该学习过内存布局,以及了解各种变量的存储位置,例如局部变量存储在 ...
- 【Linux进程概念】冯 诺依曼体系结构 操作系统 进程 fork 进程状态 优先级
文章目录 [写在前面] 一.冯 ? 诺依曼体系结构 ?? 体系结构 ?? 数据流向 ?? 实例 二.操作系统 (Operator System) ?? 概念 ?? 计算机体系及操作系统定位 ?? 管理 ...
- 【Linux】四、Linux 进程概念(上篇)
目录 前言 一.冯诺依曼体系结构 1.1 冯诺依曼体系结构是什么 1.2 冯诺依曼体系结构为什么这么设计 1.2.1 思考 1.2.2 了解一下计算机的存储分级 1.2.3 解释 1.3 往下要明确几 ...
- Linux ——进程的虚拟地址空间,逻辑地址和物理地址,进程管理命令
进程的虚拟地址空间 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,虚拟地址空间的大小由计算机的硬件平台决定,比如32位的平台决定了虚拟地址空间为4G(因为32位系统上指针能够寻址 ...
最新文章
- 深度学习——数据预处理篇
- Vim 键盘指令高清图
- python打地鼠游戏代码_妈妈和宝宝在家,自己做了个打地鼠游戏,网友:宝宝笑得好开心...
- ubuntu14.04下通过.frm, .MYD,.MYI文件恢复建立mysql数据库
- 虚函数实现多态---C++
- Microsoft Edge 浏览器开始支持webkit私有样式
- UI设计干货模板|输入框设计临摹素材
- ZooKeeper学习第一期---Zookeeper简单介绍
- .net remoting 使用事件
- 微软在线实验室启用谷歌的reCAPTCHA,我们又丢失了一个好东东
- pmp 第六版 模拟卷1疑难问题
- 支付:在线支付功能的概述
- PAT乙级-1028人口普查
- 【190302】VC+ 视频捕捉与录像+实例源码源代码
- 学习Python的第七天
- Objective-C学习笔记(二)——OC基本语法概述
- 超详细面试准备(10分钟打遍所有初级后端开发面试)
- 从前慢-Mysql高级及实战
- 英语钻石法则(一)-----句子中心论
- 小米Redis的K8s容器化部署实践