吃透进程地址空间,理清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
深入理解操作系统:进程调度与优先级队列
【10月更文挑战第31天】在计算机科学的广阔天地中,操作系统扮演着枢纽的角色,它不仅管理着硬件资源,还为应用程序提供了运行的环境。本文将深入浅出地探讨操作系统的核心概念之一——进程调度,以及如何通过优先级队列来优化资源分配。我们将从基础理论出发,逐步过渡到实际应用,最终以代码示例巩固知识点,旨在为读者揭开操作系统高效管理的神秘面纱。
|
1天前
|
算法 调度 UED
深入理解操作系统:进程管理与调度策略
【10月更文挑战第34天】本文旨在探讨操作系统中至关重要的一环——进程管理及其调度策略。我们将从基础概念入手,逐步揭示进程的生命周期、状态转换以及调度算法的核心原理。文章将通过浅显易懂的语言和具体实例,引导读者理解操作系统如何高效地管理和调度进程,保证系统资源的合理分配和利用。无论你是初学者还是有一定经验的开发者,这篇文章都能为你提供新的视角和深入的理解。
12 3
|
3天前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
4天前
|
算法 调度 UED
深入理解操作系统的进程调度机制
本文旨在探讨操作系统中至关重要的组成部分之一——进程调度机制。通过详细解析进程调度的概念、目的、类型以及实现方式,本文为读者提供了一个全面了解操作系统如何高效管理进程资源的视角。此外,文章还简要介绍了几种常见的进程调度算法,并分析了它们的优缺点,旨在帮助读者更好地理解操作系统内部的复杂性及其对系统性能的影响。
|
5天前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
15 2
|
5天前
|
消息中间件 算法 Linux
深入理解操作系统之进程管理
【10月更文挑战第30天】在数字时代的浪潮中,操作系统作为计算机系统的核心,扮演着至关重要的角色。本文将深入浅出地探讨操作系统中的进程管理机制,从进程的概念入手,逐步解析进程的创建、调度、同步与通信等关键过程,并通过实际代码示例,揭示这些理论在Linux系统中的应用。文章旨在为读者提供一扇窥探操作系统深层工作机制的窗口,同时激发对计算科学深层次理解的兴趣和思考。
|
6天前
|
消息中间件 算法 调度
深入理解操作系统:进程管理与调度策略
【10月更文挑战第29天】本文将带领读者深入探讨操作系统中的核心组件之一——进程,并分析进程管理的重要性。我们将从进程的生命周期入手,逐步揭示进程状态转换、进程调度算法以及优先级调度等关键概念。通过理论讲解与代码演示相结合的方式,本文旨在为读者提供对进程调度机制的全面理解,从而帮助读者更好地掌握操作系统的精髓。
19 1
|
6天前
|
算法 调度 UED
深入理解操作系统中的进程调度
【10月更文挑战第29天】探索进程调度的奥秘,本文将带你深入了解在操作系统中如何管理和控制多个并发执行的程序。从简单的调度算法到复杂的多级反馈队列,我们将逐步揭示如何优化系统性能和提高资源利用率。准备好一起揭开进程调度的神秘面纱吧!
|
7天前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
10天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
175 1