进程地址空间(虚拟地址 | 物理内存)


前言

在之前的学习中,我们只学习了图中的下半部分(用户空间),当时还并未涉及到操作系统。
在这里插入图片描述

我们写一段代码来验证一下这张图。

#include<stdio.h>
#include<stdlib.h>

int g_val = 100; //初始化
int g_unval; //未初始化
int main(int argc, char* argv[], char* env[])
{	
	// 以main函数为代表,打印代码区
	printf("code addr : %p\n", main); 
	// 初始化数据
	printf("val addr : %p\n", &g_val);
	// 未初始化数据
	printf("unval addr : %p\n", &g_unval);

	char* mem = (char*)malloc(10);
	// 堆
	printf("heap addr : %p\n", mem);
	// 栈:指针变量(函数内,局部变量)
	printf("stack addr : %p\n", &mem);
	// 第一个命令行参数
	printf("opt addr : %p\n", argv[0]);
	// 最后一个命令行参数
	printf("opt addr : %p\n", argv[argc - 1]);
	// 环境变量
	printf("env addr : %p\n", env[0]);
}

运行结果 ↓
在这里插入图片描述
按照之前的说法,在C程序地址空间当中,地址的增长方向是自底向上。分别是代码段、已初始化全局数据区、未初始化全局数据区、堆区、栈区、命令行参数、环境变量。

其中,堆栈相对而生。栈向地址减小方向生长,堆向地址增大方向生长。堆栈之间的区域称为共享区。

再往上,叫做内核空间。

有两个小细节:
1、代码段不是从0号地址开始的,它有一个确定的地址。
2、共享区里一般放的是动态库,共享内存等。

这是以前的说法,是不准确的。
本文将详细介绍对于地址空间的认知。

首先要纠正的是,之前的程序地址空间应该叫做,进程地址空间

一、简单理解地址空间

我们知道猫咪是具有领地意识的,那么,是不是这片领地,猫咪随时都在使用呢? 不是的。
它只是限定了一片区域,属于它自身的活动范围。

所谓进程(猫咪)的地址空间(活动范围),进程不是随时随地使用这个地址空间内所有的资源。

【第一层理解】地址空间的作用:仅仅限定了一块内存空间,属于当前进程的一段活动范围,并不代表这个进程把所有资源都占有了。

二、虚拟地址

我们先来看一段代码

#include<stdio.h>
#include<unistd.h>
int g_val = 100;

int main()
{
	pid_t id = fork();
	if(id == 0){
		//parent
		g_val = 1000;
		while (1)
		{
			printf("g_val:%d, addr : %p  child \n", g_val, &g_val);
			sleep(1);
		}
	}
	else if (id > 0){
		//child
		while(1)
		{
			sleep(3);
			printf("g_val:%d, addr : %p  father \n", g_val, &g_val);
			sleep(1);
		}
	}
	else{
		//error
	}
}

fork之后,父子进程谁先运行是由调度器决定的(随机的)。
让子进程先运行,修改全局变量g_val的值,观察现象。

在Linux中的运行结果是这样的。
在这里插入图片描述
我们发现,父进程打印的g_val依然是100。同时,父子进程打印的地址是一样的!
内存中的同一个地址,被不同的进程读取,打出来的值却完全不一样。
这是不可能发生的。

【第二层理解】所以,我们可以知道——地址空间绝对不是物理内存

进程和物理内存之间还存在一个虚拟层,叫做进程地址空间。

它其实是一种可被父进程指向的数据结构,父进程能看到的所有的内存的现象(或者叫布局情况)是被地址空间所解释的。而这部分区域可以在物理内存的任意区域被映射,此时就可以通过进程地址空间的内容访问物理内存中被映射的部分。

我们运用语言在屏幕上打印出来的地址其实都叫做“虚拟地址”。

从上面的例子,可以得出结论:
这里的地址,绝对不是物理地址,而是叫做虚拟地址。父子进程访问的数据,绝对被保存到了不同的物理内存中。
(只有物理内存有保存数据的能力,所谓的地址空间它仅仅是一种区域的划分。)


现象解释

为什么同样的地址,呈现的却是不一样的内容?

在创建子进程且子进程没有改写全局变量时,因为没有写入,只是读取,操作系统本着不浪费空间的目的,父子进程默认共享这段空间。
进程的运行是具有独立性的。体现在数据层面上先进行独立!

