进程地址空间

🏷️前情回顾

在这里插入图片描述

我们首先要明白一个点:每个进程运行之后,都会有一个进程地址空间的存在

我们以一个父进程为例,它首先一个进程pcb:
假设它长这样:在这里插入图片描述

接下来假设我们有一块内存,这个内存是物理意义上的,假设它长这样:
在这里插入图片描述

我们把以前学到的那个虚拟内存也画出来:

在这里插入图片描述

在这里插入图片描述

假设我们定义了一个全局变量gval 那么它的位置如下图所示:
在这里插入图片描述
但是这个地址空间上并没有保存数据的能力, 最终你的数据必须得在物理内存当中保存。

在这里插入图片描述

那这个内容存放在物理地址中,为了我们以后访问到这个内容,那么它一定有一个物理地址的:
![[Pasted image 20240515214101.png]]
为了让我们的进程能够通过它的虚拟地址找到它的物理地址,我们的操作系统会为我们的进程维护一张映射表:
在这里插入图片描述

在这里插入图片描述

继每一个进程运行之后都会有一个进程地址空间的存在,每一个进程在运行之后也会有自己的页表映射结构

在这里插入图片描述

在这里插入图片描述

当我们的父进程在创建子进程的时候,那么既然是进程,那在内存中就会多一个叫pcb的东西。我们上面说了每一个进程运行都会存在自己的进程地址空间和页表,所以这个子进程也有。操作系统就把父进程的进程地址空间和页表等这些内容拷贝给了子进程。

这样会导致一个问题,那就是 子进程页表里的虚拟地址和物理地址和父进程的一样了。
如果这个时候,你想在子进程中修改一下全局变量,我们假设这个全局变量叫 g_val ,你想把它的值由 100 修改为 200 ,但由于父子进程页表里的内容是一样的,你这样也把父进程的g_val 的值也修改了呀。
所以,为了避免这个问题,在你想修改值的时候(即:你要进行 写 操作时 ) , 操作系统会在物理内存上把你要修改的空间拷贝下来再重新给你开一个空间。让你在这个新的空间里对g_val 进行修改。这种操作我们叫 写时拷贝
在这里插入图片描述

这里的页表是子进程的页表。因为上文我是修改的子进程的g_val 的值,如果你是修改的父进程的g_val 的值 那么操作系统就会修改父进程页表的物理地址 。

在这里我们要提醒一下,以上的改变都是改变页表的物理地址,并没有改变虚拟地址。所以才会出现本文开头提到的 对同一个地址进行访问却出现了不同的结果的问题。因为我们访问的是虚拟地址,父子进程的虚拟地址是一样的,但是在你修改值之后。对应进程页表的物理地址会发生变化。页表映射到了不同的物理内存处。

📌 写时拷贝发生在哪里?

物理内存中

📌 写时拷贝是谁来做的?

写时拷贝是由操作系统完成的

📌 会影响上层语言吗?

不影响

🏷️ 什么是地址空间?什么是区域划分?

📌 聊聊空间

一个进程能够访问的空间的范围,
我们知道是:2的32次方,(32根数据线,一共有2的32种方式)这个进程不能把这个范围全部访问完,但是这个进程可以访问的范围是这么大,所以叫进程地址空间。

📌 聊聊 c/c++中变量的概念

我们之前写代码的时候会有下面的场景:

int a = 10; // 这个 a 叫做变量名

但是当我们的代码经过编译形成了可执行程序之后 还有变量名的概念吗?

没有了

其实我们的代码经过编译之后就没有变量名这个概念了,我们所有的代码都会转化为相应的地址

📌 提一下c++中的深拷贝和浅拷贝

在 C++ 中,深拷贝和浅拷贝是两个常见的概念,尤其是在对象的复制过程中。这两者主要区别在于如何处理对象中的指针或动态分配的资源。以下是对这两个概念的详细说明:

浅拷贝(Shallow Copy)

浅拷贝仅复制对象的所有成员的值,不论是基本类型还是指针类型。在浅拷贝过程中,两个对象会共享相同的动态分配内存。这可能导致一个对象改变共享数据时,另一个对象的数据也会随之改变。

示例代码:

#include <iostream>

class Shallow {
public:
    int* data;

    Shallow(int value) {
        data = new int(value);
    }

    // 浅拷贝构造函数
    Shallow(const Shallow& source) : data(source.data) {
        std::cout << "Shallow copy constructor called!" << std::endl;
    }

    ~Shallow() {
        delete data;
    }

    void print() {
        std::cout << "Data: " << *data << std::endl;
    }
};

int main() {
    Shallow obj1(10);
    Shallow obj2(obj1);  // 调用浅拷贝构造函数

    obj2.print();
    obj1.print();

    return 0;
}

在这个例子中,obj1obj2 共享相同的内存地址。如果 obj1obj2 试图修改或删除 data,将导致未定义行为,因为两者都试图删除同一块内存。

深拷贝(Deep Copy)

深拷贝不仅复制对象的所有成员,还复制动态分配的内存空间。每个对象都有自己独立的内存副本,修改一个对象不会影响另一个对象。

示例代码:

#include <iostream>

class Deep {
public:
    int* data;

    Deep(int value) {
        data = new int(value);
    }

    // 深拷贝构造函数
    Deep(const Deep& source) {
        data = new int(*source.data);
        std::cout << "Deep copy constructor called!" << std::endl;
    }

    ~Deep() {
        delete data;
    }

    void print() {
        std::cout << "Data: " << *data << std::endl;
    }
};

