Linux系统编程14:进程入门之Linux进程中非常重要的概念之进程地址空间-原来我们看到的地址全部是虚拟的

本文介绍了Linux系统中进程地址空间的概念,包括虚拟地址与物理地址的区别,进程地址空间的作用,以及页表在地址映射中的关键角色。通过示例程序展示了同一虚拟地址在不同进程中对应不同物理地址的现象,强调了进程间隔离的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(1)旧知回顾

学习C/C++总免不了这张图
在这里插入图片描述
这张图帮助我们解决了不少C/C++问题,但是我们对它的认识还是不够深刻。现在我我们用一端C程序,深刻的取感受一下这个过程的内存地址的变化

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

int gobal_val=100;//全局变量已经初始化
int gobal_unval;//全局变量未初始化
int main(int argc,char* argv[],char* env[])
{
	printf("main函数处于代码段,地址为:%p,十进制为:%d\n",main,main);
	printf("\n");
	printf("全局变量gobal_val,地址为:%p,十进制为:%d\n",&gobal_val,&gobal_val);
	printf("\n");
	printf("全局变量未初始化gobal_unval,地址为:%p,十进制为:%d\n",&gobal_unval,&gobal_unval);
	printf("\n");
	
	char* mem=(char*)malloc(10);
	
	printf("mem开辟的堆空间,mem是堆的起始地址,是%p,十进制为:%d\n",mem,mem);
	printf("\n");	
	printf("mem是指针变量,指针变量在栈上开采,其地址为%p,十进制为:%d\n",&mem,&mem);
	printf("\n");	
	printf("命令行参数起始地址:%p,十进制为:%d\n",argv[0],argv[0]);
	printf("\n");
	printf("命令行参数结束地址:%p,十进制为:%d\n",argv[argc-1],argv[argc-1]);
	printf("\n");
	printf("第一个环境变量的地址:%p,十进制为:%d\n",env[0],env[0]);
	printf("\n");
}

在这里插入图片描述

(2)程序地址空间?

上面的图在学习C/C++时我们称之为程序的地址空间分布图,说白了就是一段程序在其运行过程中的数据的内存分布情况,但是接下来的叙述可能和你所认为的有所出入

A:同一个地址有两个数据?

如下C程序,有个全局变量val,初始值为0,使用fork创建一个子进程,然后让父进程先睡眠三秒,先让子进程运行,子进程运行时把val改为100,三秒后父子进程同时运行

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

int val=0;
int main()
{
	pid_t id=fork();
	if(id==0)
	{
		val=100;
		while(1)
		{
			printf("现在是子进程:val=%d,其地址为%p\n",val,&val);
			sleep(1);
		}
	}
	else if(id>0)
	{
		sleep(3);
		while(1)
		{
			printf("############################################\n")
			printf("现在是父进程:val=%d,其地址为%p\n",val,&val);
			sleep(1);
		}
	}
	else
	{
		exit(-1);
	}
	sleep(1);
	return 0;

}

效果如下,问题就在于同一个变量怎么会同时具有两个不同的值?
在这里插入图片描述

B:物理地址和虚拟地址

我们能够很明确一点:同一片空间不可同时具有两个值,就像现实生活中,你不可能同时再学校又同时在家。那么只有一种解释:你所看到并不是真实的,也就是说你所看到的地址并不是真实的物理地址,而是表象上的相同,他们对应的真实的物理地址肯定是不相同的,我们称这种地址为虚拟地址

实际上:我们使用C/C++看到的地址,全部都是虚拟地址,真实的地址用户看不到,而操作系统就负责将虚拟地址转换为物理地址

可以这样描述:父进程启动(它的内存空间也是虚拟的不是真实的),然后遇见fork就创建了一个子进程,fork就把父进程的内存状态拷贝了给自己一份,如果val的值没有改变,那么操作系统本着不浪费一点空间原则,直接使用val这个虚拟地址所对应的物理地址就可以了。但是好景不长,子进程把val修改掉了,所以操作系统就把子进程val的虚拟地址所对应的物理地址进行了修改。从表面看起来好似地址没有变,但是真实情况早都变化了。

在这里插入图片描述

C:进程地址空间及作用

1:早期直接访问物理内存的缺陷所在

我们知道进程=进程控制块PCB+代码+数据,早期计算机,也就是没有进程地址空间的时候进程一旦启动,这些东西就会被全部装入内存,那么此时进程访问的就是真实的物理内存,如果画一张图,应该就是下面这样
在这里插入图片描述
但是这样防止有很大坏处:比如说野指针,放的这么近,一旦指针访问的别的进程的数据,这就出了大乱子了。还有进程在运行过程中,是会产生数据的,产生的数据一旦不能放在本进程后面,就要另外找内存去存放,这样就会导致不连续的现象,也增加了异常访问的情况

2:进程地址空间的发明

所以计算机设计者意识到了这种模式缺陷,想到了一种方法:增加一个中间层,利用中间层映射物理内存。程序访问内存时不直接访问物理内存,先访问中间层,如果中间层访问没有问题,那么操作系统就会将中间层映射到物理层,完成正常执行

一个进程创建之后,操作系统会为这个进程分配一个专属于它的大小为4GB的虚拟进程地址空间(4GB是因为32位系统中,指针是4个字节),与它相对的是一片真实的物理地址空间,操作系统在映射虚拟内存时只会映射到那一片物理空间,而且需要特别注意这个虚拟空间并不是真的有4GB,它只是虚拟的 。由于每一个进程都有自己的虚拟的进程地址空间,所以它只能访问自己的进程的数据,这样做实现了隔离,也就是进程之间的相互独立。并且把虚拟地址空间划分为这样,那样的区,这样的话也能解决数据的连续存放
在这里插入图片描述
3:页表
不管怎样,程序运行一定要在物理内存上,所以如何映射成为了关键,,这种映射称为页表
映射时,让虚拟地址和物理地址一一对应,进程在虚拟地址上的地址就对应了物理内存上的某个地址。这样的话就不存在非法访问了,因为每个进程都有自己独立的进程地址空间,如果非法访问,页表上根本就找不到这样的地址,所以操作系统拒绝映射。“代码,字符串是只读的,数据是可读可写的”就是基于这个原因,虽然咋页表上能找到,但是对代码,字符串做了权限限制,一旦监测到这些数据类型要做一些写入操作,那么同样操作系统拒绝映射

在这里插入图片描述

D:进程地址空间如何工作

还是那个观点“先描述,再组织”,也就是说我们看见的那个经典的栈,堆分布图,其实本质也是一个结构体,这个结构体隶属于task_struct,叫做task mm_struct(文件在mm_types.h)

在这里插入图片描述
这张图可以说明task_stuct和task mm_struct的关系
在这里插入图片描述
申请空间的本质就是想内存索要空间,得到物理地址,然后在特定区域申请没有使用的虚拟地址,建立映射关系,再返回虚拟地址

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐江湖

创作不易,感谢支持!

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

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

打赏作者

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

抵扣说明:

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

余额充值