吃透进程地址空间,理清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寻址的最小单位就是字节,如果需要多个地址的话就连续申请多个字节,但一般是把 首地址 返回,在应用层再根据这个首地址去确定其在内存当中的位置,然后再加上类型,确定所申请的内存空间有多大(类型的本质就叫做偏移量)
  • 那上面说了这么多,其实本质我们还是想要讲一点:==地址空间本身就是线性结构==
相关文章
|
21天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
47 1
|
25天前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
11天前
|
运维 监控 Ubuntu
【运维】如何在Ubuntu中设置一个内存守护进程来确保内存不会溢出
通过设置内存守护进程,可以有效监控和管理系统内存使用情况,防止内存溢出带来的系统崩溃和服务中断。本文介绍了如何在Ubuntu中编写和配置内存守护脚本,并将其设置为systemd服务。通过这种方式,可以在内存使用超过设定阈值时自动采取措施,确保系统稳定运行。
29 4
|
23天前
|
C语言 开发者 内存技术
探索操作系统核心:从进程管理到内存分配
本文将深入探讨操作系统的两大核心功能——进程管理和内存分配。通过直观的代码示例,我们将了解如何在操作系统中实现这些基本功能,以及它们如何影响系统性能和稳定性。文章旨在为读者提供一个清晰的操作系统内部工作机制视角,同时强调理解和掌握这些概念对于任何软件开发人员的重要性。
|
23天前
|
Linux 调度 C语言
深入理解操作系统:从进程管理到内存优化
本文旨在为读者提供一次深入浅出的操作系统之旅,从进程管理的基本概念出发,逐步探索到内存管理的高级技巧。我们将通过实际代码示例,揭示操作系统如何高效地调度和优化资源,确保系统稳定运行。无论你是初学者还是有一定基础的开发者,这篇文章都将为你打开一扇了解操作系统深层工作原理的大门。
|
24天前
|
存储 算法 调度
深入理解操作系统:进程调度的奥秘
在数字世界的心脏跳动着的是操作系统,它如同一个无形的指挥官,协调着每一个程序和进程。本文将揭开操作系统中进程调度的神秘面纱,带你领略时间片轮转、优先级调度等策略背后的智慧。从理论到实践,我们将一起探索如何通过代码示例来模拟简单的进程调度,从而更深刻地理解这一核心机制。准备好跟随我的步伐,一起走进操作系统的世界吧!
|
23天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
24天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
27天前
|
算法 Linux 调度
深入浅出操作系统的进程管理
本文通过浅显易懂的语言,向读者介绍了操作系统中一个核心概念——进程管理。我们将从进程的定义出发,逐步深入到进程的创建、调度、同步以及终止等关键环节,并穿插代码示例来直观展示进程管理的实现。文章旨在帮助初学者构建起对操作系统进程管理机制的初步认识,同时为有一定基础的读者提供温故知新的契机。
|
27天前
|
消息中间件 算法 调度
深入理解操作系统之进程管理
本文旨在通过深入浅出的方式,带领读者探索操作系统中的核心概念——进程管理。我们将从进程的定义和重要性出发,逐步解析进程状态、进程调度、以及进程同步与通信等关键知识点。文章将结合具体代码示例,帮助读者构建起对进程管理机制的全面认识,并在实践中加深理解。