程序地址空间:深度解析其结构,原理与在计算机系统中的应用价值

news/2025/3/17 11:51:49/

目录

1. 程序地址空间回顾

1.1 虚拟地址

2.进程地址空间

分页&虚拟地址空间

引入新概念 

解释上述关于同样的地址不同的变量值问题

回答一个历史遗留问题

 ​编辑

3.虚拟内存管理

虚拟内存是什么

虚拟地址空间区域划分

为什么要有虚拟地址空间 


1. 程序地址空间回顾

我们C语⾔的知识板块中,讲过关于存储的知识:

可是我们对他并不理解!可以先对其进⾏各区域分布验证:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}

结果:

$ ./a.out
code addr: 0x40055d
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x1791010
heap addr: 0x1791030
heap addr: 0x1791050
heap addr: 0x1791070
test static addr: 0x601038
stack addr: 0x7ffd0f9a4368
stack addr: 0x7ffd0f9a4360
stack addr: 0x7ffd0f9a4358
stack addr: 0x7ffd0f9a4350
read only string addr: 0x400800
argv[0]: 0x7ffd0f9a4811
env[0]: 0x7ffd0f9a4819
env[1]: 0x7ffd0f9a482e
env[2]: 0x7ffd0f9a4845
env[3]: 0x7ffd0f9a4850
env[4]: 0x7ffd0f9a4860
env[5]: 0x7ffd0f9a486e
env[6]: 0x7ffd0f9a4892
env[7]: 0x7ffd0f9a48a5
env[8]: 0x7ffd0f9a48ae
env[9]: 0x7ffd0f9a48f1
env[10]: 0x7ffd0f9a4e8d
env[11]: 0x7ffd0f9a4ea6
env[12]: 0x7ffd0f9a4f00
env[13]: 0x7ffd0f9a4f13
env[14]: 0x7ffd0f9a4f24
env[15]: 0x7ffd0f9a4f3b
env[16]: 0x7ffd0f9a4f43
env[17]: 0x7ffd0f9a4f52
env[18]: 0x7ffd0f9a4f5e
env[19]: 0x7ffd0f9a4f93
env[20]: 0x7ffd0f9a4fb6
env[21]: 0x7ffd0f9a4fd5
env[22]: 0x7ffd0f9a4fdf

可以看出栈区的存储是由高地址到低地址的,而堆区则相反是由低地址到高地址,它们中间的共享区我们后面再讲。

现在有个问题是:程序地址空间到底是不是内存呢?
先不着急回答,我们可以试验一下,先写一个程序:

定义一个全局变量,让子进程修改它的内容,我们知道如果子进程修改父进程的数据的话,会发生写时拷贝,在内存中另外开辟一个空间存储子进程修改过的数据,理论上来说,发生写时拷贝之后,父进程的这个变量地址与子进程的不一样,那么究竟是不是这样呢,我们来看结果:

我们发现它们的值虽然不一样,但是地址竟然一模一样。

1.1 虚拟地址

我们再看下一个例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

 输出:

//与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8

我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解,因为⼦进程按照⽗进程为模版,⽗⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动: 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再
读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

输出结果:

//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:

(1)变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
(2)但地址值是⼀样的,说明,该地址绝对不是物理地址
(3)在Linux地址下,这种地址叫做 虚拟地址
(4)我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,用户⼀概看不到,由OS统⼀管理 。

OS必须负责将 虚拟地址 转化成 物理地址

2.进程地址空间

所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?

看图:

分页&虚拟地址空间

引入新概念 

在系统中,每一个虚拟地址空间都指向一张叫“页表”的表,它又是干什么用的呢?页表是用来映射虚拟空间地址到物理地址的。现在我们可以给出几个结论,方便下面理解:

(1)一个进程对应一个虚拟地址空间。

(2)一个虚拟地址空间对应一张页表。

(3)页表是用来映射虚拟空间地址到物理地址的。

解释上述关于同样的地址不同的变量值问题

    假设我们有一个进程,它有一个变量我们将它的值设置为100,这个变量会有两个地址,一个是虚拟空间地址,一个是物理地址,都放在页表中,当我们访问这个变量的地址时,通过页表中它的虚拟地址找到它的物理地址进行访问。这时我们创建一个子进程,子进程的数据和代码都会共享父进程的,而页表则是直接复制一张内容一模一样的,这样就发生了浅拷贝,子进程可以通过复制过来的页表的地址访问我们父进程定义的变量:

当子进程对这个变量修改数据时,系统会在物理内存中创建一块新的空间,然后将这块空间的地址替换子进程在页表中的这个变量的物理地址,到这一步,父子进程的这个变量的虚拟地址还是一模一样,而物理地址则发生了改变,这就是写时拷贝。 

回答一个历史遗留问题

在前面的章节我们留下了一个问题没有解答,那就是下图中的第三问——为什么一个变量即大于0,又等于0?

 

答:有两个进程,有进程将id这个变量修改了之后,发生了写时拷贝,父和子进程拿着同样的虚拟地址在自己的页表中拿到了不同的物理地址进行访问!所以一个变量才会即大于0又等于0。

3.虚拟内存管理

虚拟内存是什么

  描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构

在系统中,假设内存为4G,每个进程都认为自己有4个G的空间,像这样被划分好了各个区的内存:

