[Linux]进程地址空间

进程地址空间

以32位机器为例

程序地址空间

地址空间描述的基本空间大小是字节,每个字节都要有为一的地址,所以在32位的机器下就会有2^32次方个地址,也就是4G的空间范围。这些空间被划分成为了一个个区域。范围是 0x00000000 - 0xFFFFFFFF

我们知道,在一个程序中变量或函数的地址分布情况如下图所示:

那这些地址是物理地址吗?我们用下面的代码来验证一下。

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

int global_value = 100;
int main()
{
    pid_t id = fork();

    if (id < 0)
    {
        printf("error\n");
        return 1;
    }
    else if(id == 0) 
    {
        int cnt = 0;
        while(1)
        {
            printf("子进程,pid:%d, ppid:%d | global_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
            sleep(1);
            cnt++;
            if (cnt == 6)
            {
                global_value = 600;
                printf("子进程更改了global_val!!!!!!\n");
            }
        }
    }
    else
    {
        while(1)
        {
            printf("父进程,pid:%d, ppid:%d | global_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
            sleep(2);
        }
    }
    sleep(1);
    return 0;
}

运行结果

在子进程更改global_value之前,他们的地址都是一样的。这个现象很好理解,因为在创建子进程的时候,子进程是以父进程为模板创建出来的。那么变量的地址当然是一样的。但是当子进程修改了global_value后,他们的值不一样了,但是地址还是一样的。这就有点反常了,多进程在读取同一个地址的时候怎么可能会出现不同的结果。那么就说明这里的地址就绝对不是物理地址,既然不是物理地址,那么到底是什么地址呢?请接着往下看。

虚拟地址

它是一种逻辑上的地址,并不直接对应物理内存中的实际存储单元。这意味着不同进程可能使用相同的虚拟地址,但这些虚拟地址会被操作系统映射到不同的物理地址,从而实现进程之间的内存隔离。

在Linux中,虚拟地址本质上就是一个内核数据结构对象。如下图:

其中start_codeend_code等表示了一个区域的开始和结束,通过这种方式来将4G的空间划分为不同的区域。虚拟地址空间被进程的pcb管理着,每个进程都会有一个进程pcb,因此每个进程都会有一个虚拟地址空间(也就是进程地址空间),也就是说每个进程都认为自己独享内存资源。

再来看下面这张图

当程序在编译形成可执行文件的时候,就是按照虚拟地址空间的编址方式对代码和数据进行编址的。所以当可执行程序没有被加载到内存的时候,他自己本身就有了一套地址。当加载到内存后,程序则又多出了一套物理地址。此时我们就有了两套地址,一是:标识物理存在中代码和数据的地址,二是:在程序内部相互跳转的时候用的虚拟地址。

由于程序的内部就有地址(虚拟地址),当CPU在执行我们的程序的时候,它所使用的就是虚拟地址。它通过虚拟地址找到对应在mm_struct的区域,再通过页表映射的方式找到代码和数据实际存放物理内存的位置。从这可以看出CPU从始至终都没有见到过物理地址,它所见到的都只是虚拟地址。

写时拷贝

有了这些了解让我们再来看下之前给出的问题,为什么多进程在读取同一个地址的时候会出现不同的结果。

继续看图:

由于进程具有独立性,当一个进程尝试修改共享的数据时,并不是直接修改,而是在内存中重新开辟一块空间,将数据拷贝到新开辟的空间中,然后再将页表中对应的物理地址修改成新开辟空间的地址,而对应的虚拟地址不变。这就是写时拷贝。

为什么要有虚拟地址

  1. 安全

    若是没有虚拟地址空间,那么进程就会直接的访问物理内存。如果进程在访问物理内存的时候进行了越界访问,这就会破坏其他进程正在使用的内存数据,导致系统崩溃或数据丢失。
    而有了虚拟地址空间,那么进程想要访问物理内存则必须通过页表映射的方式由虚拟地址找到物理地址,若地址是非法的,那么页表就直接会进行拦截,从而保证了物理地址不被错误的访问。

  2. 独立性

    虚拟地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性。

    若一个进程对被共享的数据做修改,如果影响了其他进程,则不能称之为独立性,因此在任何一方尝试对被共享的数据做修改时,操作系统先进行数据的拷贝,更改页表映射,然后才让进程对数据进行修改。

  3. 简化内存管理

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

    不仅仅操作系统会遵守虚拟地址空间的规则,编译器也要遵守。编译器编译代码的时候,就是按照虚拟地址空间的方式对代码和数据进行编址的。因此在程序被加载到内存之前,它在磁盘中就已经有了一套虚拟地址,用来在程序内部互相跳转。而在被加载到内存后,它又有了一套物理地址,这个物理地址是为了标识代码和数据的地址。

    cpu读取指令的时候,指令内部就已经有了一套虚拟地址;cpu内部的寄存器同样使用的是虚拟地址。因此当程序运行的时候,cpu从始至终都没有见到过物理内存。

posted @   羡鱼OvO  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示