前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >由浅入深的了解进程(6)---地址空间

由浅入深的了解进程(6)---地址空间

作者头像
薛定谔方程难
发布2024-08-07 15:50:18
730
发布2024-08-07 15:50:18
举报
文章被收录于专栏:我的C语言
进程的地址空间

1、直接代码展示的现象

其中当父子进程之间的g_val改变之后,为什么即使是不同的值了之后,两个进程中的g_val的地址还是一样的? 虽然不能够确认这是什么意思,但是这个绝对不是物理地址,如果是物理地址的话,一个地址修改过值了之后不可能还能表示另外一个值,所以这个应该是虚拟地址。

2、基本理解

由于硬盘中存在可执行的程序,当我们开始运行程序的时候,就会把代码和数据放在内存中,由前面所学到的一切的有关进程的知识之中,我们知道在操作系统中有着进程task_struct来帮助管理这些数据和代码以及别的一些状态。同时此次介绍的将是进程中的另一部分,地址空间。 地址空间能够被进程中的指针找到,在32位的操作系统之下,地址空间通常都是4GB的。这些都是在OS内部的

task_struct结构体把代码和数据分开准确的存放在地址空间的栈,堆等地方,但是这个数据不是真正的保存,而是提供线性的虚拟地址最终找到真正的存在物理地址中的数据。 究竟是按照什么找到的呢?是按照分页来从虚拟地址找到物理地址。

其实首先对于子进程来说,由于会继承父进程的数据和代码,所以说子进程在开始的时候是直接浅拷贝父进程中所有的内容,地址空间,虚拟地址以及页表,但是如果直接按照页表来说找到对应的数据的话,并且修改成功,那么这次的修改就会被父进程看到,从而也改变了父进程中的数据,也就势必会导致父进程本身运行的问题。 进程本身在运行的时候应该符合独立性 ,所以就不应该子进程在修改数据的时候改变父进程中的数据。 所以回到刚刚的问题,为什么两个的地址是一样的,但是最终的结果确实代表的不同的值? 就是因为,子进程在修改这个数据之前,操作系统会在物理内存中重新开辟一个空间,开辟完空间之后,就把老的数据拷贝到新空间之中,把新的物理地址和老的物理地址相比,把新的物理地址放在子进程的页表当中,重新构成映射,此时子进程的指向就不会指向原来的地址,当这个程序执行结束之后,程序才会继续执行,修改变量的值。==这些操作都是操作系统准备的,OS自主完成写时拷贝。==所以这样的结果就是打印的地址是一样的,但是所展示的结果却是不一样的原因。 如果说每一次创建进程,就会创建一个自己的地址空间,每一个进程也都要有自己独立的页表。所以操作系统中,创建一个进程,不仅仅是创建一个进程的PCB,对于很多的地址空间,OS也要进行管理,那应该如何管理众多进程的地址空间呢?先描述,再组织。地址空间的本质就是内核中的一个结构体对象。

3、细节问题

3、1、独立性细节

独立性: 如果父子进程不写,未来一个全局变量是父子共享的,代码是共享的(只读的)。 1、问题: 为什么我们要这么做?换句话说,为什么不是直接把所有的数据都拷贝到子进程之中呢?为什么是只有在修改变量的值的时候才创建新的内存空间? 1、答案: 那是因为在父进程中有很多的数据子进程不一定会修改,比如说命令行参数和环境变量,子进程几乎都不会进行修改,所以由于这部分的数据本来改动就不大,但是占据的数据却很大的话,每次还要单独拷贝一份放在内存空间中就会相当的消耗资源空间。所以写时拷贝就是一种按需申请,不会过多的浪费空间。同时还保证了进程的独立性。 2、问题: 是不是写时拷贝的时候,由于每一都需要重新开辟空间然后再创建变量的操作,会不会降低运行的速度反而不如直接拷贝呢? 2、答案: 其实不然,如果直接进行拷贝的话,那拷贝的可不只是一点半点,而是很多的信息。反而这样才是更需要时间,降低效率的。

3、2、地址空间细节

1、什么是划分区域? 1、解释: 划分区域就是像上面图上展示的地址空间一样,一块地址空姐分为不同的功能。如何用计算机语言做到呢?

代码语言:javascript
复制
struct area
{
	int start;
	int end;
}
struct destop
{
	struct area left;
	struct area right;
}
//如果想要变换的话,就能够直接通过这样的形式进行改变
d.left.start=1;
d.left.end+=20;
d.right.begin-=20;
d.right.end=100l

所以地址空间的本质也就是内核中的一个struct的结构体,内部很多的属性都是表示start和end的范围。

