进程地址空间

文章介绍了进程地址空间的概念,强调了为何需要进程地址空间,主要是为了保障安全性、实现进程独立性和保持统一性。在32位系统中,每个进程有4GB的虚拟地址空间。当进程如父子进程共享全局变量时,通过写时拷贝技术保证数据独立,而页表则在虚拟地址到物理地址映射中起到关键作用。
摘要由CSDN通过智能技术生成

目录

一、程序地址空间回顾

二、进程地址空间

三、为什么要有进程地址空间

1、安全性

2、进程独立性

3、统一性


注:学习背景为32位平台

一、程序地址空间回顾

在之前学习C/C++时,可以知道C/C++程序在运行时内存会被划分为如下几块区域:

在学习了进程的一些基本概念后,执行下面的代码:

#include <stdio.h>
#include <unistd.h>

int global_val = 100;

int main()
{
    // 创建一个子进程
    pid_t id = fork();

    if (id < 0)
    {
        // 子进程创建失败,不玩了
        printf("fork() error!\n");
        return 1;
    }
    else if (id == 0)
    {
        int n = 0;
        while (1)
        {
            printf("子进程:pid=%d, ppid=%d, global_val=%d, &global_val=%p\n", getpid(), getppid(), global_val, &global_val);
            sleep(1);
            if (n == 10)
            {
                global_val = 300;
                printf("----------子进程的global_val修改为:%d----------\n", global_val);
            }
            ++n;
        }
    }
    else 
    {
        while (1)
        {
            printf("父进程:pid=%d, ppid=%d, global_val=%d, &global_val=%p\n", getpid(), getppid(), global_val, &global_val);
            sleep(2);
        }
    }
    return 0;
}

 代码运行结果如下:

在子进程的global_val被修改之前,子进程和父进程global_val有相同的值和地址,这很好理解,因为子进程在被创建时,会继承父进程的一些属性。

在子进程的global_val被修改之后,子进程和父进程global_val拥有不同的值,这也是可以理解的,因为两个进程的数据是独立的,但是最难理解的是,为什么同样的地址,取出来的是不同的值呢?

答案是:因为值不同,所以地址一定是不同的;

所以给出的地址是一个“假地址”,我们称之为“虚拟地址”;

操作系统负责将真实的物理地址和“虚拟地址”做出转化。

二、进程地址空间

用“程序地址空间”来描述会有一些不准确,更准确的是“进程地址空间”,因为只有程序在运行起来后,才会被分配内存,拥有自己的地址空间。

每一个程序运行起来,操作系统都会创建一个PCB(进程控制块)结构体来存储一些进程的基本信息做统一管理;每一个进程都有自己的地址空间,同样的,操作系统为了管理这些虚拟地址空间,也会创建一个结构体来管理这些虚拟地址空间。

在学习进程地址空间之前,要理解一些概念:

1、进程会认为它自己是独占所有地址空间的(实际上并不是);

2、地址空间的基本单位是字节Byte;

3、地址具有唯一性,即每个地址都标识唯一的一块空间;

4、在32位操作系统下,有32根地址线,可以表示0x00000000-0xFFFFFFFF共2^32次方个地址,也就是共有2^32 Byte = 2^22 KB = 2^12 MB = 2^2 GB,也就是4GB的空间可以被标识。

也就是说每一个进程都认为自己独享4GB的空间,结合本文最开始程序地址空间的划分,可以定义如下结构体:

struct mm_struct {
    // 声明正文代码的虚拟地址起止空间
    uint32_t code_start, code_end;

    // 声明堆的虚拟地址起止空间
    uint32_t heap_start, heap_end;

    // 声明栈的虚拟地址起止空间
    uint32_t stack_start, stack_end;

    // ......
};

// 定义某个进程的虚拟地址空间
struct mm_struct process_A = {
    0x10000000, 0x1FFFFFFF, // 正文代码的虚拟地址空间
    0x22222222, 0x3FFFFFFF, // 堆的虚拟地址空间
    0x55555555, 0x6FFFFFFF, // 栈的虚拟地址空间
    // ......
};

这样,每一个进程都有自己的PCB,都有自己的虚拟地址空间,可以映射到物理内存,因为程序在运行过程中,通过malloc或者new向操作系统申请的空间不会太大(如果太大可以操作系统可以拒绝),所以虽然每个进程都认为自己独享4GB的内存空间,但它们实际使用的比这小很多。

同时,在虚拟地址到物理地址的映射过程中,由一个叫做页表的东西来控制(文件操作会详细解释)。每一个进程都有自己独立的页表。

三、为什么要有进程地址空间

1、安全性

如果让进程直接访问物理内存,如果越界、非法访问,会导致其他进程的代码被破坏、信息被泄漏;当进程需要访问内存的时候,先在进程地址空间访问,进程地址空间将请求提交给页表,页表判断如果是非法访问,就会拒绝请求。从而保证了其他程序的安全。

2、进程独立性

在本文最初的代码中,父进程创建了子进程后,global_value作为全局变量,是被父子进程共享的,也就是两个进程的global_value实际上是同一块物理内存空间。

但是在子进程要修改全局变量时,为了保证进程数据的独立性,操作系统就会把全局变量拷贝一份到一个新的物理地址空间,并修改页表,让虚拟地址与物理地址重新对应,然后再做修改,这种方式称为写时拷贝

3、统一性

我们写好的C/C++程序,在经过编译预处理、编译、汇编、链接,最后生成可执行程序。那么,生成的可执行程序中一定是存在地址的!

举个例子,在链接的过程中,我们在代码中调用系统的库函数,就会转换成对应的函数的地址。

而这个地址,很明显不可能是物理地址,所以它一定就是虚拟地址。

所以,虚拟地址的存在,可以让编译器以统一的视角来编译代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王红花x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值