在学习fork函数的时候父子进程的返回值不同
而且可以使用一个变量来分别表示父子进程的pid
当时我们说这个涉及到写时拷贝的知识,
而且说了一句当时看起来比较"奇怪"的话
在Linux中,可以用同一个变量名来表示不同的内存
我们肯定非常好奇Linux是如何做到的呢?
这就需要先介绍一下进程地址空间的问题了
当我们了解完进程地址空间之后,写时拷贝的问题就迎刃而解了
要想更好地理解进程地址空间的概念,我们可以从我们C语言学习过程中听说过,或者说是了解过的内存分区这个概念开始谈起
经过我们C语言的学习
对于下面这个图,大家应该并不陌生
其实:下面这张图才是更加完整的版本:
主要是又添加了命令行参数和环境变量(它们是位于栈区上方的)
栈区和堆区之间有共享区(这一点不是我们本节的内容)
静态区细分为未初始化数据和已初始化数据
为了更好地帮助大家理解,我们写一段代码带大家验证一下
栈区的地址和增长方向,
堆区的地址和增长方向,
静态区和代码段的地址我们都验证过了
下面我们来验证一下命令行参数和环境变量的地址是否比栈区的地址大即可
我们修改一下code.c
其实:
1.命令行参数和环境变量的地址是大于栈区地址的
2.指向命令行参数和环境变量的指针的地址也是大于栈区地址的
3.环境变量的地址大于命令行参数的地址
我们先来验证一下
第2,3条
再来验证一下第1,3条
验证成功
其实:
正是因为命令行参数和环境变量在内存中也有自己的一席之地
因此它们才能够做到在程序的运行过程中一直存在
至此相信大家已经对语言角度上的内存分区有了比较清晰的认识
下面再给大家看一份代码
这份代码的意思是:
前5s,父子进程同时运行,此时子进程还没有修改val
打印父子进程中val的值和val的地址
前5s结束后,子进程修改val
打印父子进程中val的值和val的地址
其实,我们刚才介绍的那个内存分区并不是真正的物理内存的分区
而是进程地址空间
我们在这里打印出来的地址并不是物理地址,而是虚拟地址(也叫做线性地址)
也就是说我们在C语言或者C++或者等等任何语言中所学习的,所用到的所有地址其实都不是物理地址,而是虚拟地址
这些虚拟地址跟物理内存之间有某种映射关系,一一对应于物理地址
不过我们不要担心,学习语言时,按照虚拟地址去学习即可,语言内部已经帮助我们处理好任何我们所能想到的问题了
首先,我们要明确一点:
所有的变量名,函数名等等
在编译之后都会变成该变量,该函数的地址!
我们先抛出结论,解释一下这个现象,然后我们再去详细介绍进程地址空间
结合下面的图片来看
这是刚创建子进程之后
这时发生写时拷贝之后
下面我们就能够解释一下为什么
在Linux中,可以用同一个变量名来表示不同的内存
因为在父子进程中同一变量名尽管对应于相同的虚拟内存地址
但是那个相同的虚拟内存地址却对应于不同的物理内存地址,因此可以用同一个变量名来表示不同的内存
也正是因此,那个变量val在父进程中的值是10,在子进程中的值是99,
而且父子进程中val的虚拟地址也可以相同
每一个进程,都会存在一个进程地址空间,在32位平台下,有0到4个GB
进程地址空间是一个结构体对象
页表也是一个结构体,它不仅仅存放了虚拟地址和物理地址的映射关系,它的结构也是很复杂的,我们在这里介绍一下页表的权限属性
val所对应的物理内存的数据对于父子进程来说是只读属性
数据段和代码段对于父子进程来说是共享的,不能允许它们随意修改,因此它们的权限都是r权限
在子进程刚被创建之后,此时还没有发生写时拷贝
当我们想要修改这个数据时:
上面是合理的请求
下面给大家演示一下不合理的请求:
在C语言的学习过程中,大家可能都听说过字符串具有常性,不能修改
为什么不能修改呢?
其实就是因为在我们想要修改的时候,触发缺页中断,操作系统认为这样是不合理的,因此把我们的进程干掉了,因此我们的程序就报错了
注意:gcc编译器是允许char*指向一个字符串的
不需要加const
Segmentation fault:段错误
此时因为页表映射时的权限只有r,没有w,因此字符串常量不能被修改
也就是说是这个行为是在操作系统层面上阻止的,跟语言无关
我们知道:
进程=内核数据结构+代码和数据
以上就是Linux进程地址空间的全部内容,希望能对大家有所帮助!