什么是进程地址空间

在学习C语言时,我们对内存的划分是这样的:
在这里插入图片描述

其本质就是把内存划分成了一个一个的区域
我们可以写一个程序来验证一下这个划分是否正确

int g_val = 100;//初始化的全局变量
int g_unval;//未初始化的全局变量
int main()
{
	int a = 10;//局部变量
	const char* s = "hello";//hello是一个常量,s是静态变量
	char* heap = (char*)malloc(10);//在堆上开辟的空间
	printf("栈区:%p\n", &a);
	printf("常量:%p\n", &s);
	printf("堆区:%p\n", heap);
	printf("未初始化全局变量:%p\n", &g_unval);
	printf("已初始化全局变量:%p\n", &g_val);
	printf("静态变量:%p\n", s);
	printf("代码段:%p\n", main);
	return 0;
}

下面实在Linux下的运行结果:
在这里插入图片描述
结果是符合我们的划分的,但这时单进程的情况,如果我们用两个进程来观察地址,结果如何?

int g_val = 10;
int main()
{
	if (fork() == 0)
	{
		int cnt = 5;
		while (cnt)
		{
			printf("I am child,times:%d,g_val=%d,&g_val=%p\n", cnt, g_val, &g_val);
			cnt--;
			sleep(1);
			if (cnt == 3)
			{
				printf("更改数据\n");
				g_val = 100;
				printf("更改完成\n");
			}
		}
	}
	else
	{
		while (1)
		{
			printf("I am parent,g_val=%d,&g_val=%p\n", g_val, &g_val);
			sleep(1);
		}
	}
	return 0;
}

下面是运行结果
在这里插入图片描述
在子进程对全局变量g_val做修改之前,父子进程都可以看到g_val,都打印出了它的地址和数值,因为子进程是继承了父进程的代码和数据,这是没有问题的,但是在子进程对g_val做出修改之后,他们打印出的g_val的地址是一样的,但是值却是不一样的,出现了一个地址两个值的情况,这显然是不正确的,如果我们在程序中看到的地址如果是真实的物理地址的话,那么这种情况是绝对不可能存在的,所以我们使用的应该是虚拟地址

我们在语言层面所看到的地址基本全部都是虚拟地址。

什么是进程地址空间

每一个进程都认为自己在独占内存,所以每一个进程都有一个地址空间并且以整个内存的大小(32位为4G)来对内存的区域进行划分,在内核中描述进程地址空间是一个数据类型struct mm_struct,具体是进程的地址空间变量,其内容大致如下:

struct mm_struct
{
	unsigned int code_start;
	unsigned int code_end;
	
	unsigned int init_data_start;
	unsigned int init_data_end;

	unsigned int heap_start;
	unsigned int heap_end;

	unsigned int stack_start;
	unsigned int stack_end;
	.......
}

里面存放的内容可以认为是该进程对内存进行划分的各个区域的起始地址和结束地址,并且都是以4G的空间划分的。而OS里可能会有多个进程,不可能让一个进程独享所有的内存资源,所以进程的地址空间中的地址就是虚拟地址。

虚拟地址&物理地址

程序要运行,就要被加载到内存中,而系统中可能会存在多个进程同时运行的情况,所以让一个进程独享所以的内存资源是不可能的,而地址空间又是按照其独享资源的方式划分并且分配的,并没有考虑到和其他进程共用内存的情况,所以如果直接使用地址空间中的地址,那么就可能出现不同进程之间使用地址发生冲突的情况。所以在把进程加载到内存时,就需要把进程中的虚拟地址以某种方法映射到真实的物理内存地址中。

我们把虚拟地址转换成物理地址是通过页表和MMU完成的。
页表:本质是一种映射表,把虚拟地址映射到物理地址上。
MMU:内存管理单元,是一个硬件,被集成在CPU中。

可以把页表理解为一张表,它存放的是虚拟地址与物理地址之间的对应关系,如下图:
在这里插入图片描述
页表之中还有对权限的管理。映射的结构如下图
在这里插入图片描述

地址空间的作用

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

程序运行时不同进程的代码和数据都会被加载到物理内存上,所以不同进程的数据在物理内存上的距离可能会非常接近,而在我们写程序的时候,我们不能保证我们一定不会非法访问到别的进程的数据,所以在页表的部分,每一个映射里面还会有有关权限的内容,以此来防止进程非法访问别的进程数据。例如在学习C语言时我们知道常量区的数据是不可以修改的,其原理就是常量区的数据OS给用户的权限只有r权限,没有w权限。那么别的进程的数据页表和OS就会不允许用户映射。

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

当我们向内存中申请一块空间时,我们可能现在暂时不会使用它(使用:对空间进行读写),那么在OS的角度上,如果把空间马上给进程,而进程又没有立刻使用,就意味着有一部分空间本来可以让别的进程马上使用,却被当前进程闲置了,那这就造成了空间浪费。
所以OS不会在进程申请空间的时候就立刻把空间给进程(先不建立映射),而是当进程要使用这块空间时,才会在物理内存上给进程分配(基于缺页中断进行物理内存申请:写时拷贝)。

如果物理空间满了而进程又申请了空间,OS就可以通过内存管理算法,把闲置的空间放到磁盘中供进程使用。

3.站在CPU和应用层的角度,进程可以统一看做使用了4GB空间,而每个空间区域的相对位置是比较确定的。

程序的代码和数据可能被加载到物理内存的任意位置。但是每个进程的不同段的虚拟地址是相同的,以main函数的地址为例,那么每个进程的main函数的虚拟地址都是相同的,CPU只需要存main函数的虚拟地址,然后就可以通过映射不同进程的页表,找到不同进程的main函数的代码地址,其他数据也是如此。这样就大大减少了内存管理的负担。

OS这样设计的目的就是让每一个进程都认为自己是独占系统资源的,而如果没有地址空间,进程之间就会存在差异而差异就意味着复杂

总结

现在在回过头来分析一下篇头的代码,可以画出如下的结构图
在这里插入图片描述
因为子进程的创建是以父进程为模板的,所以在子进程修改数据之前,他们映射的是同一块物理空间,但是在子进程对g_val修改之后,OS写时拷贝了物理空间,改变了映射关系,所以物理地址就不一样了,但是虚拟地址没有发生变化,而我们能够看到的地址都是虚拟地址,所以我们才发现g_val是一个地址但是却有两个值。

父子进程的代码共享的本质就是他们的代码是映射到同一个块空间的。
所有,所有的只读数据,一般都只有一份,因为操作系统维护一份的成本是最低的。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

c铁柱同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值