【Linux进程(五)】进程地址空间深入剖析-->虚拟地址、物理地址、逻辑地址的区分

🎬 个人主页:HABuo

📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、语言层级的地址空间

📚二、虚拟地址空间

📚三、虚拟地址空间的设计

📚四、虚拟地址与物理地址之间的关联

📚五、为什么要有虚拟地址空间

📚六、总结


前言

上篇博客我们介绍了进程切换、环境变量相关知识,本篇博客将带领你领略Linux头一座大山:虚拟地址空间,可能内容比较难理解,但是不要害怕,我都理解了你还害怕什么,经过本篇博客的学习之后,在C/C++中动不动什么栈区什么代码区这区那区的,这些都是在哪,用来干啥的,就全都通透了!所以慢慢来,别着急!

本章重点:

本篇文章着重讲解进程中的虚拟地址和物理地址的关系,了解虚拟地址的内核本质是什么 ,以及页表和物理地址的映射逻辑和写时拷贝的具体体现,期间我们将回答三个问题:

  • 什么是地址空间?
  • 地址空间是如何设计的?
  • 为什么要有地址空间?

📚一、语言层级的地址空间

C/C++程序员认为,程序的内存分布是这样的:

需要注意:

  • 堆是向上增长的,栈是向下增长的。
  • 在写C程序时定义的常量字符串实际上在上面区域的正文代码区,因为它和正文代码区紧挨着,所以常量区的字符串不允许修改!(只不过这种是传统UNIX术语中所提到的,现代术语进行了更精确的划分,如下)
cpp 复制代码
高地址
┌─────────────────┐
│  栈(stack)      │ ← 局部变量
├─────────────────┤
│   ↓             │
│   (共享区)       │
│   ↑             │
├─────────────────┤
│  堆(heap)       │ ← malloc分配
├─────────────────┤
│  .bss           │ ← 未初始化静态/全局
├─────────────────┤
│  .data          │ ← 已初始化静态/全局
├─────────────────┤
│  .rodata        │ ← 常量字符串等
├─────────────────┤
│  .text          │ ← 代码段
└─────────────────┘
低地址
  • 第二点是,栈虽然说是向下增长的但是栈中的数组,结构体等结构的地址是向上增长的,比如说开辟数组时,开辟十个空间,那么数组中第一个元素在空间的最下面,也就是地址最低处,然后依次往上放后面的元素。

上述理解是我们在学习C/C++时所知道的,我们认为程序的地址就是上述的地址,但对吗?从来没人给我们说对不对,所有人都是硬着头皮说,我们也硬着头皮去接受,但是今天我就告诉你,错的,啊?不能你说错就错,那我前面的努力学习算什么!别急,见下面的例子你就知道是对是错!

cpp 复制代码
#include<stdio.h>    
#include <unistd.h>
#include <sys/types.h>
#include<stdlib.h>
int tmp = 10;
int main()
{
    printf("fork之前,tmp: %d,&tmp:%p\n", tmp, &tmp); 
    int id=fork();
    if(id==0)//子进程执行的代码    
    {
        tmp=20;
        while(1)
        {
            printf("子进程,tmp: %d,&tmp: %p\n",tmp,&tmp);
            sleep(1);
        }
    }
    if(id>1)//父进程执行的代码    
    {
        while(1)
        {
            printf("父进程,tmp: %d,&tmp: %p\n",tmp,&tmp);
            sleep(1);
        }
    }                                                                                                       
    return 0;
}

代码很好理解,但是聪明的你很快的看到了一个细思极恐的事情,当子进程把tmp的值更改后,竟然同一个地址出现了不同的值,啊?这不跟我扯呢,C/C++中我们都知道地址具有唯一性,那这个.......,现象不用质疑,因此我们前面学的知识要推翻了,所以今天这篇博客你看的值了!让我们来探究一下到底为什么!

📚二、虚拟地址空间

上面我们知道,C/C++中语言层级的地址是不对的,我确确实实在编译器当中能见到这个地址,你给我说不对,那这个地址是什么地址?事实上它是虚拟地址,下面我们就来认识一下它,我们先从一个例子开始:

有一个富豪,它有10亿资产,由于年轻时比较浪,所以他有四个私生子,这四个私生子并不知道彼此的存在,私生子A是个医生,私生子B是个企业家,私生子C是个街头混混,私生子D是个学生,富豪分别对小A,B,C,D说:

(1),小A啊,你要是努力做个医生,以后我的10亿美金都是你的了

(2),小B啊,要是你把你的公司运作的很好,以后我的10亿美金就是你的了

(3),小C啊...小D啊...

故事还没结束,有一天,A说:老爸我要买医疗器械,钱呢你也别以后了你直接把这10亿美金直接给我,你以后就安享天年就ok了,他老爹心想,我还没死呢,就开始这样计划了,滚犊子!但是又不能不给,毕竟是事业,富豪就给了儿子A10万美金,小C又说:老爸,给我两千美金吧,我吃不起饭了,富豪一听就把钱打过去了,所以我们知道,这四个人都可以用10亿美金以内的钱,但是永远用不到10亿美金!

现在列出人物和地址的对应关系:

  1. 富豪对应操作系统
  2. 10亿美金对应物理地址
  3. 私生子对应每一个进程
  4. 富豪画的饼对应虚拟地址空间

因此我们理解了,虚拟地址空间是什么,虚拟地址空间就是操作系统为每个进程画的饼,让每个进程都以为我是独占所有内存的,但是操作系统也知道进程有多少能耐,给你一个你以为的所有内存,反正是取之不竭用之不尽你就用去吧,因此在每个进程看来内存就我自己用

