当前位置: 首页 > article >正文

【Linux系统编程】—— 虚拟内存与进程地址空间的管理:操作系统如何实现内存保护与高效分配

文章目录

  • 程序地址空间的概念
  • 虚拟地址与进程的关系
  • 进程地址空间
  • 进程地址空间的结构
  • 为什么使用虚拟内存(虚拟地址空间)

前言: 在现代操作系统中,进程的内存管理至关重要。操作系统通过虚拟地址空间来隔离不同进程的内存,确保它们不互相干扰,同时也能够高效地管理有限的物理内存资源。在本文中,我们将详细探讨虚拟地址空间、进程地址空间的概念,并通过具体代码示例帮助理解。

程序地址空间的概念

当我们在学习编程语言(如C语言)时,可能会遇到程序地址空间的概念。程序的地址空间是指在内存中为程序分配的一个虚拟地址区域,这个区域划分了代码段、数据段、堆、栈等不同的内存区域。

地址空间布局示例
以下是一个典型的程序地址空间布局:

  • 代码段(Text Segment):存放程序的代码。通常是只读的。
  • 数据段(Data Segment):存放已初始化的全局变量和静态变量。
  • BSS段:存放未初始化的全局变量和静态变量。
  • 堆(Heap):用于动态分配内存(如通过malloc分配的内存)。
  • 栈(Stack):用于存储局部变量和函数调用信息。
    在这里插入图片描述
#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;
}
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

通过这些输出,可以看到不同内存区域(如代码段、数据段、堆、栈和只读数据段)的地址分布和特点。堆的地址通常递增,栈的地址则通常递减,静态变量和全局变量则存储在数据段。

虚拟地址与进程的关系

在进程中,每个变量和内存段都有一个虚拟地址。虚拟地址并不直接对应物理内存中的地址,而是通过操作系统的地址映射机制转化为物理地址。

虚拟地址示例:进程间的地址映射
通过fork()函数,我们可以创建一个子进程。子进程会复制父进程的内存空间,但由于虚拟地址的存在,它们看到的地址相同,但物理地址不同。这是因为操作系统为每个进程分配了独立的虚拟地址空间。

#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

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

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

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

进程地址空间

所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
在这里插入图片描述
上⾯的图就⾜矣说明问题,同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

进程地址空间的结构

每个进程在操作系统中都有自己的地址空间,这个地址空间由多个虚拟内存区域组成。操作系统通过mm_struct结构来管理进程的虚拟内存空间。

mm_struct包含以下关键字段:

  • mmap:指向虚拟内存区域(VMA)的链表。
  • task_size:该进程虚拟内存的大小。
  • start_code, end_code, start_data, end_data:表示代码段和数据段的起始和结束地址。

以下是mm_struct的结构定义:

struct mm_struct {
    struct vm_area_struct *mmap;
    struct rb_root mm_rb;
    unsigned long task_size;
    unsigned long start_code, end_code;
    unsigned long start_data, end_data;
    unsigned long start_brk, brk;
    unsigned long start_stack;
    unsigned long arg_start, arg_end;
    unsigned long env_start, env_end;
};

可以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:
在这里插入图片描述
定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的,不过他们所在⽂件是不⼀样的,mm_struct所在的⽂件是mm_types.h。

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

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

vm_area_struct是描述虚拟内存区域(VMA)的结构。每个VMA表示进程的一个内存区域,如代码段、数据段、堆等。

在这里插入图片描述
在这里插入图片描述

为什么使用虚拟内存(虚拟地址空间)

这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?

下面一个小故事来进行描述:

