【Linux】基础:进程地址空间
创始人
2024-01-20 23:39:15
0

【Linux】基础:进程地址空间

摘要:本文首先通过复习关于C语言内存空间的知识来做实验提出问题,从而引入进程的地址空间。需要理解的是进程地址空间的组织形式与其表示意义,在需要理解如何完成进程地址空间的划分以及关键对应物理内存的思想,掌握虚拟的概念。最后通过解释设计原因,帮助读者更深入理解进程地址空间。


文章目录

  • 【Linux】基础:进程地址空间
    • 一. 背景介绍
      • 1.1 C语言地址空间
      • 1.2 问题提出
    • 二. 进程地址空间概述
    • 三. 进程地址空间划分方式
    • 四. 虚拟地址与物理内存
    • 五. 进程地址空间设计原因
    • 六. 总结

一. 背景介绍

1.1 C语言地址空间

在C语言学习过程中,可能曾经了解过C语言的程序地址空间分布,对于32位的机器,内存空间大小为4GB,从低地址到高地址,分别划分区域为:正文代码、初始化数据、未初始化数据、堆、共享区、栈区与命令行参数环境变量几个部分。其中需要注意的是栈的地址是从高地址向低地址使用的,而堆区相反

现在,通过进一步的实验来验证该图的空间分布,在此简单书写各区域的代码,并对其取地址并打印,进行比较,实验如下:

proc:proc.cgcc -o $@ $^ -std=c99
.PHONY:cleanrm -f proc
#include
#include
#include
#include
int gal_unval;
int gal_val = 100;int main(){const char *s = "hello world";printf("Address of main: %p\n", main);printf("Address of const String: %p\n",s);printf("Address of int: %p\n",&gal_val);printf("Address of unint: %p\n",&gal_unval);char *heap = (char*)malloc(sizeof(char));printf("Address of heap: %p\n",heap);int a = 10;int b = 20;printf("Address of Stack_s: %p\n",&s);printf("Address of Stack_a: %p\n",&a);printf("Address of Stack_b: %p\n",&b);return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
Address of main: 0x4005d6
Address of const String: 0x400758
Address of int: 0x60102c
Address of unint: 0x601034
Address of heap: 0x1cc06b0
Address of Stack_s: 0x7ffe402086a0
Address of Stack_a: 0x7ffe4020869c
Address of Stack_b: 0x7ffe40208698

可以发现,从打印出来的结果是由小到大排序,符合上图的区域分布。

再进行实验验证,栈的地址是从高地址向低地址使用的,而堆区相反,实验如下:

#include
#include
#include
#includeint main(){char *heap = (char*)malloc(sizeof(char));char *heap1 = (char*)malloc(sizeof(char));char *heap2 = (char*)malloc(sizeof(char));char *heap3 = (char*)malloc(sizeof(char));printf("Address of heap: %p\n",heap);printf("Address of heap1: %p\n",heap1);printf("Address of heap2: %p\n",heap2);printf("Address of heap3: %p\n",heap3);int a = 10;int b = 20;int c = 30;printf("Address of Stack_a: %p\n",&a);printf("Address of Stack_b: %p\n",&b);printf("Address of Stack_b: %p\n",&c);return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
Address of heap: 0x8472a0
Address of heap1: 0x8472c0
Address of heap2: 0x8472e0
Address of heap3: 0x847300
Address of Stack_a: 0x7fff4fbb45fc
Address of Stack_b: 0x7fff4fbb45f8
Address of Stack_b: 0x7fff4fbb45f4

实验输出地址排序符合预期结果。

1.2 问题提出

再进行一次实验,来查看父子进程的数据对于C语言地址空间的分布情况,实验代码与输出结果如下:

#include
#include
#include
#include
int gal_val = 1;
int main(){if(fork() == 0){//childint nums = 5;while(nums){printf("I am child ,nums = %d,gal_val = %d,gal_val_address = %p\n", nums ,gal_val,&gal_val);nums--;if(nums == 2){printf("====================== 在此处修改了gal_val的值为100 ======================\n");gal_val = 100;}sleep(1);}}else{//parentwhile(1){printf("I am father ,gal_val = %d,gal_val_address = %p\n",gal_val,&gal_val);sleep(1);}}
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 5,gal_val = 1,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 4,gal_val = 1,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 3,gal_val = 1,gal_val_address = 0x60103c
=========================== 在此处修改了gal_val的值为100 ===========================
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 2,gal_val = 100,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c
I am child ,nums = 1,gal_val = 100,gal_val_address = 0x60103c
I am father ,gal_val = 1,gal_val_address = 0x60103c

从实验结果可以发现,好似在最开始时,父进程与子进程公用一块地址空间,在后来,我们对程序的变量进行了修改,会发生非常熟悉的写时拷贝,预期结果是在内存中开辟了一个块新的地址空间储存了这个子进程数据,可是实验结果出现了预期以外的现象,在这里的值改了,但是地址空间却还是同一块地址空间,这是否非常奇怪

在此对该实验现象进行介绍,其实在这里的地址空间并不是物理的地址空间,而是虚拟的地址空间,是逻辑上的地址空间,因此进程的地址空间是虚拟的。在此实验中提出了进程的虚拟空间这一个概念,本文将会介绍,如何通过虚拟空间来实现进程地址空间的管理,如何与物理内存交互起来,最后会对该实验结果进行重新的解释。当然本文只是对进程地址空间的概述,因此不会探讨的太深刻。

二. 进程地址空间概述

在进程概念中提到过,对于操作系统的管理方式是对管理对象进行描述再进行组织,对于地址空间的管理也是使用了同样的思路。对于每个进程来说,创建时会同时会创建一个进程控制块,进程控制块包含了进程的所有属性,其中也有管理进程地址空间的数据结构,在Linux中这个数据结构名为mm_struct

对于mm_struct,是内核中的一个数据结构类型,是具体进程的地址空间变量,其表示的大小就是内存的大小,通过这个数据结构,进程可以认为自己拥有了物理内存的实际空间,可是这是不现实的,在操作系统中有如此之多的进程,怎么可能每个进程都是独占物理内存,因此这个独占是虚拟的。而能设置如此的虚拟空间,是因为有着内存调度算法的作用,可以通过其来提高效率,提高空间利用率,本文对此不会赘述,将在后文具体描述。

对于mm_struct与PCB和物理内存,在此关系进行一个简单的图示:

三. 进程地址空间划分方式

对于进程地址空间的划分是通过偏移量来划分的,类比与刻度尺,在通过每个区域的头尾刻度进行划分。也就是在地址空间上,对应的地址空间以线性的视角看待,地址空间上进行区域划分时,分别对应线性位置的头尾来划分区域,但对应的线性位置是分配划分的虚拟地址。

划分的地址空间是按照物理内存大小进行划分给的,对于32位地址的内存,就是4GB的内存。也就是说每个进程都会认为自己拥有4GB的内存空间,而对于操作系统而言,这4GB是虚拟的,如果是物理空间也是不可能做到的。用C语言进行简单的描述如下:

struct mm_struct{.......unsigned int code_start;unsigned int end_start;unsigned int init_data_start;unsigned int init_data_end;unsigned int uninit_data_start;unsigned int uninit_data_end;.......unsigned int stack_start;unsigned int stack_end;.......
}

四. 虚拟地址与物理内存

根据上面的分析,虚拟地址就是进程控制块在创建时同时也创建的进程地址空间,在Linux上为数据结构结构体mm_struct,虚拟地址也可以称为线性地址,其的表示的大小与内存的大小一致,从0x00000000至0xFFFFFFFF,犹如拷贝一样,但其空间是虚拟的不存在的

可是面对那么多的进程,操作系统是如何给每个进程画饼,告诉他们自身独占内存呢,实际上,操作系统可以管理页表与MMU使虚拟内存与物理内存相互对应。**MMU是一种硬件,功能相当于查页表,全称为内存管理单元,集成在CPU中。页表是操作系统给进程维护的表 ,可以理解为映射表或者哈希表,其作用是将虚拟地址与物理地址对应,还包括了虚拟地址访问物理地址的权限。**如此设计简单来说是为了方便管理内存,合法使用物理内存。在此只是对页表的内容进行简单的提示,不做过多赘述,未来会进行更详细的说明。简易图示如下:

五. 进程地址空间设计原因

进程地址空间通过虚拟空间这种方式的设计原因主要有三点:

  • 通过添加一层软件层,完成有效的对进程操作内存够进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全;
  • 将内存申请和内存使用在时间上划分清楚,通过虚拟地址空间,来屏蔽申请内存的过程,达到进程读写内存够和OS进行内存管理操作进行软件上面的分离;
  • 站在CPU的角度和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的;

原因一:通过添加一层软件层,完成有效的对进程操作内存够进行风险管理(权限管理),本质目的是为了保护物理内存以及各个进程的数据安全。

对于原因一,在上述分析中,我们知道进程地址空间是虚拟空间,不与实际的物理空间进行直接交互,需要通过查页表才能完成交互。试想如果每个进程都可以直接访问实际物理地址空间,那么难以方式越界访问,非法访问,错误访问等问题,对内存安全与各个进程安全是非常严重的威胁,因此通过添加一个软件层,可以通过操作系统的介入,对进程访问内存进行监测,防止非法行为的发生。

原因二:将内存申请和内存使用在时间上划分清楚,通过虚拟地址空间,来屏蔽申请内存的过程,达到进程读写内存和OS进行内存管理操作进行软件上面的分离;

在进程申请地址空间时,实际上是只是申请了,而实际物理空间并不一定为之开辟,因为进程申请物理空间后,不一定就对其使用,所谓的使用就是对其进行读写。当进程没有使用申请的空间时,但又分配了实际的物理空间给该进程,就意味着原本这些空间可以给其他进程使用的,而现在却被闲置了,这样就会造成空间上的浪费。

因此操作系统可以通过虚拟内存的方式,将内存申请与使用在时间上划分,通过内存管理算法,在申请空间后需要使用,进行缺页中断,进行物理内存申请,达到进程读写内存和OS进行内存管理操作进行软件上面的分离。由这种方式来进行地址空间管理,对于进程而言OS做的内存申请的动作是透明的。

原因三:站在CPU的角度和应用层的角度,进程统一可以看作统一使用4GB空间,而且每个空间区域的相对位置是比较确定的;

对于CPU与应用层,将进程地址空间的虚拟内存看作物理内存直接使用,直接划分区域,这种较为确定区域的划分,便于CPU访问,而物理内存中的数据和代码是可以加载到任何位置的,只需通过操作系统的页表管理,就可以完成对虚拟内存物理内存的对应,减少物理内存管理的负担。

总结:如此设计最终可以达成一致的目标:每个进程都认为自己是独占系统资源的。

六. 总结

通过上述说明,引入了虚拟内存的概念,清楚C语言的地址空间并不是物理空间,而且对于每个程序来说应该是进程的地址空间。之所以出现开始的问题现象,是因为对于父子进程而言,都有着属于自己的进程地址空间,在Linux中,用结构体mm_struct表示,而子进程是以父进程为模板的,当有其他数据生成时才会发生写时拷贝,而发生写时拷贝时,虚拟内存申请空间,但在物理内存中才可以体现数据的差异,示意简图如下,可以发现虚拟地址一致但是在页表指向后,物理内存是不一样的。

这样的设计还可以减少内存的浪费,比如面对常量字符串时,如果字符串内容相同,可以共同指向同一块物理内存空间,代码示例与结果如下:

#include
#include
#includeint main(){const char* str1 = "hello world\n";const char* str2 = "hello world\n";printf("str1 Address:%p\n",str1);printf("str2 Address:%p\n",str2);return 0;
}
[lht@VM-12-7-centos Blogs_processAddress]$ ./proc 
str1 Address:0x400688
str2 Address:0x400688

补充:

  1. 代码将会放到:Linux_Review: Linux博客与代码 (gitee.com) ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...