吃透进程地址空间,理清OS内存管理机制-1

简介: 吃透进程地址空间,理清OS内存管理机制

一、前言

Hello,大家好。本文要给大家带来的是有关Linux中的进程地址空间的讲解

  • 首先我们来看着一张图,相信有学习过 C/C++内存管理 的同学一定可以清楚下面的这张图。知道内存中划分了很多的区域,包括 栈区、堆区、静态区、只读常量区、代码段、共享区等等
  • 但是呢却不知道为什么要存在这样一个分布?以及为什么要这样来分布?

image.png💬 在本文中我将会带大家去理解一下这个进程地址空间

二、细说进程地址空间

1、一段测试的代码


1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <assert.h>
  4 
  5 int g_val = 100;    // 全局变量
  6 
  7 int main(void)
  8 {
  9     pid_t id = fork();
 10     assert(id >= 0);
 11     while(1)
 12     {
 13         if(id == 0)
 14         {
 15             // child
 16             printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n",     getpid(), getppid(), g_val, &g_val);
 17             sleep(1);
 18         }                                                                                   
 19         else
 20         {   
 21             // father
 22             printf("我是父进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n",     getpid(), getppid(), g_val, &g_val);                                                         23             sleep(2);               
 24         }            
 25     }    
 26     return 0;
 27 }
  • 然后我们来看执行的结果,可以发现因为fork()的原因,在返回结果的时候进行了分流,继而父子进程所在的分支就都被执行了,因为父子进程所访问的都是同一个全局变量,所以我们所看到打印出来的结果的值都是一样的,而且地址也都是一样的

image.png

那现在我对上面的代码做一个小小的修改~

  • 在子进程内部我们去修改一下这个g_val的值


if(id == 0)
{
  // child
     printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val);
     sleep(1);
     g_val++;                                                                       
}
  • 然后再去执行一下可以发现 子进程 每次打印出来的g_val一直在递增,但是呢 父进程 所打印出来的g_val却始终没有发生变化,而是保持【100】不变
  • 但是呢很奇怪的一个现象是,父子进程所访问这个全局变量的地址却是同一个,这是为什么?相信有很多同学对此产生了很大的疑惑

image.png

所以由上面的这个现象可以联想到我们在讲解 进程的基本概念 的所提到的相关概念

  • 也就是对于进程而言是具备独立性的,所以子进程对于全局数据的修改,不会影响父进程。这也联想到了一个知识点叫做【写时拷贝】,子进程若是需要修改数据的时候,会将父进程的数据拷贝一份进行修改,而不是直接去进行修改,导致数据出现了问题

那我们便可以发出这样的疑问了:同样去读取一个地址,竟然读到了不同的值,它是一个普通的地址吗?

  • 很明显这不是一个物理地址,还记得我们在讲解 C语言指针 的时候所提到的【地址】吗?那是我么说到指针其实就是地址,现在我们又要深入地去谈一谈了,对于我们之前一直所聊到的地址,其实并不指的是 物理地址,却是一个 虚拟地址
  • 也就是说我所打印出来看到的,其实并不是一个内存中真实的地址,只是我们看到的是这个地址罢了,在内存中所对应的可能又是另一个地址

好,有了虚拟地址的概念,我们来小结一下:

💬 父子进程在访问同一变量的时候,这个变量的地址绝对不是【物理地址】,因为它们读到了不同的值,我们在语言层面所用的地址,叫 虚拟地址 / 线性地址

2、引入地址空间

① 富豪与他的私生子👨

接下去我会先通过一个故事来引入一下虚拟地址空间

:book:在十九世纪美国纽约呢,有一个大富翁,坐拥千万美金,他呢有4个私生子,留下了一笔遗产给到他们💴

  1. 其中【A】是 做生意 的,自己本身就是一个商人,也不是很缺钱。
  2. 其中【B】是 卖化妆品 的,也靠着这个赚了很多钱
  3. 其中【C】正在美国的一所重点大学 -- 哈佛大学 读书,靠着努力学习获得了很多的奖学金
  4. 其中【D】是最小的,在高二的那一年就 辍学 了,现在在混社会