2、如何理解地址空间? 2、解释: 举个例子来说,就像是一个漂亮国中的有一个大富豪,手里有10亿dollar。他悄悄咪咪有4个私生子,每一个私生子都差不多到20多的岁数了,此时大富豪就对每一个人说,你们好好干,好好闯荡,要是怎么样有什么成就的话,就把我的资产全部都继承给你们。这样的话,每一个私生子都会认为只有自己的父亲只有一个孩子。如果将来遇到麻烦的事情的话,向自己的老爸要钱,他们也都会认为都有10亿能够帮助自己。可是事实并不是这样的。所以大富豪也得管理这10亿,因为不可能每一个孩子都能够花上10亿 。 所以这样的现实中存在的案例,也能够用来理解操作系统,其中大富豪的角色就是操作系统,10个亿对应的就是物理内存,私生子对应的是进程,给每一个私生子画的大饼就是进程地址空间。 3、为什么要有地址空间? 3、解释: 为什么要对每一个进程构建一个地址空间呢?我们为什么不直接通过进程的task_struct中直接记录下我们存放在物理内存中的地址呢? 首先,如果是直接访问的话,可能存在着访问越界的情况,访问越界的话,可能还会导致别的进程的数据被修改,这样的话还会影响别的进程。而且实际的物理内存中,代码区,数据区,栈区,堆区,共享区,命令行参数和环境变量都是无序且杂乱的,直接访问不太方便管理。 所以地址空间加上页表的好处就能够让这些数据变得让task_struct看起来有序,方便我们的管理,能够让原本无序的物理内存中存储的数据变为一个可以连续访问的。 除此之外,操作系统并不是说每一次的进程中malloc的时候都需要再内存空间中申请一块地址,只有在使用的时候才会真正的再物理内存中申请,在没开始使用的时候,只不过是在地址空间上申请,在页表中申请了虚拟地址,这样的话能够有效的节省物理内存。进程管理模块和内存管理模块进行解耦。 还有, 如果我们有地址空间的话,还能够在越界的时候在查找页表的时候直接能够找到相对应的错误,能够直接禁止访问,说明地址空间还拥有拦截非法请求的功能。本质的目的就是对物理内存的保护。 4、如何进一步理解页表 ==4、回答:==页表不仅仅只是虚拟地址和物理内存中相互对应的关系,还有在页表中还存在判断是否存在于内存中以及对这段数据的rwx权限的描述。 所以,我们之前的一个问题现在来看的话,就能够拥有更好的解释。

代码语言:javascript
复制
char *str="hello world"
*str='H'

这段代码在之前的时候,已经讲过了是错的,因为一个指向静态常量区的不能够修改。 那有没有进一步的思考,为什么不能修改呢?其实就是因为这段代码在进程中的task_struct的进程空间中存放的页表中的虚拟地址找到的物理内存中的地址的条件中是只能够读,不能够写的,所以刚刚的代码才是错误的。 所以我们在写代码的时候即使是写错了,也不会对原本物理内存中的进程造成影响,因为检查到错误的时候就已经报错了,不会再进行下一步的写入物理内存。 5、进一步理解写时拷贝,操作系统是这么支持写诗拷贝的? 5、回答: 由于进程地址空间的在一开始的时候我们定义的那个代码,首先是只有父进程的,此时的页表对于的一开始定义的变量是有指定的物理内存的并且此时的页表中对于这个变量的权限是可读也可写的。但是当我们创建了子进程的时候,操作系统就会修改页表中变量的权限,修改为可读的,这样的话,如果我们在子进程修改的话,OS就会识别到错误,当识别到错误杀掉进程之前的话还是需要进行一些操作的,会识别1、是不是数据不在物理内存中 2、是不是数据需要写时拷贝 3、如果上面两种情况都不是的话,才会进行异常处理。 情况一表示的是缺页中断,属于正常情况。 情况二表示的是就是需要写时拷贝。怎么判断出来的?数据中存在计数数据,当计数为2,并且为可读可写的话,就会进行写时拷贝。 6、如何理解虚拟地址? 6、回答: 在一开始的时候我们是怎么得到虚拟地址的呢,或者说虚拟地址是如何写的呢?其实程序中本身就拥有代码,只要程序编译结束之后,所有原本的变量啊函数名啊,等等一些列的代码,都会变成地址,变成地址直接调用,所以在这个时候就有了地址,但是这个地址也不可能是直接把内存中空余的地址直接利用上的,所以程序中的地址就是虚拟地址,当我们加载到物理内存的时候,虚拟地址就会和真正的物理地址相互配对,形成页表,完成地址空间。—这也叫做平坦模式。

4、问题回溯

我们之前的文章提到过,当时的时候说的没有办法解决,但是现在我们学完进程空间,我们就能够解决,为什么一个变量能够返回代表两个不同的值。所以id能够既能够大于0,也能够等于0。

5、Linux2.6内核进程调度队列

Linux系统中,每一个CPU都有一个运行队列。 其中蓝色的区域中的queue[140]表示的前99个都不需要利用,只有从100开始到139是我们存放于优先级的时候用到的。全名叫做task_struct * queue[140]。每一个指针都维护着相同优先级的进程。正如图上所示。优先级是从60到99的,所以queue中的100的位置相当于是优先级的60,往后一次类推。 其中的bitmap[5]的全称是long bitmap[5],5 * 32=160>140,4 * 32=128<140,所以就选择5个元素。虽然其中有160个bit位,但是只是用140个。所以拥有这个就能够不需要遍历queue中的位置,去找那个优先级是存在的,哪个是不存在的。大大的提高了效率。其中的nr_active表示的就是一共有多少个进程。 此时所介绍的都是在处理已经拥有的进程,但是我们在运行的时候,进程也可以继续再添加啊。 其中蓝色和红色两个结构体内容是一样的,在图上的active的指针默认是指向蓝色的,expired指针默认是指向红色的。CPU找进程的时候不是直接找到相对应的队列的,而是找到刚刚说的指针在找到指针中的queue[140]中进行寻找。并且找到相对应的队列进程只进不出,另外一个队列就是只出不进,这个就包括了在运行的时候有别的进程加入的情况。在进行结束一个队列之后,我们就需要swap(active,expired),相互转换一下,然后继续执行,这样CPU就开始转起来,运行起来了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-08-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进程的地址空间
  • 1、直接代码展示的现象
  • 2、基本理解
  • 3、细节问题
    • 3、1、独立性细节
      • 3、2、地址空间细节
      • 4、问题回溯
      • 5、Linux2.6内核进程调度队列
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档