【Linux】虚拟地址空间

发布于:2025-08-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言:

        上文我们讲到了Linux中的环境变量【Linux】环境变量-CSDN博客

        本文我们来讲一下Linux中的虚拟地址空间

 初识空间分布

        下面是虚拟地址空间的大致分布图:

 

        我们可以先通过代码来验证是否正确:

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

int g_unval;
int g_val = 100;

int main(int argc, char* argv[], char* env[])
{
    const char* str = "helloworld";
    printf("code addr: %p\n", main);
    printf("init global addr: %p\n", &g_val);
    printf("uninit global addr: %p\n", &g_unval);

    static int test = 10;
    char* heap_mem = (char*)malloc(10);
    char* heap_mem1 = (char*)malloc(10);
    char* heap_mem2 = (char*)malloc(10);
    char* heap_mem3 = (char*)malloc(10);

    printf("heap addr: %p\n", heap_mem);
    printf("heap addr: %p\n", heap_mem1);
    printf("heap addr: %p\n", heap_mem2);
    printf("heap addr: %p\n", heap_mem3);

    printf("test static addr: %p\n", &test);
    printf("stack addr: %p\n", &heap_mem);
    printf("stack addr: %p\n", &heap_mem1);
    printf("stack addr: %p\n", &heap_mem2);
    printf("stack addr: %p\n", &heap_mem3);

    printf("read only string addr: %p\n", str);
    for (int i = 0; i < argc; i++)
    {
        printf("argv[%d]: %p\n", i, argv[i]);
    }
    for (int i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n", i, env[i]);
    }

    return 0;
}
hyc@hcss-ecs-4ce7:~/linux/虚拟地址$ ./test
code addr: 0x55da1c103189
init global addr: 0x55da1c106010
uninit global addr: 0x55da1c10601c
heap addr: 0x55da1d1f06b0
heap addr: 0x55da1d1f06d0
heap addr: 0x55da1d1f06f0
heap addr: 0x55da1d1f0710
test static addr: 0x55da1c106014
stack addr: 0x7ffcf95ad350
stack addr: 0x7ffcf95ad358
stack addr: 0x7ffcf95ad360
stack addr: 0x7ffcf95ad368
read only string addr: 0x55da1c104004
argv[0]: 0x7ffcf95ad71f
env[0]: 0x7ffcf95ad726
env[1]: 0x7ffcf95ad736
env[2]: 0x7ffcf95ad744
env[3]: 0x7ffcf95ad75f
env[4]: 0x7ffcf95ad780
env[5]: 0x7ffcf95ad78c
env[6]: 0x7ffcf95ad7a1
env[7]: 0x7ffcf95ad7b0
env[8]: 0x7ffcf95ad7bf
env[9]: 0x7ffcf95ad7d0
env[10]: 0x7ffcf95addbf
env[11]: 0x7ffcf95addf5
env[12]: 0x7ffcf95ade17
env[13]: 0x7ffcf95ade2e
env[14]: 0x7ffcf95ade39
env[15]: 0x7ffcf95ade59
env[16]: 0x7ffcf95ade62
env[17]: 0x7ffcf95ade6a
env[18]: 0x7ffcf95ade7e
env[19]: 0x7ffcf95ade9a
env[20]: 0x7ffcf95adebe
env[21]: 0x7ffcf95adecf
env[22]: 0x7ffcf95adf10
env[23]: 0x7ffcf95adf78
env[24]: 0x7ffcf95adfab
env[25]: 0x7ffcf95adfbe
env[26]: 0x7ffcf95adfd1
env[27]: 0x7ffcf95adfe8

虚拟地址

        打印不同进程下的相同变量的地址与值

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

int g_val = 0;