A B C D 呢彼此并不知道彼此的存在image.png那此时大富翁分别对这几个孩子说:

  • 小A啊,你做生意呢就好好做,到时候等我临走的时候就把这10亿美金一并给你
  • 小B啊,你这个化妆品行业最近挺火的,好好干争取上市,到时候再给你一笔钱就当是投资了
  • 小C啊,书要好好读,一定要出人头地,到时候拿着我给你的这一笔钱自己开家公司当老板
  • 小D啊,你要混的话就好好混,争取有一年可以混成想黑帮教父那样,再给你一笔钱就更好过了

于是富翁就给这四个孩子分别都画了张大饼⚪,虽然不知是否会兑现承诺,但是先给每个人都说到这个

那对于我们上面所讲的故事我们可以对应计算机去做一个抽象💻

  • 这个大富翁呢则是对应我们所熟知的【操作系统】,拥有最大的掌管权;
  • 这些孩子们呢就是操作系统中的各个【进程】,由OS来进行管理;
  • 对于富豪给各个孩子所画的饼我们称之为【进程地址空间】,每个饼都对应一个具体的区域;
  • 对于这10亿美金来说呢我们称之为【内存】,用于分配给各个进程来使用;

image.png

那富豪既然给孩子们画了这些饼的话,按需不需要将这些饼给统一收好然后一一分发给各个孩子呢?即操作系统是否需要将各个进程地址空间给组织管理起来

答案是:当然要。因为管理的本质就是 —— ==先描述,再组织==

  • 所以画的这一个个的饼即【进程地址空间】,其实就是一个个的结构体对象,我们将其称作为是mm_struct

② 38线竟是这么来的!

首先我们来看到这个进程地址空间,刚才我们讲到画的这一个个大饼就对应着每个进程的【进程地址空间】

  • 我们看到在这个进程地址空间中有着很多的区域:栈、堆、共享区等等,这些我们在上面就有讲说到过,在这里面的【正文代码段】中呢,可能就有我们在前面所讲到过的虚拟地址,它呢可能并不是内存中的一个实际地址,而是需要通过一定的手段才能找到内存中的那一个物理地址,继而找到正确的内容
  • 那不管是画的饼还是这一个个的分块的区域结构,都是抽象的,我们若是想看到一些实在的东西,就还需要将其转换为 物理层面的内容。就像富豪要给私生子们拿这笔钱就需要去银行里实际地取出来才行(那时没有网银)

image.png

不过对于mm_struct这个地址空间中的【代码区】、【数据区】、【堆区】、【栈区】到底该如何理解?

📕这里我还是通过一个故事来进行引入

  • 小花和小胖两个人呢在一所学校读中学,有一天小胖惹小花生气了,不小心打翻了她的水杯,于是呢小花就在桌子的中间画了一条 “38线”:表示小胖不可以越过这条线,一旦越过的话就算是违规了

image.png

那我现在想问:小花画这条38线的本质是什么?

  • 相信反应快的同学一下子就能说个大概:没错,就是了【区域划分】。那如果使用计算机的术语去表述的话该如何去表述呢?

那就是使用我们在C语言中所学习到的struct结构体,内部呢有【start】和【end】这两个成员 ⇒ 那么对线性区域进行 指定start和end的划分即可完成区域的划分!


struct area
{
  int start;
    int end;
}
  • 对这两块区域去做一个初始化的话就可以像下面这样


struct area xiaohua_area = {1, 50}; 
struct area xiaopang_area = {50 100};

但是呢在某一天呢,小胖又惹小花生气了,于是呢小花把这条线从55对半划到了给小胖只剩 3 的区域,那也就变成了真正的 “38线”

  • 那对应到代码层面我们就可以去做这样一个修整。将小花的末节区域end修改为【80】,并且将小胖的初始区域start设置为【80】


xiaohua_area.end = 80
xiaopang_area.start = 80

image.png

③ 地址空间的深层理解

清楚了什么叫做区域划分之后,我们再通过进一步的理解加深对地址空间认识

  • 但是在上面讲了这么半天【线性划分】和【线性区域】,这我们本节所要讲的 地址空间 有什么关系呢?

