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

举报
egoist2023 发表于 2025/09/15 18:31:52 2025/09/15
【摘要】 本文介绍了C/C++程序的内存空间布局,重点讲解了虚拟地址空间的概念及其作用。通过代码实验,验证了各内存区域(代码区、全局区、堆、栈)在虚拟地址空间中的分布,并通过fork实验说明父子进程虚拟地址相同但数据独立,体现了写时拷贝机制。文章还介绍了Linux内核用于管理进程虚拟内存的mm_struct和vm_area_struct结构。最后分析了虚拟地址空间的优势,包括提升安全性、简化管理和提高效率。

目录

前言

虚拟地址

虚拟地址空间是什么

如何理解空间划分

为什么要有虚拟地址空间

前言
在学习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没有变化,这是可以理解的,因为我们前面说进程之间是具有独立性的,即使是父子进程。

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

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

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

对应到计算机中:

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

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

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

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

fork为什么有两个值

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

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

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

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

堆空间为何能申请多次? 
那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:
当虚拟区较少时采取单链表,由mmap指针指向这个链表;
当虚拟区间多时采取红⿊树进⾏管理,由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;
如何理解进程之间互相独立

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

扩展

为什么要有虚拟地址空间
这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存⼤⼩是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的监管之下来进⾏访问!!即在由虚拟到物理内存转换的时候,可以进行安全审核!!!变相保证物理内存的安全,维护进程独立性特性。
    进程管理模块和内存管理模块就完成了解耦合
    进程视角所有的内存分布都可以是有序,由“无序”变“有序”

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。