int main()
{
    pid_t id = fork();
    
    if (id < 0)
    {
        perror("fork");
        return 0;
    }
    else if(id == 0)
    { 
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else
    {
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    
    sleep(1);
    return 0;
}

        我们可以看到父子进程的输出结果都是一样的,很好理解没有问题。

        但如果子进程对变量进行修改会发生什么?

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

int g_val = 0;

int main()
{
    pid_t id = fork();
    
    if (id < 0)
    {
        perror("fork");
        return 0;
    }
    else if (id == 0)
    { 
        g_val=100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else
    {
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    
    sleep(1);
    return 0;
}

        这时我们就会发现输出变量的地址是一模一样的,但是变量的值却不一样!这就很奇怪了。

总结:

        相同的地址值却不一样,说明我们所打印出来的绝对不可能是真实的地址。而这就是我们说所的虚拟地址

        我们在使用C/C++看到的地址,全部都是虚拟地址!物理地址,用户一概都看不到,由OS统一管理。

        换言之,OS负责讲虚拟地址转化为物理地址

虚拟地址空间

结论一:

        1.一个进程,对应拥有一个虚拟地址空间

        2.一个进程,也对应拥有一个页表。页表的作用是建立虚拟地址与物理地址的映射关系,用于虚拟地址与物理地址的转化。

        3.虚拟地址空间单位为:1字节,既一个地址指向的空间大小为1字节。32位机器有2^32个地址,64位机器有2^64个地址。32位机器的虚拟空间大小就有4G(2^32*1字节)其中3G是用户可用空间,1G是内核空间(暂时不用管)

        如果我们创建一个子进程,我们知道子进程的task_struct是拷贝父进程的,而子进程与父进程共享代码与数据(没有新继承加载进来的情况下)这也就意味了子进程的页表映射关系与父进程一模一样

        但如果子进程要修改数据呢?会发生什么?

结论二:

        子进程修改数据会发生写时拷贝:在物理空间上重新开辟,并在页表中重新建立映射关系。注意:虚拟地址并没有改变,仅仅改变了物理地址!!!

        这也就解释了最开始的代码中,为什么明明打印出来的地址相同,但是值却不同。 

写时拷贝的意义

有的同学可以困惑,为什么不创建进程的时间直接拷贝一份呢?

1.减少了创建进程的时间

        如果创建时就直接拷贝,会增加进程创建的时间:并不会进程中每一个变量都会修改

2.减少了内存的浪费

        对于不需要修改的变量,依旧拷贝就会产生冗余数据,浪费内存空间

虚拟地址空间的本质 

mm_struct

        虚拟地址空间的本身其实就是一个结构体!(mm_struct)

        我们知道虚拟空间中有许多区域划分,而如果确定区域的位置呢?只需要知道各个区域的起始位置以及结束位置就可以了。所以mm_struct的主要成员变量是各个区域的位置信息

        想要对区域进行调整,也只需要修改对应区域的start、end即可

struct mm_struct
{
    unsigned long start_code,end_code;
    unsigned long start_data,end_data;
    ......
}

        我们说一个进程对应一个虚拟地址空间,既对应一个mm_struct。操作系统中有多个进程,必然也有多个mm_struct。那么就如何管理的呢?

        1.当数量较少时,采用单链表管理

        2.当数量较多时,采用红黑数管理

vm_area_struct

        mm_struct中还有一重要成员:vm_area_struct(存放其指针)

        vm_area_struct用来表示一个独立的虚拟内存区域,由于每个不同的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表示不同类型的虚拟内存区域,方便进程快速访问。

为什么要有虚拟地址

1.让无序的物理地址,变为有序

2.在地址的转化过程中,可以对地址以及操作进行判定是否合法,进而保护了物理内存

        例如:char* str="abc";  *str="a",为什么会报错?因为在查找页表时被拦截了!

3.让进程管理和内存管理,进行一定程度的解耦合

        不让虚拟地址与物理地址有强相关性

补充:

        1.我们可以不加载代码和数据,只有task_struct、mm_struct、页表

        2.再次理解挂起:保留虚拟地址空间以及页表中的虚拟地址,清空页表中的物理地址,将物理地址空间唤入到磁盘的swap分区中


网站公告

今日签到

点亮在社区的每一天
去签到