那在这里我可以先给出结论:

地址空间本质就是一个线性区域

首先呢我们可以先来理解一下什么叫做【线性空间】👈

  • 我在 C语言指针章节 里也有说到过在32位系统中有32根总线,可以有2^32^个排列组合,即有2^32^个地址,从最低的地址0x00000000到最高的地址0xFFFFFFFF,这里的每一个地址都是连续的,换算成十进制就是 0 ~ 42亿多。所以我们把这个地址空间称之为【线性空间】
  • 因为这些数字是线性的,所以地址空间整体是线性的。每一个数字表示一个地址,一个地址表示一个字节

image.png

知道了什么是线性空间后,我们还要去进一步理解类型的相关概念

  • C语言数据类型章节 以及上面的指针章节我有将其过对于一个数据类型于我们而言可以去区分各种不同的数据;于计算机而言呢例如对于 指针来说决定了它走一步可以跨过多大的范围

💬 所以我们要明白类型存在的意义

  • 在计算机里可以帮我们确认当前变量申请的起始地址,然后会再根据类型来看它到底能取几个字节

在一开始我们就有提到过有关进程地址空间中的各种区域,那现在在熟知了【线性空间】这个概念之后呢,知道了一个区域有,那此时我们如何使用代码去维护这一段段的空间呢?

  • 下面是我在Linux的源代码中节选出来的:
  • 例如【代码段】,它的区域就是使用code_startcode_end来进行维护的
  • 例如【栈区】,它的区域就是使用stack_startstack_end来进行维护的


struct mm_struct
{
    long code_start;
    long code_end;
    long init_start;
    long init_end;
    ...
    long brk_start;
    long brk_end;
    long stack_start;
    long stack_end;       
}

那既然可以使用结构体来表示这些区域的话,那我想请问:那么区域之间的数据叫做什么呢?

  • 例如[1000, 2000]之间的这段数据,12001500 or 1700这些,上面说到过,对于一个区域内的数据都是连续的,而且每一个数字代表一个地址,因此我们可以称它们为【虚拟地址 / 线性地址】

:book: 最后我们来总结一下:

  • 地址空间就是一段线性范围,从全0到全F,换算成十进制就是 0 ~ 42亿多。每一个数组不叫做整数,而叫做地址。因为数字是线性的,每个数字表示一个地址,每个地址对应1字节,因为 CPU寻址的最小单位就是字节,如果需要多个地址的话就连续申请多个字节,但一般是把 首地址 返回,在应用层再根据这个首地址去确定其在内存当中的位置,然后再加上类型,确定所申请的内存空间有多大(类型的本质就叫做偏移量)
  • 那上面说了这么多,其实本质我们还是想要讲一点:==地址空间本身就是线性结构==
