【Linux系统】进程地址空间

Hello,我是云边有个稻草人,今天学习 Linux系统【进程概念】进程地址空间ヾ(◍°∇°◍)ノ゙

【Linux系统】_本篇文章所属专栏,持续更新中,欢迎订阅!

本文介绍了Linux系统中的进程地址空间概念。通过代码示例验证了内存分区(栈区、堆区、静态区)的地址分布,并解释了父子进程变量地址相同但值不同的现象,说明这是虚拟地址而非物理地址。详细讲解了虚拟地址空间的实现方式,包括mm_struct结构体和vm_area_struct的组织形式(链表和红黑树)。最后阐述了虚拟地址空间的三大作用:将物理内存有序化、保护内存安全、解耦进程与内存管理。文章指出虚拟地址空间通过页表映射机制,既保证了进程间的隔离性,又实现了内存访问的安全检查。
Linux系统,【进程概念】相关章节:

【Linux系统】第八节---进程概念(上)---冯诺依曼体系结构+操作系统+进程及进程状态+僵尸进程---详解!-CSDN博客

【Linux系统】第九节---进程状态续集+进程优先级+进程切换-CSDN博客

【Linux系统】第十节---进程概念------环境变量 | 详细讲解,这篇包会!-CSDN博客


引入

学习C语言时,我们知道将内存分为栈区,堆区,静态区,栈区存放形参和局部变量,堆区用来动态开辟内存,静态区存放全局变量和static修饰的静态变量。下面我们来验证一下

cpp 复制代码
#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);
    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;
}

先看常量字符串和代码

验证全局变量和static修饰的静态变量

被static修饰的变量,作用域是局部的,生命周期是全局的,其实在存储上已经是全局变量,只不过作用域是局部的。因为static修饰的变量被编译到已初始化数据区了,没有初始化的全局变量被编译到了未初始化数据区

static 定义的变量,不随着函数调用释放,只初始化一次,存储上完全等同于全局变量,不随着函数调用而释放。即使不初始化也是会初始化为0。

从下面的地址大小结合上面的内存内部图也可以看出来,初始化的全局变量地址较小,未初始化的全局变量地址较大

cpp 复制代码
init global addr: 0x601034
uninit global addr: 0x601040
test static addr: 0x601038

一个奇怪的现象:

运行结果:

子进程想要对变量进行修改,会发生写时拷贝,子进程会重新开辟一块空间来存放g_val,子进程和父进程读到的g_val的值不一样,所以说子进程和父进程的g_val应该是两块空间,但是为什么取到的地址是一样的呢?

所以:

  • 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
  • 但地址值是⼀样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做虚拟地址
  • 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址⽤⼾⼀概看不到,由OS统⼀管理

OS必须负责将 虚拟地址 转化成 物理地址

OK,正文开始------

五、进程地址空间

1、引入新概念,解释上面的现象

2、如何理解虚拟地址空间?

cpp 复制代码
struct mm_struct
{
    /*...*/
    struct vm_area_struct *mmap;           /* 指向虚拟区间(VMA)链表 */
    struct rb_root mm_rb;                  /* red_black树 */
    unsigned long task_size;              /*具有该结构体的进程的虚拟地址空间的大小*/
    /*...*/

    // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

    /*...*/
};

3、虚拟地址空间是怎么实现的?

描述Linux下进程的虚拟地址空间的所有的信息的结构体是 mm_struct 。每个进程只有⼀个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。


可以说,mm_struct 结构体是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:

第一步:先在虚拟内存里占好位置、建立映射

第二步:访问时才临时分配物理内存、把代码读进物理内存

占位置 ≠ 把代码和变量放进内存

占位置 = 先把「虚拟地址区间划好、登记好页表」,但里面暂时不放真实内容。

没有变量,怎么占数据段、全局变量位置?

同理:OS 读取程序文件头,看到:

  • 全局数据段一共多少字节
  • BSS 未初始化变量一共多少字节

然后直接在虚拟地址空间划一块同等大小的空地址区间

此时:

  • 变量根本没创建
  • 物理内存没分配
  • 只是虚拟地址被 "预留" 了

必须先把磁盘上真正的代码、二进制数据,拷贝到物理内存里; 进程的虚拟内存地址空间里,才真正有了可访问、可执行的具体代码和数据内容。

cpp 复制代码
struct mm_struct
{
    /*...*/
    struct vm_area_struct *mmap;           /* 指向虚拟区间(VMA)链表 */
    struct rb_root mm_rb;                  /* red_black树 */
    unsigned long task_size;              /*具有该结构体的进程的虚拟地址空间的大小*/
    /*...*/

    // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

    /*...*/
};

那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:

  1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
  2. 当虚拟区间多时采取红⿊树进⾏管理,由mm_rb指向这棵树。

linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进程快速访问。

cpp 复制代码
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;
  • 多次小 malloc :还是一个堆、一个 VMA,不会多。
  • 多次大 malloc (走 mmap):会生成多个独立的 vm_area_struct ,但这些不是堆,是匿名映射段。
cpp 复制代码
进程虚拟地址空间
+------------------+ 高地址
|    栈区           |  ← 有一个 vm_area_struct
+------------------+
|                  |
| mmap 映射区域     |  ← 可能有 N 个 vm_area_struct
|                  |
+------------------+
|    堆区 (brk)     |  ← 有一个 vm_area_struct
+------------------+
|    数据段         |  ← 有一个 vm_area_struct
+------------------+
|    代码段         |  ← 有一个 vm_area_struct
+------------------+ 低地址
cpp 复制代码
进程 mm_struct
       ↓
vm_area_struct (代码段) <-> vm_area_struct (数据段) <-> vm_area_struct (传统堆) <-> vm_area_struct (mmap堆1) <-> vm_area_struct (mmap堆2)

4、为什么要有虚拟地址空间

(1)将地址从无序 → 有序。(把物理内存杂乱无序,变成进程眼里整齐、规范、统一的虚拟地址布局)

(2)地址转化的过程中,也可以对你的地址和操作进行合法化判定,进而保护物理内存。

a、野指针

进程某块虚拟内存 (堆 / 栈 / 局部变量)已经被释放;页表中对应的映射条目被删除 ,虚拟地址再也没有关联的物理地址;对应的物理内存页也被系统回收、分配给别人 ;但指针变量还存着原来那个旧虚拟地址 ;这时这个指针就是野指针

进程通过野指针去访问:MMU 做地址转换 → 发现无页表映射、地址非法 → 触发硬件异常 → 内核直接干掉进程(段错误 SIGSEGV)

b、char *str="hello";*str='H';

"hello"字符串常量 ,编译后放在 .rodata 只读数据段; 这个区域在虚拟地址空间里,权限被标记为:只读(Read-Only); str 指针指向这块只读虚拟地址 ;你执行 *str = 'H'试图往只读地址里写数据 ;CPU 做地址转换 + 权限检查 ;发现:写操作 + 只读页 → 权限非法 ;操作系统直接杀死进程 → 段错误(Segmentation Fault)。

进程访问合法虚拟地址,但没有物理页映射或页面不在内存 ,MMU 地址翻译失败触发缺页中断,内核分配物理页、建立映射后,程序重新正常执行。

(3)将进程管理和内存管理进行一定程度的解耦合!

文字部分------为什么要有虚拟地址空间

这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?


存在这么多问题,有了虚拟地址空间和分⻚机制就能解决了吗?当然!

Linux系统 【进程概念】------进程地址空间。

完------


至此结束------

我是云边有个稻草人

期待与你的下一次相遇!

相关推荐
想唱rap6 小时前
传输层协议TCP
linux·运维·服务器·网络·c++·tcp/ip
曦夜日长6 小时前
Linux系统篇,权限(二):缺省权限、最终权限的计算、文件隔离的两种方式
linux·运维·服务器
云水一下6 小时前
黑客的“猜密码”游戏:SSH暴力破解实战与Linux安全加固
linux·渗透测试·ssh·暴力破解
kebidaixu6 小时前
OK3568开发板更新Ubuntu22.04方法总结
linux·运维·服务器
晚风予卿云月7 小时前
【Linux】Linux2.6 O(1)调度器超详解 | 进程切换+内核链表 | 面试必背
linux·运维·面试
www.028 小时前
Linux 终端守护神 Tmux :如何优雅地管理后台实验与恢复会话
linux·运维·服务器·人工智能·tmux
广州灵眸科技有限公司8 小时前
瑞芯微(EASY EAI)RV1126B yolov11-track多目标跟踪部署教程
linux·开发语言·网络·人工智能·yolo·机器学习·目标跟踪
谷雨不太卷8 小时前
计算机网络:套接字
linux·服务器·计算机网络
YuanDaima20488 小时前
WSL2 与 Ubuntu 22.04 基础环境部署指南
linux·运维·服务器·人工智能·ubuntu·docker