🎁个人主页:我们的五年****
🔍系列专栏:Linux课程学习****
🌷追光的人,终会万丈光芒
🎉欢迎大家点赞👍评论📝收藏⭐文章
Linux学习笔记:
https://blog.csdn.net/djdjiejsn/category_12669243.html
前言:
之前听过一遍课,但是如今还对它很模糊,不知道它到底是如何进行的。所以今天重新听课,重新学习。进程地址空间话题很大,这次也只能讲很少一部分,能对进程地址空间的宏观了解就OK。
目录
一.小实验(不是物理地址,而是虚拟地址/线性地址)
Linux大哥,你别骗我,我之前一直给我的时物理地址,没想到你给我一个虚拟的地址,我真的看透你了。线性
路上的一个下BUG
刚刚在进行运行代码的时候,运行结果没有输出,一直卡在那,我以为是出现什么错误了。结果是我没有换行,因为我创建了子进程,这时候的显示器文件缓冲区采取的刷新模式可能是满刷新,当缓冲区满的时候,才会进行刷新。所以一开始在显示器上没有看见东西。后面啪的一下出现很多内容。大概就是这个原因。啊哈哈哈哈哈。下面我又无知了,请原谅我。
1.1实现目的和预想:
因为之前不是说父进程和子进程的代码共享,数据会独立一份吗?现在我们创建一个子进程,打印同一个值的地址是不是一样的,按理来说,他们地址是不一样的,因为数据相互独立。但是真实的结果是如何呢?
看上面的图,我们居然发现他们的地址是一样,啊?这好像不对啊,但好像又可以理解。我们之前学C++的时候,比如字符串string的拷贝的时候,不会立马进行拷贝,不会立即复制字符串的内容,而是增加一个引用计数。这里好像也可以这样理解,如果你不对数据进行修改,那么我还是用原来的呗,你要修改的时候,我再给你申请空间,进行赋值,再让你修改呗!那么我们下面就把gval的值修改一下,看是不是地址会不一样。
好像结果不是这样的,不是要写时拷贝,是因为(Linux大哥)C语言给了我们一个虚拟的地址。
1.2修改gval的值继续进行实验:
我们每次循环让子进程的gval发生变化,他们的地址还是一样的。为什么会这样啊,这就真的不能理解的啊?上面我还能去想想可能是写时拷贝的原因,在这里写时拷贝也关不上啊!
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
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);
sleep(1);
}
}
else
{
while(1)
{
printf("父进程,pid:%d,ppid:%d,gval:%d,&gval:%p\n",getpid(),getppid(),gval,&gval);
sleep(1);
}
}
return 0;
}
二.虚拟地址是什么?
这不是程序地址空间哈,这是进程地址空间。为了引入虚拟地址空间,必须把老师说的小故事在这里重复一遍。
2.1小故事(画饼):
在美国有一个大富翁,他很有钱,手上有10亿美金,并且他有很多孩子。有一天他的孩子A跑过来对他说:我想要100美元。大富翁听到以后,觉得没啥,我都有10亿美金了,这100美元能算啥,就把这100美元给了A孩子。但是还没道A孩子的手上,B孩子就冲过来把大富翁手上的100美元给抢走了,B孩子说这100美元是我先看到的,应该是我的。B孩子还没说完,C孩子就抢着说:B孩子都拿了100美元,那我要200美元。A孩子听到以后觉得很不公平,为什么最开始是我先找父亲要的钱,你们反倒比我要的多,为此A孩子说,那我最起码也要200美元。大富翁听到以后,非常头疼,不知道该怎么办?
在美国的另一边,也有一个大富翁,他很有钱,也有10亿美元,也有很多的孩子,但是这些孩子都是他的私生子,他们不知道彼此的存在,都以为大富翁只有我这一个孩子。有一天,大富翁跑到私生子A住的地方去看望A孩子,然后发现A孩子在一家上市公司上班,每天工作非常辛苦。大富翁对孩子A说,好样的!好好干,以后我10亿美金全是你的,孩子A听到以后,非常的开心,认为只要好好干,以后父亲的10亿美金全是我的。
后面大富翁又跑到B私生子那里,B孩子还在读大学,他是他们学习篮球队的主力 ,为了以后能打进NBA,每天训练也是非常的辛苦。大富翁看到以后,对B孩子说:好好打球,只要你好好打球,以后我的10亿美金也是你的。孩子B听到以后,也是非常的开心,觉得马上就能继承父亲的10亿美金了。
大富翁看望完B孩子以后,就去看望私生子C了,C孩子是女儿,现在在读高中,但是跳舞很厉害,她的梦想是去当模特。大富翁看到C孩子以后,也对C孩子说,只要你好好跳,以后我的10亿美金全部都是你的,此时C孩子很开心......当然,大富翁还有其他很多的私生子。
大富翁对每个私生子都是这么说的,每个私生子都以为自己可以拿到父亲的10亿美金,却不知道他们根本拿不到,只能拿到一部分。
以后每个孩子在找父亲要钱的时候,都会有一个潜台词,就是父亲有10亿,而且以后都是我的。
饼画多了,也需要管理。先描述,再组织。这个大富翁就是操作系统!这里的大饼就是程序地址空间!
2.2理解虚拟地址空间:
假如内存大小是4GB,一个进程过来申请了500MB,此时这个进程认为还有3.5GB可以用。但是另外一个进程也申请了500MB,这个进程也觉得它还有3.5GB可以申请,其实此时内存的真实大小只有3GB了。
2.3mm_struct结构体
理解区域划分的本质:
交代区域的开始和结束,就能进行区域划分。
mm_struct中就有每个地址区域的地址空间。有起始位置和结束位置。
PCB中,有一个mm_struct结果体指针,指向一个mm_struct结构体。
mm_struct是由谁来初始化的?信息来源是可执行程序,因为可执行程序被编译好,就有需要多大的空间等信息。可以不加载可执行程序的内容,但是这些信息必须被加载,这些属于PCB的范畴。
栈区是运行的时候,操作系统自己创建的,由操作系统决定。
物理内存延迟开辟,需要的时候,先给你虚拟地址,没有对应的物理地址,只有当真正使用的时候,才会填入物理地址形成映射。
2.4内存编址
最小内存寻址是计算机能访问的最小内存单元。内存是按字节来划分的,32位系统中地址总线通常是32了,能最大表示的地址范围是2的32次方的存储单位,2的10次方是1024,2的32次方就表示4GB大小。
用unsigned long就能表示地址的所以范围,这就是内存编码。
三.页表
3.1代码共享:
当父进程创建子进程的时候,子进程会拿父进程的PCB进行初始化,mm_struct也相同,页表也相同。页表中的代码区指向同样的物理地址,所以形成了代码共享。父子映射道同样的内存代码区域。所以子进程也是看到fork以上的代码的,只是不会执行它。
3.2页表和物理地址的映射
页表也被子进程继承,所以最开始的虚拟地址和物理地址都是和父进程一样的,用的还是父进程的代码和数据。当时当子进程的数据修改的时候,在此之前,操作系统OS会先申请空间给子进程用,然后修改子进程中的页表物理地址,虚拟地址不变。所以父子进程的有不同的页表,向上返回的虚拟地址是一样的,但是通过不同的页表能映射到不同的物理空间上去。所以就能解释上面小实验了。
在C语言中,变量名就相当于地址。
3.3页表中的标志位
页表中会有很多的标记位,下面就来谈谈rwx标记位和isexit标记位。
3.3.1rwx权限:
rwx标记位可以设置权限,比如代码区,只能是只读的,当如果要写,页表不让你从虚拟地址映射到真实的物理地址了。还有可能直接杀死进程。在代码层面,如果去修改一个只读区,编译器是识别不出来的,这是在运行时,才会去发生映射,才会报错。所以为了让编译器能检查,就引入了const。
3.3.2isexit标记位:
1.分批加载。2.挂起等操作。
表示在页表中这个映射关系是否存在,是都能在内存中找到。因为有种种原因,数据不会一直占着内存,会被放回到磁盘中,内存的大小是有限的。所以这个标记就是说是否在内存中存在。当不存在的时候,如果要进行操作,就会重新加载。所以才能跑大型程序。
四.为什么要有虚拟地址+页表
野指针就是有一个虚拟地址,在进行映射的时候,发现找不到真实的物理地址,或者权限不对,就会发生错误。操作系统就可以控制该进程终止。
可执行程序的代码和数据可以加载到物理内存的任何地址处。有页表进行映射。
4.1安全和隐私保护:
虚拟地址的设定,可以避免程序直接访问物理内存。必须通过页表的映射才能知道物理内存。页表是操作系统来管理的。防止错误程序和恶意软件篡改内存数据,增加系统的稳定性。
4.2进程管理和文件管理解耦合:
进程在进行管理的时候,操作的是虚拟地址,不要去物理地址。这样就大大降低了程序开发的复杂度。
4.3有效利用内存资源:
让进程都以为独占内存空间,但是操作系统可以进行自由切换,没用的时候,就能从内存中放到磁盘中。
4.4让进程以统一的视角看待物理内存:
代码和数据可以加载到物理内存的任意位置,但是对于mm_struct一样还是相同的数据放在一起。
无序变有序。