但是我们总共只有四个G的内存,所以,上述每个进程都有4G内存空间是不可能的, 这些空间都是虚拟的,但是既然有这样的空间,就要管理起来,系统是怎样管理的呢?先描述,再组织。没错,这些进程的虚拟地址空间被系统使用结构体描述,使用某种数据结构将它们组织起来了。现在我们知道了,虚拟地址空间是一个个结构体对象,它名为mm_struct。

虚拟地址空间区域划分

  每个虚拟地址空间有4个G的空间(实际并没有),也就是2的32次方个字节,系统通过数字来划分这些字节的数字来给虚拟地址空间的各个内存区划分区域:

在进程源码中找到mm_struct:

往下翻:

这就应证了我们上面所讲的。 

每个进程中包含最多的信息就是这样使用两个数字来描述一块空间的开始和结束,假设有100个字节空间,张三和李四各分50个字节,我们通过以下方式来调整区域划分:

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。

struct task_struct
{
/*...*/
struct mm_struct
*mm; //对于普通的⽤⼾进程来说该字段指向他的
虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。
struct mm_struct
*active_mm; // 该字段是内核线程使⽤的。当该
进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所
有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
/*...*/
}

以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样
每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况: 

定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的,不过他们所在⽂件是不⼀样的,
mm_struct所在的⽂件是mm_types.h。

struct mm_struct
{
/*...*/
struct vm_area_struct *mmap;
/* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb;
/* red_black树 */
unsigned long task_size;
/*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}

那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种: 

1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
2. 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。

linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚
拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进程快速访问。

struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end;
//虚存区结束
struct vm_area_struct *vm_next, *vm_prev;
//前后指针
struct rb_node vm_rb;
//红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm;
//所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags;
//标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff;
//⽂件映射偏移量
struct file * vm_file;
//映射的⽂件
void * vm_private_data;
//私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region;
/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy;
/* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

所以我们可以对上图在进⾏更细致的描述,如下图所⽰:

为什么要有虚拟地址空间 

1.将“无序”变“有序”。由于物理内存是由诸多进程共享的,所以一个进程在物理内存的地址大概率不是有序的,使用虚拟地址空间,只需要将进程的各个地址保存到页表,再给它们映射有序的虚拟空间地址,这样我们就不用管物理地址是否连续了。

2.地址转换过程中,也可以对你的地址和操作进行合法性判定,进而保护物理内存。在页表中存在权限的概念,不同的区权限可能不同,假设我们运行一段代码:

char* str="helloworld";
str="H";

程序会崩溃,为什么呢?因为str存的是字符串常量,字符串常量在常量区,而常量区在页表中的权限是只读,代码在查找页表时,权限拦截了,操作与权限冲突,程序当然不能执行了。 

3.让进程管理与内存管理进行一定程度的解耦合。

本章完。


http://www.ppmy.cn/news/1579799.html

相关文章

C++:二分习题

1. 借教室 503. 借教室 - AcWing题库 在大学期间&#xff0c;经常需要租借教室。 大到院系举办活动&#xff0c;小到学习小组自习讨论&#xff0c;都需要向学校申请借教室。 教室的大小功能不同&#xff0c;借教室人的身份不同&#xff0c;借教室的手续也不一样。  面对海…

ICMP 协议

文章目录 ICMP 协议概述数据包格式实例分析ICMP 请求ICMP 应答 参考 本文为笔者学习以太网对网上资料归纳整理所做的笔记&#xff0c;文末均附有参考链接&#xff0c;如侵权&#xff0c;请联系删除。 ICMP 协议 概述 ICMP&#xff08;Internet Control Message Protocol&…

机器学习中的激活函数是什么起什么作用

在机器学习&#xff0c;尤其是神经网络中&#xff0c;​激活函数​&#xff08;Activation Function&#xff09;是一个非常重要的组件。它的主要作用是为神经网络引入非线性&#xff0c;从而使神经网络能够学习和表示复杂的模式或函数。 1.激活函数的定义 激活函数是一个数学…

Matlab 汽车振动多自由度非线性悬挂系统和参数研究

1、内容简介 略 Matlab 169-汽车振动多自由度非线性悬挂系统和参数研究 可以交流、咨询、答疑 2、内容说明 略 第二章 汽车模型建立 2.1 汽车悬架系统概述 2.1.1 悬架系统的结构和功能 2.1.2 悬架分类 2.2 四分之一车辆模型 对于车辆动力学&#xff0c;一般都是研究其悬…

90.HarmonyOS NEXT应用发布与版本管理指南:规范化发布流程

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; HarmonyOS NEXT应用发布与版本管理指南&#xff1a;规范化发布流程 文章目录 HarmonyOS NEXT应用发布与版本管理指南&#xff1a;规范化发布流程1.…

Linux基础开发工具—vim

目录 1、vim的概念 2、vim的常见模式 2.1 演示切换vim模式 3、vim命令模式常用操作 3.1 移动光标 3.2 删除文字 3.3 复制 3.4 替换 4、vim底行模式常用命令 4.1 查找字符 5、vim的配置文件 1、vim的概念 Vim全称是Vi IMproved&#xff0c;即说明它是Vi编辑器的增强…

ffmpeg + opencv 打静态库编译到可执行文件中

下载ffmpeg ,我下载的为6.0 版本,解压后执行: ./configure --enable-static --disable-shared --pkg-config-flags=“–static” --extra-cflags=“-fPIC” --extra-cxxflags=“-fPIC” --prefix=/usr/local2.等待配置完成,执行 make && make install 进行编译安装…

【零基础入门unity游戏开发——unity3D篇】3D物理系统之 —— 3D碰撞器Collider

考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…