相关文章
|
4天前
|
算法 调度 UED
深入理解操作系统内存管理:原理与实践
【4月更文挑战第23天】 在现代计算机系统中,操作系统的内存管理是保证系统高效、稳定运行的关键组成部分。本文旨在深入探讨操作系统中内存管理的理论基础、关键技术以及实际操作过程,通过对内存分配策略、虚拟内存技术、分页与分段机制等核心概念的详细解析,为读者提供一个清晰、全面的内存管理视角。此外,文章还将通过案例分析,展示内存管理在解决实际问题中的应用,以期加深读者对操作系统内存管理复杂性的认识和理解。
|
20天前
|
算法 程序员
深入理解操作系统内存管理:分页系统的优势与挑战
【4月更文挑战第7天】 在现代操作系统中,内存管理是一项至关重要的任务,它确保了计算机能够高效、安全地运行各种程序。分页系统作为内存管理的一种技术,通过将物理内存分割成固定大小的单元——页面,为每个运行的程序提供了一种独立且连续的内存地址空间。该技术不仅简化了内存分配,还允许更高效的内存使用和保护。本文探讨了分页系统的核心原理,优势以及面临的挑战,旨在为读者揭示其在操作系统设计中的重要性。
|
1天前
|
算法
深入理解操作系统的内存管理
【4月更文挑战第26天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键组成部分。本文将深入探讨操作系统内存管理的核心技术,包括虚拟内存、物理内存分配策略、分页和分段机制以及内存交换技术。通过分析这些技术的工作原理及其优缺点,读者将获得对操作系统如何优化内存使用和管理的深刻理解。
|
2天前
|
算法 Linux 调度
深入理解操作系统中的进程调度策略
【4月更文挑战第25天】 在多任务操作系统中,进程调度策略是核心组件之一,它负责决定哪个可运行的进程将获得CPU时间。本文将探讨不同的进程调度算法,包括它们的原理、优缺点以及适用场景。我们将重点分析先到先服务(FCFS)、短作业优先(SJF)、轮转调度(RR)和多级反馈队列(MLFQ)等经典算法,并讨论现代操作系统如Linux和Windows中的实际调度策略。文章的目的是为读者提供对操作系统进程调度机制深度了解,并展示其在系统性能和用户体验中的关键作用。
|
2天前
|
算法
探索现代操作系统的虚拟内存管理
【4月更文挑战第25天】 操作系统的心脏——虚拟内存管理,是确保多任务并发执行和系统稳定性的关键。本文将深入剖析虚拟内存的核心机制,包括分页、分段、请求调页以及交换技术。我们将探讨虚拟内存如何允许操作系统使用有限的物理内存来模拟更大的地址空间,以及这一过程对性能的影响。此外,文章还将介绍一些高级话题,比如内存分配策略、页面置换算法以及虚拟内存的优化方法。
|
2天前
|
负载均衡 算法 调度
深入理解操作系统中的进程调度策略
【4月更文挑战第25天】 在现代操作系统的核心功能中,进程调度策略扮演着至关重要的角色。本文将详细解析进程调度的基本概念、调度算法的种类及其背后的原理,并探讨它们对系统性能的影响。通过比较不同的调度策略,我们可以更深入地理解操作系统如何管理资源,确保多任务环境下的效率和公平性。
|
3天前
|
存储 算法
深入理解操作系统的内存管理机制
【4月更文挑战第24天】 在现代计算机系统中,操作系统扮演着资源管理者的角色,其中内存管理是其核心职责之一。本文将探讨操作系统如何通过内存管理提升系统性能和稳定性,包括物理内存与虚拟内存的概念、分页机制、内存分配策略以及内存交换技术。我们将透过理论与实践的结合,分析内存管理的关键技术及其对系统运行效率的影响。
|
10天前
|
存储 算法 数据安全/隐私保护
深入理解操作系统的内存管理机制
【4月更文挑战第17天】 在现代计算机系统中,操作系统扮演着资源管理者的角色,其中内存管理是其核心职能之一。本文探讨了操作系统内存管理的关键技术,包括虚拟内存、物理内存分配与回收、分页和分段机制,以及内存交换技术。通过分析这些机制的原理和实现,我们旨在加深读者对操作系统如何有效管理和保护内存资源的理解。
11 1
|
12天前
|
算法
深入理解操作系统的内存管理机制
【4月更文挑战第15天】 本文将探讨操作系统中至关重要的一环——内存管理。不同于通常对内存管理概念的浅尝辄止,我们将深入研究其核心原理与实现策略,并剖析其对系统性能和稳定性的影响。文章将详细阐述分页系统、分段技术以及它们在现代操作系统中的应用,同时比较它们的效率与复杂性。通过本文,读者将获得对操作系统内存管理深层次工作机制的洞见,以及对设计高效、稳定内存管理系统的理解。
|
15天前
|
存储 大数据 量子技术
深入理解操作系统的内存管理
【4月更文挑战第12天】 在现代计算机系统中,操作系统扮演着关键角色,它负责协调和管理硬件资源,确保系统运行的高效与稳定。其中,内存管理是操作系统的核心功能之一,它涉及物理内存的分配、虚拟内存的映射以及内存保护等关键操作。本文旨在深入剖析操作系统内存管理的基本原理与实践,探讨其对系统性能和安全性的影响,并简述当前的挑战与创新方向。