int main() {
    Deep obj1(10);
    Deep obj2(obj1);  // 调用深拷贝构造函数

    obj2.print();
    obj1.print();

    return 0;
}

在这个例子中,obj1obj2 有自己独立的 data 内存块,修改一个对象的数据不会影响另一个对象。

深拷贝与浅拷贝的应用场景

  • 浅拷贝 通常用于无需独立内存副本的情况,节省内存和时间开销。例如,简单结构体或不涉及动态内存分配的对象。
  • 深拷贝 适用于需要独立内存副本以防止对象间相互干扰的情况。例如,涉及动态内存分配或需要对象独立的场景。

📌 在这里拷贝页表的方式其实就是一种浅拷贝

相当于 子进程就只是把父进程的地址拷贝下来了,所以他们两个的页表是一样的,都是同一个, 只有当发生写时拷贝时这个时候才是深拷贝, 我们可以按上述理解。

📌 什么是地址空间?什么是区域划分?

在操作系统中,地址空间是指一系列地址,这些地址用来表示计算机内存中的每一个字节。地址空间是一个抽象概念,它为操作系统和程序提供了一种机制,以便在不干扰其他程序的情况下管理和使用内存资源。

地址空间的详细说明

1. 定义

地址空间是操作系统为每个进程提供的一组内存地址。这些地址可以是物理内存地址,也可以是虚拟内存地址。==地址空间使得每个进程认为它独占整个内存,==从而简化编程模型并提高系统的安全性和稳定性。

2. 类型
  • 物理地址空间:直接对应实际的物理内存地址。物理地址空间通常是由硬件(如CPU和内存管理单元MMU)直接访问的。
  • 虚拟地址空间:由操作系统提供的一种抽象,使每个进程拥有自己独立的内存地址范围。虚拟地址空间通过地址映射技术(如分页或分段)映射到物理内存地址。
3. 虚拟地址空间的优势
  • 隔离性:每个进程的地址空间是独立的,这样一个进程不会干扰另一个进程的内存空间,提高了系统的安全性。
  • 灵活性:操作系统可以将虚拟地址映射到物理内存的不同位置,使得内存管理更加灵活。
  • 保护性:通过地址转换机制,操作系统可以保护内存不被非法访问。

划分:
在计算机科学和操作系统领域,区域划分(Segmentation)是一种内存管理方案,它将进程的地址空间分成若干个大小不同的区域或段(Segment),每个段代表一个逻辑的内存区域,如代码段、数据段、堆栈段等。

在这里插入图片描述

地址空间一定是一个内核的数据结构对象,就是一个内核结构体!

在Linux中 这个叫做进程/ 虚拟地址空间的东西,叫做 mm_struct

 struct mm_struct {
	 
 }

当一个进程被创建时,除了创建进程的pcb 还要创建这个 mm_struct

我们可以知道 这个mm_struct 里面的大致内容:
在这里插入图片描述

就是代码段的开始和结束,数据区的开始和结束 堆区的开始和结束 等

✎ 知识扩展

我们之前学过 栈区和堆区是相对而生的,这不就是对应栈区和堆区的空间进行调整吗,就是栈区的start和end进行调整,堆区的start和end进行调整


🏷️ 为什么要有地址空间?

我们知道每一个进程都有pcb 我们叫他:task_struct
每个进程也要有自己对应的一个地址空间(虚拟地址空间,它的结构体叫做:struct mm_struct 它是这个结构体的对象)
在这里插入图片描述
在这里插入图片描述

我们虚拟内存中的数据可以通过页表 映射到物理内存中,但是虚拟内存中数据的来源是:我们存放在磁盘中的可执行程序加载到内存中的。

✎ 问:我们磁盘中的可执行程序加载到内存中的时候 是加载到特定的位置?还是任意位置都可以?

答: 因为有页表的存在,它可以直接对加载的代码进行映射,所以 你可以将代码随意加载到任何地方。

📌 理由一 让进程以一个统一的视角看待内存,所以任意一个进程,可以通过地址空间+页表的方式将乱序的内存数据变成有序,分门别类的规划好

我们刚才知道了,我们的可执行程序加载到内存中是随意的,但是由于有虚拟地址的存在(即:进程地址空间)
我们的进程看到的数据都是这样的:
在这里插入图片描述
这样之后既简洁美观而且任意一个进程,可以通过地址空间+页表的方式将乱序的内存数据变成有序,分门别类的规划好。你如果直接用物理内存 那么进程看到的数据都是杂乱无章的

📌 理由二: 存在虚拟地址空间,可以有效的进行进程访问内存的安全检查。

✎ 页表还有一个区域叫:访问权限字段

在这里插入图片描述
由于这个字段的存在可以有效的拦截一些进程的危险操作,从而保护好我们的数据。

我们观察以下代码:

char *str = "hello linux";
*str = "H";

这段代码运行时会报错,因为str所指向的内容是字符常量,而字符常量是不能被修改的。
那么问题来了:

✎ 为什么字符常量是不能被修改的?

现在我们就可以解答了,因为 字符常量区 在页表里映射时访问权限字段 写的是 r 所以就没有办法修改

✎ 每一个进程都有一个页表,那么进程和页表是怎么对应起来的?

当一个进程在进行各种转化各种访问的时候,一定是这个进程正在运行的时候。 每一个进程在运行的时候,一定是在cpu上运行的。 cpu中有一个特殊的寄存器——CR3 这个 CR3 会保存当前进程的页表的地址。问, 里的地址 存的是虚拟地址还是物理地址呢? :这里存放的页表的地址就是物理地址。

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值