一旦!有任意一方,对数据进行了修改,父子进程就要进行数据的独立。单独给修改数据的这一方重新开了一块空间,重新构建一种映射关系。

在整个操作过程中,父进程是没有受任何影响的,且对子进程来说,只影响了子进程物理内存单独开辟的空间。它本身的虚拟地址是没有发生变化的,变的只是虚拟地址到物理地址的映射关系。

简单来说就是,
二者虚拟地址相同,但被映射到了不同的物理内存处。所以打出来的值也是不同的!
在这里插入图片描述

【第三层理解】地址空间是对物理内存的一种虚拟化表示。
进程只能看到虚拟地址,操作系统会帮我们把虚拟地址转化为物理地址。

进程的地址空间是内存吗?不是,给进程看到的永远都是虚拟地址,把物理内存保护起来。

三、三个问题搞懂地址空间

1. 什么是地址空间?

  • 本质是描述进程所占有空间的一张表,而它在操作系统内核当中,就是一个数据结构,这个数据结构,将我们的内存划分成了若干种区域,分别对应我们学过的那些区域。

每个进程都有一个进程的地址空间,系统中可能存在多个进程,每个进程透过地址空间访问内存,所以,系统中一定存在多个地址空间。所以,地址空间也要被管理起来。

只要是管理,就要先描述,再组织

  • 先描述:

地址空间本质就是一个数据结构,
在Linux当中,叫做struct mm_struct(linux内核当中的地址空间结构体)
它包含了一些区域信息,能够实现区域划分。

struct mm_struct
{
	//划分区域
	Unsigned long code_start;
	Unsigned long code_end;
	
    Unsigned long init_data_start;
	Unsigned long init_data_end;

	Unsigned long uninit_start;
	Unsigned long uninit_end;
	
	Unsigned long head_start;
	Unsigned long head_end;
	....
}

申请空间的本质是:向内存索要空间,得到物理地址。然后在特定区域申请没有被使用的虚拟地址,建立映射关系,返回虚拟地址即可。

  • 再组织:

进程的PCB(task_struct)里有指向mm_struct的指针。
mm_struct相当于一张进程的执行地图。
我们知道一个进程 = PCB + 代码数据
因此,就把进程的PCB和mm_struct关联起来了。

2. 为什么要有地址空间?

假设如果没有地址空间,那么进程访问的地址都是物理地址。
当发生非法操作或异常的情况时,有可能就破坏其他空间的代码或数据。同时,也大概率会存在一个进程的数据存放的内存位置,是不连续的(代码和数据不连续)。

会导致

  1. 访问特别不方便
  2. 增加了异常越界的概率

为修正这些问题,计算机设计者想到了给每个进程创建一个虚拟地址空间。

解决了问题

  1. 保护物理内存:一个进程只会映射自己的数据,如果越界访问别人的空间,那么在页表当中是查找不到这种映射关系的,操作系统直接进行终止。也就是,只要进入内存访问了,一定是经过页表合法转化过来的。
  2. 页表中有相关的权限管理。可以通过页表的结构来限制一个进程对物理内存空间的访问。
  3. 转换的过程是操作系统做的。从最初的由用户去直接访问物理内存,改成了由操作帮我们访问,那么进程做的任何非法操作,操作系统可识别到。
  4. 同时做了空间连续化处理,方便使用。让我们的地址信息以连续的形式暴露给用户。

3. 地址空间是如何工作的?

地址空间上所呈现暴露给上层的所有的地址都叫做虚拟地址,而实际进程访问时,要通过页表映射转换到物理内存,然后拿到对应的代码和数据。

四、一些补充

通过认识地址空间,我们可以再次清晰进程!

  • 进程从硬盘加载到内存,操作系统为其创建task_struct 和 mm_struct 并且用指针把二者关联起来。
    然后,虚拟地址(mm_struct)通过页表和物理内存建立一种关系。

补充PCB中的内容分类说明:

内存指针:进程的pcb中包含了可以帮我们找到它代码和数据的指针信息。

程序计数器:保存当前正在执行指令的下一条指令的地址。

上下文数据:一个进程在CPU上运行会产生各种临时数据(就叫做当前进程的上下文),CPU内部保存数据的区域只有一份。
当进程切换时:时间片到了,或者有更高优先级的进程过来时,当前进程要发生切换,切换时要进行上下文保护;重新恢复回来时,要进行上下文恢复。保证这个进程能够接着上次被中断的位置继续运行。

  • 21
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值