【Linux仓库】虚拟地址空间【进程·陆】

发布于:2025-07-13 ⋅ 阅读:(24) ⋅ 点赞:(0)

🌟 各位看官好,我是

🌍 Linux == Linux is not Unix !

🚀 今天来学习Linux的指令知识,并学会灵活使用这些指令。

      注意:本章节所采用kernel 2.6.32 ,在32位平台下。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

前言

在学习C、C++内存空间布局的时候,我们只能将下图称为程序地址空间。

实际上,它应该叫做的虚拟地址空间。

验证该程序地址空间确实是从低地址到高地址:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.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);

    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); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)

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

    return 0;
}

下图所示可以看到该空间确实是按这样的排布从低地址到高地址。

   


虚拟地址

    int gval = 100;//全局变量

    pid_t id = fork();
    if(id==0)
    {
        while(1)
        {
            printf("我是子进程,pid:%d , ppid:%d , gval:%d ,
                &gval:%p\n",getpid(),getppid(),gval,&gval);
            sleep(1);
            gval++;
        }
    }
    else
    {
        while(1)
        {
            printf("我是父进程,pid:%d , ppid:%d , gval:%d , &gval:%p\n",getpid(),getppid(),gval,&gval);
            sleep(1);
        }
    }

在上面这段程序中,父进程fork一个子进程。接着父子进程分别进入自己的执行流。子进程的执行流是每次打印都会将gval进行++,同时打印gval的地址;父进程也同样打印gval值和它的地址,但是不对gval进行++。

可以看到子进程的gval值一直有在变化,而父进程的gval没有变化,这是可以理解的,因为我们前面说进程之间是具有独立性的,即使是父子进程。

但我们竟还发现同一个地址,还能查出来两个不同的值???不对啊,一个地址只能有一个值才对啊,为什么还能出现两个值的情况,这似乎是违背常理的。

因此,我们可以确定的是这个地址根本不是物理内存的地址,否则就不会出现一个地址对应两个值的情况 --> 它是虚拟地址!!!(历史所学的所有地址都是虚拟地址,我们看不到物理地址!只有系统调用才能看到。物理地址,用户一概看不到,由OS统一管理。OS必须负责将虚拟地址转化成物理地址 。

虚拟地址空间是什么

为了引入虚拟地址空间的概念,这里讲述个小故事:

有一个男人叫小帅,他是某知名公司的董事长,名下财产就有10亿,并且他有三个儿子。他对大儿子(律师)说:儿子啊,你就勤勤奋奋地工作,等我死后我名下的10亿财产全部归你;他对二儿子(赌鬼)说:孩子你就别赌博了,听老爸的话,等我死后自己的10亿财产都归你,二儿子听到有着好事,想也没想直接伸手问老爸要10亿资产。小帅怒火中烧地说:急什么急!都说资产等我死后再给你;他对小儿子(学生)说:孩子啊,你好好的在学校学习,将来等父亲死了后自己名下的10亿财产全部都归你,而二儿子每次要买书本玩具时都可以向父亲的财产申请一笔小数额的钱。(三个儿子之间互相并不知道父亲都给他们对应的承诺。而10亿财产只有一份,但都对三个儿子做出了10亿财产的承诺,这不就是现实生活中的画大饼吗!

对应到计算机中:

操作系统(小帅)对每一个进程(儿子)做一个承诺:每一个进程都能拥有这整块虚拟地址空间(10亿财产),但实际上进程确不能真真正正地得到这一整块虚拟地址空间,每次要使用空间时只能向操作系统要一些

既然操作系统对每一个进程都画出了一个大饼,哪个大饼是属于哪个进程的,那么这就意味着会存在很多的大饼。那这些大饼是否需要被管理呢?肯定是需要的,那么该如何管理呢?

先描述,再组织!!!

虚拟地址空间规定:从全0到全F的一端空间。

在32位机器下:虚拟地址空间的范围【0,2^32】,大概为4G。

并且虚拟地址空间中用户空间是提供给程序员使用的,程序员可以直接用地址来访问。内核空间只能由系统调用来访问,用户并不能进行获取。

证明进程间互相独立

父进程创建子进程,子进程会以父进程的代码和数据为副本进行创建,此时都指向父进程的物理内存的g_val值。而当对g_val的值做修改时,操作系统会对g_val值做写时拷贝,即在物理内存开辟一段新的空间,将g_val的值拷贝到新空间上,并更改映射关系,从而使同一份虚拟地址可以出现两个值,因为它们映射到的物理内存的位置是不一样的!!!


fork为什么有两个值

还记得之前的fork系统调用吗?当时引出了三个问题:

  1. 为什么父进程返回子进程的标示符,子进程返回0;
  2. 为什么可以有两个返回值?
  3. 为什么这里一个变量可以返回一个大于0,又能返回等于0?

现在问题3不就能解惑了吗?由于fork要reutrn,return的本质是什么?不就是写入吗?写入不就要修改数据吗?那不就是要发生写实拷贝,发生了写时拷贝导致它们映射到的物理内存的位置有所不同,这就是一个变量可以返回大于0,又能返回等于0的原因!!!

如何理解空间划分

可以看到我们的虚拟地址空间也有划分的范围的,如:常量区应在哪里到哪,堆区从哪到哪,栈区从哪到哪。那么linux下进程的地址空间的对应位置是交给谁管理的呢?

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。 mm_struct 负责描述 整个进程的虚拟地址空间的宏观信息。

堆空间为何能申请多次? 

那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:
  1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
  2. 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。

可是堆空间可以申请很多次啊?它是怎样做到每一次申请都有自己的起始地址和大小啊??

linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚
拟内存区域功能和内部机制都不同,因此⼀个进程使⽤ 多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域 。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进程快速访问。
vm_area_struct :描述单个内存区域的具体细节,每个  vm_area_struct 描述进程地址空间中 一段连续的内存区域。
   
struct vm_area_struct 
{
    unsigned long vm_start; //虚存区起始
    unsigned long vm_end; //虚存区结束
    struct vm_area_struct *vm_next, *vm_prev; //前后指针
    struct rb_node vm_rb; //红⿊树中的位置
    unsigned long rb_subtree_gap;
    struct mm_struct *vm_mm; //所属的 mm_struct
    pgprot_t vm_page_prot;
    unsigned long vm_flags; //标志位

    struct 
    {
        struct rb_node rb;
        unsigned long rb_subtree_last;
    } shared;

    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;
    const struct vm_operations_struct *vm_ops; //vma对应的实际操作
    unsigned long vm_pgoff; //⽂件映射偏移量
    struct file * vm_file; //映射的⽂件
    void * vm_private_data; //私有数据
    atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
    struct vm_region *vm_region; /* NOMMU mapping region */
#endif

#ifdef CONFIG_NUMA
    struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif

    struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

如何理解进程之间互相独立

进程 = 内核数据结构 + 代码和数据

有了现在的学习,我们清楚内核数据结构不止PCB,还有虚拟地址空间和页表(一种数据结构,建立映射关系)-->

进程 = 内核数据结构(PCB,虚拟地址空间和页表)+ 代码和数据

要理解进程之间时互相独立的,就得说明内核数据结构+代码和数据是互相独立的:

  • 内核数据结构是互相独立的,每次创建进程时都会拷贝父进程的内核数据结构;
  • 而代码区的数据是可读的,因此也是独立的;
  • 那数据区的数据不是可读可写吗?如果修改了数据就不会相互独立了。但不用担心,如果我们修改了数据操作系统不就要为我们的数据进行写时拷贝:数据层面的分离,从而保证我们的数据也是独立的。

因此内核数据结构 + 代码和数据都是独立,证明了进程也是互相独立的!!!

扩展


为什么要有虚拟地址空间

这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B。
   

这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。

1. 安全风险
每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内
存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
2. 地址不确定
众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中
去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷⻉
的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程
都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程
在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了
3. 效率低下
如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理
内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷⻉到磁盘的交换分区中,好腾出内
存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷⻉
时间太长,效率较低。
那为什么虚拟地址空间和分页机制能解决此问题呢?
  • 地址空间和⻚表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和⻚表进⾏映射,也⼀定要在OS的监管之下来进⾏访问!!即在由虚拟到物理内存转换的时候,可以进行安全审核!!!变相保证物理内存的安全,维护进程独立性特性。
  • 进程管理模块和内存管理模块就完成了解耦合
  • 进程视角所有的内存分布都可以是有序,由“无序”变“有序”



网站公告

今日签到

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