如何给网站加关键词,建工社网校官网,衡水哪儿做wap网站,西安网站建设云阔网络深入Linux系列之进程地址空间
1.引入 那么在之前的学习中#xff0c;我们知道我们创建一个子进程的话#xff0c;我们可以在代码层面调用fork函数来创建我们的子进程#xff0c;那么fork函数的返回值根据我们当前所处进程的上下文是返回不同的值#xff0c;它在父进程中返…深入Linux系列之进程地址空间
1.引入 那么在之前的学习中我们知道我们创建一个子进程的话我们可以在代码层面调用fork函数来创建我们的子进程那么fork函数的返回值根据我们当前所处进程的上下文是返回不同的值它在父进程中返回子进程的PID在子进程中则会返回0那么我们可以利用返回值然后定义一个变量来接收这个返回值然后通过if else逻辑达到让我们的父子进程有着不同的执行流的目的那么想必刚才所说的过程对于你来说已经十分熟悉对于fork函数如何使用也已经轻车熟路但是在刚才的过程中我们唯独会有一个疑问也就是我们定义的一个变量来接收这个返回值同一个变量在不同的进程中竟然会出现两个不同的值那么这就和我们的理解有点冲突因为一个变量当前赋值且没被修改的情况下不可能具有两个不同的值就像一个人只能拥有一种性别一样。 那么我们之前的文章也详细介绍过我们的fork但是对于这个情况当时并没有提及那么我们现在来思考一下为什么会出现这样的现象那么我们知道我们进程是具有独立性那么要做到独立性也就意味着我们父子进程的数据不能做到共享为此做到该数据独立性那么我们的fork函数会采取写时拷贝的机制那么我们如果子进程读取我们父进程的数据那么此时子进程会与父进程共享一个物理内存页面但是如果我们子进程要对数据进行写入操作的话那么这时则会触发写时拷贝机制那么我们操作系统会为其要写入的数据专门拷贝得到一个副本然后在这个副本中做写入从而做到独立性所以根据我们之前对fork函数的学习以及认识有了这个写时拷贝机制我们认为此时我们父子进程关于接收这个返回值的变量那么它一定是有两份副本一份是父进程一份是子进程所以也就解释了它为什么能够具备有两个值。 那么接下来可以用c语言写一段简单的代码来实验验证我们刚才的推理的成立代码逻辑也很简单那么在代码中定义一个int类型全局变量g_val然后用fork返回值让父子进程有着不同的执行流然后我们让子进程对g_val进行修改然后接着父子进程分别打印这个全局变量的值以及对应的地址如果打印的结果是子进程中的全局变量的值是修改后的结果并且对应的地址与父进程不同那么就能够验证我们之前的结论。
那么我们根据结果发现值确实不同但是地址是相同的那么值不同证明父子进程确实对于g_val有各自对应的副本所以子进程的值与父进程的不同那么能够验证我们刚才所说的结论那么既然g_val在父子进程中有着各自对应的一份副本那么地址不可能是相同的那么既然是相同的那么只能说明一个原因那就是这个地址不是实际的物理内存地址那么这个地址究竟是什么呢那么这就和本文的主题进程地址空间有关了那么这个地址就是虚拟地址。
2.进程地址空间
那么在我们之前对于进程的理解上有得添加一个新的内容也就是进程地址空间。
我们知道我们进程的内核数据是被加载到我们的内存中的而当我们的进程被调度到CPU当中运行的话那么我们CPU得从我们内存中获取我们进程的各种内核数据我们知道我们CPU以及内存以及外部的io设备都是一个独立的工作单元那么它们要协同工作的话就得用“线”来连接那么数据通过该线在各个工作单元中流通那么其中这里的线我们可以分为数据线与地址线。
那么同理我们的CPU与我们的内存也有数据线以及地址线连接那么顾名思义地址线就是用来专门传输地址内容而数据线是用来传输数据内容的那么以32为机器为例那么我们的CPU与我们的内存的地址线有32根那么每一根地址线根据其高低电频的电信号分别用来表示二进制的0和1那么我们的CPU就能根据各个地址线的高低电频信号组合得到一个二进制序列而这个二进制序列就是我们的地址那么内存的寻址器获取到地址后会到内存的对应位置得到CPU要获取的数据然后通过数据线传给CPU当中的寄存器那么这32根地址线每一根线只能表示两种不同的信息要么是0或者1所以我们这32根地址线总共就能表示出2的32次方个不同的地址那么它的范围是从0000 0000到FFFF FFFF。
那么我们知道我们这32根地址线只能表示0000 0000到FFFF FFFF范围上的所有地址那么我们这0000 0000到FFFF FFFF范围上连续的地址便是我们的进程的地址空间也就意味着我们进程的各种数据那么他们的地址只能在这个范围上不能超出这个范围因为我们CPU的32根地址线只能表示这么多那么我们操作系统为了管理这个进程地址空间采取的方式是我们最熟悉的先描述再组织那么它在我们进程所对应的task_struct结构体中定义了一个指向mm_struct结构体。
而没错这个mm_struct结构体就是用来描述我们的进程地址空间那么它里面会有两个字段来表示这个进程地址空间的起始位置与结束位置那么通过这个mm_struct结构体就能在抽象建模出我们的进程地址空间。
而进程地址空间的设计还不至于此那么我们进程地址空间还专门进行了内存的布局将其分为了代码段以及数据段然后再数据段中的不同范围还分为了常量区堆区栈区等那么这些名词在我们的语言的学习上想必都听闻过。
那么至于为什么对我们的进程地址空间进行内存布局那么我们可以拿生活中的一个例子来理解就假设你是一个读者要去图书馆借阅书籍那么如果我们的图书馆的各种类型的数据是随意摆放各种类型的书籍都在同一个书架上那么我们读者要寻找到目标书籍的时间开销是很大的但是如果图书馆将这些数据按照类型分不同楼层按首字母分不同书架那么我们查找以及图书馆管理起来就很方便。
根据这个例子我们便知道要为进程地址空间布局的一个重要原因那么便是对于数据的查找以及内存管理那么具体实现则是我们的mm_struct结构体在包含其他各个数据段的结构体指针那么对应该数据段的结构体就包含起始位置以及结束位置的两个字段或者直接包含各个数据段的起始以及结束的字段那么通过我们的mm_struct结构体我们就对内存有着统一的布局和视角来看待。
而在我们进程地址空间上的各个地址这些地址也就是所谓的虚拟地址所谓虚拟也就是说它并不是我们该数据真实在物理内存当中的地址那么之所以我们不给我们的数据直接分配物理地址原因也很简单那么就是安全那么如果我们用户直接接触到了物理地址那么我们可以对数据的篡改以及覆盖的行为是不受阻拦的那么这个虚拟地址就像是一个屏障将其与物理地址隔开那么我们对物理内存的任何操作意味着都要受到约束或者阻拦这维护了数据以及系统的安全。
那么既然我们只能看到虚拟地址那么我们内存器要获取数据那么肯定得是要我们的真实的物理地址那么必然得需要将我们的虚拟地址转换为我们的物理地址那么就有了我们页表的存在。
那么页表是一个数据结构那么它建立了虚拟地址到物理地址的一个映射并且还为每个数据设置了是否具有读写权限的字段信息那么我们可以通过页表将虚拟地址转化为物理地址。所以在我们创建进程的时候我们进程的内核数据被加载到内存中同时我们还会为进程创建对应的task_struct结构体那么其中就包括了进程地址空间的创建以及页表的创建那么也就意味着我们每一个进程都有自己的进程地址空间也就是对应的mm_struct结构体和页表但是我们要注意的是我们不同的进程的进程地址空间都是相同的也就意味着我们每一个进程的mm_struct结构体的各个字段都是相同的那么这也很好理解因为我们让各个进程都以统一的逻辑来看待内存但是看不到物理内存那么我们每个进程对应的进程地址空间都是全部的从0000 0000到FFFF FFFF的4GB的空间但是对于页表不同进程肯定是不一样的因为虚拟到物理对应的映射各个进程是不同的那么在我们创建我们的进程的同时我们会初始化页表的部分数据的映射那么之所以是部分而不是全部数据则是由于有以下原因
我们知道我们操作系统会为了缓解进内存的压力会对某些进程采取挂起的手段也就是将他的内核数据先放回到外部设备比如磁盘当中那么此时它的内核数据都没加载到内存中那么自然无法完成对应的映射。
其次我们知道整个进程地址空间的大小是4GB那么我们在不同的计算机的构造下有可能会出现实际物理内存比虚拟内存小的情况比如有的物理内存是2GB那么就会导致有的数据没有加载到物理内存所以无法完成所有数据的映射的初始化。
那么对于有些不在物理内存无法完成初始化的数据那么页表会专门会有一个字段来标记它是否在物理内存如果不在那么则会被标记为缺页那么对于缺页中断的数据的话那么我们到时候进程被调度的时候操作系统一定会为缺页中断的数据重新生成对应的映射。
所以有了虚拟地址的概念那么我们对之前的我们语言层面上无法解释的东西那么我们现在就能够找到源头了比如我们知道我们c语言我们为什么不能修改字符常量那么是因为常量区的数据的权限会在页表中标记为只读权限如果我们要对访问字符常量并且执行写操作那么它就会对我们该操作进行阻拦。 重新认识fork系统调用
那么有了虚拟地址空间这个概念那么我们再来重新理解一下我们的fork函数创建子进程的一个原理那么我们调用fork函数那么我们会拷贝父进程的task_struct结构体其中就包括进程地址空间以及页表这也就是为什么说我们子进程与我们父进程是共享一份物理内存页面的调用fork函数后我们操作系统会将父进程的页表中的数据段的内容的权限全部改成只能读不能写那么如果我们子进程要对数据段的内容进行写入的话由于权限只读从而就会触发写时拷贝机制那么操作系统会在物理内存重新为该数据开辟一份空间然后接着修改我们子进程所对应的物理内存地址而虚拟内存地址则不需要修改从而做到父子进程的数据的独立有了虚拟地址的概念便解决了我们开头所说的那个问题。 有了页表这个概念之后那么我们CPU要读取我们进程在内存中的数据那么CPU的MMU内存管理单元那么它会获取到该进程的页表然后将我们进程的各种数据的虚拟地址转换成物理地址然后通过地址线传递给我们的内存的寻址器去获取目标数据然后再传到我们的CPU的寄存器当中
3.结语
那么本篇文章介绍了我们进程地址空间的概念那么我们知道进程地址空间的出现的意义第一是能让用户以及操作系统能够以统一的视角与布局看待我们的内存因为我们实际上各个数据在物理内存中的位置是离散的乱许的但是在进程空间的视角下逻辑上认为他们是有序排列比如栈区以及堆区在高地址处那么这样的内存布局的好处还方便于我们对于数据的查询以及管理。
那么第二是我们的虚拟进程地址空间以及页表的存在建立一道用户与物理内存的屏障那么用户不能通过代码直接访问到物理内存不让用户的各个行为不会受到阻拦那么可能会出现数据的修改以及越界访问等等问题。
第三是我们虚拟地址以及页表的出现那么让我们的进程管理以及内存管理相互分离开那么进程管理我们操作系统就管理我们task_struct结构体所对应的各个数据结构而内存管理则是管理我们的页表结构即可实现了进程管理与内存管理的解耦
那么这就是本篇文章对于进程地址空间的全部内容那么希望能够让你有所收获那么我会持续更新那么下一篇文章我将会解析我们进程的终止与等待那么希望你能够多多关注多多支持