进程地址空间

一、什么是进程地址空间?

在以前C/C++的学习中,我们都是以该图来理解内存划分的。

u=714782255,2591046027&fm=253&fmt=auto&app=138&f=JPEG

我们可以编写代码来验证一下地址划分是否正确。

#include<stdio.h>
#include<stdlib.h>
int a;
int b = 0;
int main()
{
    const char* str = "hello";
    printf("code addr:%p\n", main);//代码区
    printf("only read addr:%p\n", str);//字符常量区
    printf("init global value addr:%p\n", &b);//初始化全局区
    printf("uninit global value addr:%p\n", &a);//未初始化全局区

    char* p1 = (char*)malloc(1);
    printf("heap p1 addr:%p\n", p1);//堆区

    printf("stack addr:%p\n", &str);//栈区
    return 0;
}

image-20240720200052685

可以看到堆空间和栈空间有很大的镂空,堆的地址低,栈的地址高:

#include<stdio.h>
#include<stdlib.h>
int a;
int b = 0;
int main()
{
    const char* str = "hello";
    printf("code addr:%p\n", main);//代码区
    printf("only read addr:%p\n", str);//字符常量区
    printf("init global value addr:%p\n", &b);//初始化全局区
    printf("uninit global value addr:%p\n", &a);//未初始化全局区

    char* p1 = (char*)malloc(1);
    char* p2 = (char*)malloc(1);
    char* p3 = (char*)malloc(1);
    char* p4 = (char*)malloc(1);
    printf("heap p1 addr:%p\n", p1);//堆区
    printf("heap p2 addr:%p\n", p2);//堆区
    printf("heap p3 addr:%p\n", p3);//堆区
    printf("heap p4 addr:%p\n", p4);//堆区

    printf("stack addr1:%p\n", &p1);//栈区
    printf("stack addr2:%p\n", &p2);//栈区
    printf("stack addr3:%p\n", &p3);//栈区
    printf("stack addr4:%p\n", &p3);//栈区

    return 0;
}

image-20240720201228763

可以验证出,栈首先使用的是最高的地址,堆首先使用的是最低的地址,所以说堆栈是相对而生的!

这种结构叫做进程地址空间,进程地址空间是内存吗?没学习系统之前,我以为是,来验证一下。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val = 100;
int main()
{
	pid_t id = fork();
	if (id == 0)//子进程
	{
		int cnt = 5;
		while (cnt)
		{
			printf("I am child,pid:%d,ppid:%d,g_val:%d,&g_val=%p\n", getpid(), getppid(), g_val, &g_val);
			cnt--;
			sleep(1);
		}
		if (cnt == 0)
		{
			g_val = 200;
			printf("child change g_val:%d,address:%p\n", g_val,&g_val);
		}
	}
	else//父进程
	{
		while (1)
		{
			printf("I am father,pid:%d,ppid:%d,g_val:%d,&g_val=%p\n", getpid(), getppid(), g_val, &g_val);
			sleep(1);
		}
	}
	return 0;
}

image-20240720203329991

父子进程同时运行五秒钟,五秒后,子进程修改了g_val的值,父进程继续运行,子进程变成僵尸进程,

子进程修改200的值后,地址居然和之前100的地址是一样的,太离谱了,同样的地址,怎么可能有两个不同的值,所以可以肯定的是该地址不是物理地址。

故而进程地址空间不是内存地址!!!

二、页表中的虚拟地址和物理地址

进程地址空间不是内存地址,那我们的程序是如何使用内存地址的呢?这就不得不说到页表了。

我们知道进程=内核数据结构PCB+代码和数据。

在PCB结构体中,有一个mm_struct结构体,这就是进程地址空间。

故而进程运行起来之后,都会有一个进程地址空间的存在!

为了进程地址空间和内存的映射,OS还会管理一个结构:页表。页表有虚拟地址和物理地址的对应关系。

image-20240720210221388

进程地址空间中的地址都在页表中以虚拟地址的形式存在,然后通过页表的映射关系,对应到物理地址上去。

子进程会以父进程为模版,拷贝复制一份,代码共享,数据使用写时拷贝技术。

当子进程修改g_val的值为200时,内存会开辟一块新空间出来,然后将100的值拷贝到新空间中,然后再将100的值修改为200,然后重新和页表建立连接。

image-20240720211019688

这里有个小小的问题,为什么开辟一块新空间了之后,还要将100的值拷贝过来,再进行修改。将100的值拷贝过来不是多此一举吗?这是因为这里这块空间只有100这个值,以后可能不止这些东西,而且我们不需要全部修改,只需要修改一小部分,所以需要先将原先的内容全部拷贝到新空间中。

三、如何管理进程地址空间-区域划分

进程地址空间也要被OS给管理起来。

进程地址空间也是一个结构体,故而同样进程的六字箴言同时适用-先描述再组织

在OS中,每一个进程都有一个进程地址空间,而进程地址空间的大小为4GB(32位系统),这是OS给每一个进程画的大饼。进程都以为自己有4GB的空间。

而进程地址空间划分为这么多区域,OS是如何识别它们的呢?这就不得不说到进程地址空间的mm_struct结构体了,mm_struct中的字段通过区域划分的方式来区分。

mm_strucr中的内核代码:

5e9a324156ad3718e478204c2732f2a7

可以清楚的看到,进程地址空间中的区域通过各种start和end划分出来。

四、为什么存在进程地址空间

1.让进程以统一的视角来看待内存

对于物理内存的使用,是内存哪里有空间,就给我们申请哪里的空间,是很混乱的,通过页表的映射和进程地址空间,可以将物理内存分门别类的规划好。

乱序的内存数据,变成有序的。

2.访问内存进行安全检查

在页表中不止有虚拟地址和物理的映射,还有访问权限字段。

image-20240720214530257

const char*p="hello world";
*p="xxx";

一个常量字符串,它是只读的,显然是不能修改的。常量字符串在页表中的访问权限是只读的,当你修改该字符串时,OS检测到该操作是不行的,产生错误或者异常,保证程序的稳定性和安全性。

3.进程管理和内存管理进行解耦

对于进程管理,每个进程都有自己独立的虚拟地址空间,进程的创建、切换和销毁等操作主要基于其虚拟地址空间的管理,而无需直接关心物理内存的具体分配细节。

对于内存管理,物理内存的分配、回收和页面的换入换出等操作可以独立进行,而不需要依赖特定的进程。页表用于在进程运行时动态地将虚拟地址转换为物理地址,使得内存管理系统可以灵活地分配和调整物理内存,而不影响进程对其地址空间的感知。

这种解耦带来了很多好处,例如提高了内存的利用率、增强了系统的稳定性和安全性,也使得进程的创建和切换更加高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值