那紧接着问题就来了,每个进程都有那么一个虚拟地址空间,也就是大饼,你说老板今天给你说升总监,明天给你说升大堂经理,你肯定会说老板啊,你昨天明明说的是总监啊!老板尴尬了,是吗?因此这些大饼要不要管理起来呢?答案是,当然要的,饼不管理起来,就乱套了,老板的信用值也就无了!

因此,为了管理进程的虚拟地址空间,操作系统仍然用先描述再组织的方法建立结构体,因而管理进程的虚拟地址空间也就变成了对数据结构的管理!这样的结构体是mm_struct

📚三、虚拟地址空间的设计

我们从上面的了解中清楚虚拟地址空间无非就是栈区、堆区等等这区那区的,那抽象出一个结构体,我们在结构体当中如何表示呢?请看下图:

所以程序地址空间无非就是各个区域的结合,然而各个区域的划分无非就是两个整数begin和end,一个在区域的开头,一个在区域的结尾

cpp 复制代码
struct mm_struct
{
	int code_start;//代码区起始
	int code_end;//代码区结束
	int init_start;//初始化区起始
	int init_end;//初始化区结束
	int heap_start;//堆区起始
	int heap_end;//堆区结束
	......
	其他属性
};

那栈区、堆区在动态调整的时候该如何做呢?很简单!所谓的区域范围变化,``实际上就是对start和end做加减!

📚四、虚拟地址与物理地址之间的关联

我们现在知道虚拟地址不是真实的地址,那真实的地址叫什么?叫物理地址!它们之间的联系**本质就是一种映射关系!**

现代计算机使用以下方法解决问题:OS为每一个进程配对一个虚拟地址空间和一张页表,要访问物理地址时,需要先在页表进行映射,若访问的是非法地址,则会在页表层阻止你的访问!

所以为什么一个地址会有两个值?现在我们就能回答这个问题了:

1.子进程被创建的时候会按照父进程的模板创建对应的PCB和虚拟地址空间
2.任何一方想要改变共享内存,操作系统先把对应的数据拷贝一份,然后更改对应的页表映射关系,再把对应的值更改,因此从始至终虚拟地址空间的地址并没有发生改变。这个过程也叫写时拷贝

📚五、为什么要有虚拟地址空间

  • 有效的保护物理内存

上面我们提到,虚拟地址是通过页表映射到物理地址的,页表就是充当这个保护的角色,当有非法访问的时候,直接不能映射,也即是访问不到内存!

  • 使OS的耦合度更低,保证进程的独立性

因为有地址空间和页表的存在,物理内存可以不关心数据的类型,可以直接对它进行加载,这样物理内存的分配就可以和进程的管理分开来,做到它们并没有任何关系,所以内存管理模块和进程管理模块就完成了解耦合的操作!操作系统,为了保证进程的独立性,做了很多的工作:通过地址空间,通过页表,让不同的进程,晚射到不同的物理内存处。

所以独立性体现在:

1.进程与进程之间都有自己独属的PCB和虚拟地址空间、页表

2.一定层面上代码和数据也是独立的(可以共享但是代码运行起来不涉及更改)

进程=PCB(独立)+代码数据(独立),因而进程当然是独立的!

  • 让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便使用编译器也以统一的视角来进行编译代码

1.可执行程序内部就是按照虚拟地址空间那套方法已经编过址了(栈空间堆空间是没的,这两个是在运行时动态生成的)(这个地址一般称为逻辑地址)
2.可执行程序加载到内存,函数或者全局变量等天然的也具有了物理地址
3.进程虚拟地址空间会借用第一步骤中生成的虚拟地址
4.CPU读取的时候读取的正是虚拟地址空间中的虚拟地址,通过页表映射到内存中的物理地址,内存中的代码内部跳转的仍然是虚拟地址,这些虚拟地址再经CPU读取找虚拟地址空间中的内容经页表再去找对应的物理地址。

整体流程见下图:


📚六、总结

本篇博客我们了解了进程地址空间,清楚了在C/C++语言层级中,编译器所体现的地址到底是什么。它与实际存放数据的物理地址又是什么关系。

小结一下:

  • 我们由现象(同一地址打印出不同的内容)推出语言层级的地址不是物理地址、
  • 进程地址空间就是mm_struct(饼)
  • mm_struct里面包含了进程地址空间的属性信息(供操作系统维护管理)
  • 虚拟地址与物理地址通过页表建立映射关系
  • 为什么要有进程地址空间:
bash 复制代码
三点:
①有效的保护物理内存(页表对非法内容不建立映射)
②使OS的耦合度更低,保证进程的独立性(写时拷贝是一种具体体现)
③让进程以统一的视角,来看待进程对应的代码和数据等各个区域
方便使用编译器也以统一的视角来进行编译代码(磁盘中的可执行内部已经存在地址)
相关推荐
开开心心_Every2 小时前
安卓做菜APP:家常菜谱详细步骤无广简洁
服务器·前端·python·学习·edge·django·powerpoint
wdfk_prog2 小时前
WIN11如何可以安装ISO
linux·笔记·学习
旖旎夜光2 小时前
Linux(10)(中)
linux
五仁火烧2 小时前
HTTP 服务器
服务器·网络·网络协议·http
Knight_AL2 小时前
使用 Docker 快速安装 GitLab(CentOS)
docker·centos·gitlab
米高梅狮子2 小时前
01-Ansible 自动化介绍
运维·自动化·ansible
AuroraWanderll2 小时前
类和对象(六)--友元、内部类与再次理解类和对象
c语言·数据结构·c++·算法·stl
牛奶咖啡132 小时前
shell脚本编程(六)
linux·shell脚本编程·shell中的判断·if/else结构·if/elif/else结构·case选择语句·shell各种判断的语法及示例
sim20202 小时前
创建FTP账号
linux