【Linux】进程地址空间

发布于:2025-05-07 ⋅ 阅读:(10) ⋅ 点赞:(0)

📝前言:

这篇文章我们来讲讲进程地址空间

🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记C语言入门基础python入门基础C++刷题专栏


一,C语言中的程序地址空间

我们在学习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);
        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); //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 static 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)

        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; i < 5; i++) // 打印五个环境变量
        {
                printf("env[%d]: %p\n", i, env[i]);
        }

        return 0;
}

运行结果:
在这里插入图片描述
通过上面的结果,我们可以感受到不同数据被储存在程序地址空间的不同区域,且存储规则符合给出的第一张图片

二,进程地址空间

但是,程序地址空间(准确应该叫:进程地址空间)实际上只是一个虚拟地址,不是实际的物理地址。我们可以看下面这一段代码:

对于同一个全局变量g_val,子进程修改它,父进程不修改,然后两个进程都打印变量的值和地址。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
        pid_t id = fork();
        if(id < 0)
        {
                perror("fork");
                return 0;
        }
        else if(id == 0) // child
        {
        		g_val = 10; 
                printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        else // parent
        {
                printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
        }
        sleep(1);
        return 0;
}

运行结果:

parent[207900]: 0 : 0x558d767b3014
child[207901]: 10 : 0x558d767b3014

为什么地址相同,但是值会不同呢?

这是因为:

  • 打印出来的地址,并不是真实的物理地址,而是虚拟地址。(我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,用户⼀概看不到,由OS统⼀管理)
  • 父子进程的 g_val 的虚拟地址相同,因为它们共享相同的代码和全局变量布局(子进程继承父进程的地址空间)
  • 值不同是因为发生了写时拷贝
  • 当子进程要对数据进行修改的时候,会在物理内存上拷贝同样的一份数据(即:这两个数据是在不同的物理内存上的)

我们在C语言中得到的都是虚拟地址,我们要访问这个虚拟地址对应的值的时候,OS就会通过页表,将虚拟地址映射到对应的物理地址,从而获取物理地址上真实的值。

页表

页表:

第一列 第二列 第三列
虚拟地址 物理地址 权限

每一个进程都有一个自己的进程地址空间(也就是虚拟地址),虚拟地址通过页表来映射到物理地址。

如下图(从左往右,父进程的映射;从右往左,子进程的映射):
在这里插入图片描述
当我们修改子进程数据的时候,会发生写时拷贝,拷贝一份数据到新的物理内存地址上。然后修改子进程对应的页表上的映射。(虚拟地址是没改的)

  • 虚拟地址空间是一个宽度为一字节的空间
  • int g_val是四个字节,我们得到&g_val得到一个地址值,怎么访问四块空间呢?
  • 答:我们得到的是g_val的首地址,由因为知道类型是int,就可以通过首地址 + 偏移量来访问到整个g_val

缺页中断

简单说一下:

当进程访问一个尚未映射到物理内存的虚拟地址时,由MMU(内存管理单元)触发CPU异常,操作系统通过缺页中断处理程序动态分配物理内存或加载数据,确保进程可以继续执行。

缺页中断的发生场景:

  • 页面未分配(访问未映射的虚拟地址)。

  • 页面已分配但未加载到物理内存(如被换出到磁盘)。

  • 权限不足(如尝试写入只读页)。

缺页中断也分不同情况,有不同的解决策略。

三,虚拟内存管理

如何快速构建一个划分好码段、数据段、堆、栈…的虚拟地址空间呢?
答:只需要记录每个区域的起始和结束地址。

mm_struct

mm_struct就是用来管理进程地址空间的结构体。(在task_struct中)

struct task_struct
{
	/*...*/
	struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。
	struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当该进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
	/*...*/
}

mm_struct是对进程地址空间的整体描述,每⼀个进程都会有自己独立的mm_struct,在加载的时候被初始化。
整体描述是指:不直接管理内存的细节,而是通过维护VMA链表和页表,描述进程地址空间的全局布局(例如“代码段在哪里?堆有多大?”)

关键字段:

struct mm_struct {
    struct vm_area_struct *mmap;    // 指向VMA链表的头节点
    pgd_t *pgd;                    // 页全局目录(Page Table)
    unsigned long start_code;       // 代码段起始地址
    unsigned long end_code;         // 代码段结束地址
    unsigned long start_data;       // 数据段起始地址
    unsigned long end_data;         // 数据段结束地址
    unsigned long start_brk;        // 堆的起始地址
    unsigned long brk;              // 堆的当前结束地址
    unsigned long start_stack;      // 栈的起始地址
    // ... 其他字段(如内存统计、锁等)
};

细节描述就需要用到vm_area_struct

vm_area_struct(VMA)

  • vm_area_struct会对内存地址区域进行细节描述
  • 每个 vm_area_struct 描述进程地址空间中的一个连续内存区域(如代码段、堆的一个块、文件映射区等)。
  • 由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使用多个vm_area_struct来分别表示不同类型的虚拟内存区域。
  • 当VMA少的时候,就用链表来管理,当VMA多的时候,就用红黑树管理(为了提高查找效率)
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;

整体关系:

在这里插入图片描述

四,为什么要有虚拟地址空间

  • 对操作进行限制,保护了物理内存中的所有的合法数据
    • 如:每次访问内存时,必须先经过页表。页表中有权限这一项,当发现要访问的地址没有对应的权限 或者 要访问的地址根本不存在(没有对应的映射)的时候,可以拒绝访问。
  • 无序变有序,且更有效的利用物理内存
    • 如,代码和数据就可以存储在物理内存的任意位置(因为虚拟地址是有序的,用户 / 进程视角 看到的也是有序的)
  • 物理内存的分配 和 进程的管理 没有直接关系,完成解耦合。

🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!


网站公告

今日签到

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