在一个古老的计算机世界里,有两位程序A和B,它们都急切地等待着能够在内存中找到属于自己的位置。那时,计算机的内存就像一个巨大的仓库,程序们必须在这个仓库中找到合适的空间才能开始工作。
这台计算机的内存总共只有128MB,程序A和程序B各自需要自己的地方。程序A是一个小巧的任务,它只需要10MB的内存,而程序B则需要一个庞大的空间,要求高达110MB的内存。为了让它们顺利运行,操作系统就像是一位聪明的管理员,开始安排它们的宿位。
首先,管理员决定将仓库的前10MB腾给程序A,让它可以在这里忙碌。程序A在自己的空间里工作得非常高效,没有被其他程序打扰。接着,剩余的118MB空间就成了程序B的家,操作系统小心翼翼地为程序B分配了110MB的内存。程序B也开始了它的工作,虽然它需要的空间更大,但由于操作系统的精心安排,程序B能够在剩余的空间中毫不费力地运行。
两个程序在这台计算机的内存中各自找到了合适的位置,操作系统巧妙地管理着内存,确保它们都能顺利地完成自己的任务。就这样,两个程序在各自的空间里忙碌着,计算机的内存得到了高效的利用,而它们也完成了自己的使命。
在这里插入图片描述
这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。

  1. 安全风险
    在没有虚拟内存的情况下,每个进程都可以随意访问内存中的任意区域,这意味着恶意进程可能会读写系统关键内存区域。例如,如果一个木马病毒得以运行,它可以轻松地修改内存内容,甚至直接破坏操作系统或其他进程的数据,从而导致系统崩溃。

  2. 地址不确定性
    我们都知道,编译完成的程序通常存储在硬盘上,当程序运行时,操作系统会将其加载到内存中。如果直接使用物理内存地址来访问,程序无法确定它每次执行时的具体内存位置。例如,在第一次执行a.out时,内存中没有其他进程运行,所以程序可能被加载到地址0x00000000;但第二次执行时,内存中可能已经有多个进程运行,程序的加载地址就会发生变化,无法预知。这样,程序就无法稳定地使用固定的内存地址,带来了不确定性。

  3. 效率低下
    如果程序直接使用物理内存地址,它将作为一个整体(内存块)进行操作。在物理内存不足时,操作系统可能会将不常用的进程移至磁盘的交换分区以释放内存空间。然而,如果直接操作物理内存,整个进程都需要被转移,这会导致大量的数据拷贝在内存和磁盘之间来回传输。这样的操作会造成很高的延迟,极大地影响系统的效率。

存在这么多问题,有了虚拟地址空间和分⻚机制就能解决了吗?当然!

  • 地址空间和页表是由操作系统创建并维护的,这意味着任何需要使用地址空间和页表进行映射的操作,都必须在操作系统的监管下进行访问。这不仅包括用户进程的数据,还涵盖了内核的有效数据。通过这种机制,操作系统能够有效保护物理内存中的所有合法数据,防止恶意程序访问或修改不应接触的内存区域。

  • 由于地址空间和页表的存在,操作系统可以将物理内存中的数据加载到任意位置,而不必考虑物理内存和进程管理之间的直接关系。这使得操作系统的进程管理模块和内存管理模块能够实现解耦,从而提高了系统的灵活性和效率。

  • 具体来说,当我们在C或C++语言中使用new或malloc申请内存时,实际上是在申请虚拟地址空间,而不直接涉及物理内存。物理内存的分配可以是零,甚至在程序实际访问物理内存之前,操作系统才会按照需要执行内存分配,并构建相应的页表映射关系。这一过程是由操作系统自动管理的,用户和进程对此完全无感知。

  • 通过页表映射的存在,程序可以在物理内存中的任意位置加载。操作系统负责将虚拟地址空间和物理地址之间进行映射,从而使得在进程的视角下,所有内存分布看起来是有序的。这种机制大大简化了程序员的内存管理工作,并为现代操作系统提供了更高效、更安全的内存管理方案。


http://www.kler.cn/a/510738.html

相关文章:

  • 总结3..
  • 基于SpringBoot+Vue的智慧动物园管理系统的设计与实现
  • 深度学习 DAY1:RNN 神经网络及其变体网络(LSTM、GRU)
  • -bash: /java: cannot execute binary file
  • word转pdf
  • 密钥轮换时,老数据该如何处理
  • 算法日记6.StarryCoding P52:我们都需要0(异或)
  • Hugging Face功能介绍,及在线体验文生图模型Flux
  • 202509读书笔记|《飞花令·山》——两岸猿声啼不住,轻舟已过万重山
  • Solidity04 Solidity值类型
  • LLMs之Dataset:中文互联网基础语料2.0的简介、下载和使用方法、案例应用之详细攻略
  • 【2024年华为OD机试】 (B卷,100分)- 字符串分割(Java JS PythonC/C++)
  • 【服务器】Ubuntu22.04配置静态ip
  • 【论文阅读】End-to-End Adversarial-Attention Network for Multi-Modal Clustering
  • 第13章:Python TDD完善货币加法运算(二)
  • 【MyDB】3-DataManager数据管理 之 4-数据页缓存
  • 综述:大语言模型在机器人导航中的最新进展!
  • 【机器学习】机器学习引领数学难题攻克:迈向未知数学领域的新突破
  • YOLOv9改进,YOLOv9检测头融合,适合目标检测、分割任务
  • 第6章:Python TDD实例变量私有化探索
  • 推荐一个开源的轻量级任务调度器!TaskScheduler!
  • 基于单片机的智能家居控制系统设计及应用
  • 利用R计算一般配合力(GCA)和特殊配合力(SCA)
  • Go Map 源码分析(一)
  • Windows蓝牙驱动开发-蓝牙 IOCTL
  • “AI 辅助决策系统:决策路上的智慧领航员