Linux进程地址空间——钻入Linux内核架构性剖析 硬核手搓!

引言:需少部分进程基础知识,十分注意关键处图文并看原则。

我相信通过阅读此文Linux地址与物理地址架构关系清晰无比。

目录

内存地址分布图

问题:为什么采用堆、栈增长方向不一致的设计?

内存地址的本质:

虚拟内存------物理内存

1.1虚拟内存

1.1.1一个地址问题

1.1.2页表

简单页表结构理解:

1.2单进程分析

1.2.1进程-页表-磁盘联系

1.3父子进程分析

1.3.1内核代码引用

☆☆☆1.3.2父子进程与物理内存的对应关系

1.4超硬核解析虚拟内存与物理内存本质

1.4.1mm_struct

1.4.1.2mm_struct内部关键字段

1.4.2辨析进程地址空间与虚拟地址空间

1.4.2.1一个问题:

2.1计算机物理内存


内存地址分布图

常规的内存地址分布图(以32位系统下4G内存为例):

由下到上,地址逐渐增加,栈向下增长(++增加成员地址渐小++ ),堆向上增长(++增加成员地址渐大++)。

问题:为什么采用堆、栈增长方向不一致的设计?

1.充分利用内存空间。

2.减少管理复杂度。

3.减少碰撞。

内存地址的本质:

以上地址的区域分布均是虚拟内存地址表------并非真正的电脑上的物理内存。

// 可以形象地理解为本地IDE是一款游戏而其虚拟内存就是人物血量。 既然是游戏人物血量耗完,现实中你的血量会耗完吗? ps:世界上最好玩的游戏:Visual Studio

虚拟内存------物理内存

进程级+内核分析

1.1虚拟内存

我们已经了解到虚拟内存并非真正的电脑内存。

1.1.1一个地址问题

我们使用父子进程来进行探讨------代码如下:

cpp 复制代码
#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 = 100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else { //parent
        sleep(3);
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);

    return 0;
}

输出结果:

cpp 复制代码
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

我们发现父子进程的地址输出是一样的但是输出结果却不一样。

由此我们引入了进程间地址的探索,接下来进行内核级进程块式分析。

为解决以上问题,我们猜想:

会不会存在多张虚拟内存表来存储不同进程内的数据呢?

如果是真的,就实现了不同进程间数据管理的独立性。

1.1.2页表

探索虚拟内存必然要涉及对于物理内存的照应问题,因此诞生了"页表"这一概念。

功能:作为中间组件,实现物理地址到虚拟地址的映射。 因此可以猜想内部存在映射关系。

定义:页表作为操作系统内的一个核心数据结构,**用于实现物理地址到虚拟地址的映射,**它是现代操作系统内存管理的基石。

简单页表结构理解:

1.2单进程分析

在进程的知识中我们已经知道:每个进程就是一个PCB(进程控制块)结构体维护。

那么对应于虚拟地址表就是以下图示:

1.2.1进程-页表-磁盘联系

☆☆☆规定:++一块PCB维护一张虚拟内存表++ ,内存内 存储相应数据,虚拟内存表之间与物理内存表之间通过中间组件 页表联系。 // 页表不是结构体!

据此诞生 PCB---页表---物理内存 结构示意图,作为C++程序员必须刻在骨子里的图片:

1.3父子进程分析

cpp 复制代码
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

同一个虚拟地址映射两个不同的值:

1.3.1内核代码引用

内核中的源代码相关: ps:无需具体看懂,看明白逻辑即可

cpp 复制代码
// 简化的逻辑
//分配新页表
new_mm->pgd = pgd_alloc();              // 分配新页表
pgd_copy(new_mm->pgd, old_mm->pgd);     // 复制父进程页表内容

//写时复制(伪代码):          !!!!!!!!
page->_mapcount++;          // 增加引用计数
//                          !!!!!!!! 
SetPagePrivate(page);       // 标记为 COW 页面

查看源代码可知:子进程在基于父进程创建时,完完全全的进行了浅拷贝------即子进程在未发生"写"等改变原数据的前提下子进程PCB维护的表与父进程一致,一旦发生"写"等改变原数据的的操作,子进程独立创建新的虚拟表来维护。

☆☆☆1.3.2父子进程与物理内存的对应关系

父进程定义变量g_val=100; 对应地址0x112233(看图)。

为清晰展示进程间发生"写"等操作的区分,图中对于子进程额外处理了变量g_val+=1的操作(看图)。

即:真实物理内存被进程虚拟内存表通过页表照应起来! 因此我们"+=1"时就先子进程的新创建表后发生更改,体现在物理内存中如上。

1.4超硬核解析虚拟内存与物理内存本质

前言:..................

在虚拟内存前必须由PCB入口分析

源代码如下: ps:看出task_struct内拥有的"mm_struct"即可

cpp 复制代码
struct task_struct 
{
    // ... 其他字段(PID、状态、调度信息等)
    struct mm_struct *mm;          // 进程的用户空间内存描述符
    struct mm_struct *active_mm;   // 内核线程借用active_mm
    // ...
};

1.4.1mm_struct

成员strcut mm_struct就是整个虚拟内存,没错这个结构体维护虚拟内存。

简单来看,内部有区域划分线 像代码区,已初始化区、未初始化区等等很多成员,用以维护虚拟地址内部成员,便于代码操作。

cpp 复制代码
struct mm_struct
{
    long code_start;
    long code_end;
    long init_start, init_end;
    long uninit_start, uninit_end;
    //..................
}
1.4.1.2mm_struct内部关键字段

mm_struct内部通过建立**红黑树(mm_rb)+链表(struct vm_area_struct)**关联 管理 各个进程。

Ⅰ通过struct vm_area_struct 链表(mmap字段)管理所有虚拟内存区域(VMA)

Ⅱ通过红黑树(rm_rb字段)加速查找特定虚拟地址对应的VMA

图解:

源码引用:

cpp 复制代码
struct mm_struct
{
    struct mm_struct {
    // 1. 虚拟内存区域(VMA)的管理
    struct vm_area_struct *mmap;          // VMA 双向链表的头
    struct rb_root mm_rb;                 // VMA 红黑树的根
    unsigned long mmap_base;              // 内存映射区域的基地址
    unsigned long task_size;              // 进程虚拟地址空间大小
    
    // 2. 页表相关
    pgd_t *pgd;                           // 指向第一级页表(Page Global Directory)
    
    // 3. 代码、数据、堆、栈的边界
    unsigned long start_code, end_code;   // 代码段范围
    unsigned long start_data, end_data;   // 数据段范围
    unsigned long start_brk, brk;         // 堆的起始和当前结束
    unsigned long start_stack;            // 栈的起始地址
};

1.4.2辨析进程地址空间与虚拟地址空间

"虚拟地址空间"强调的是程序级分配对象 (没错就是程序级分配对象),而进程地址空间强调的是以操作系统内核的视角------即内核给代码分配对应的物理内存,物理内存通过页表映射到虚拟内存地址。 ps:在实际工作中常常混用,但本质对象就是struct mm_struct

1.4.2.1一个问题:

在一个进程下可以同时拥有大量子进程,各个子进程都有同样巨大的虚拟内存空间,都需要物理内存映射那么物理内存是怎么做到的单个小物理内存却映射如此庞大,数量繁杂的虚拟内存表呢?

答案是:物理内存并没有被切分和虚拟内存一样大的块状,而是被拆分为很多个小页框(Page Frame)。每个进程的虚拟页在"被需要时"才会被映射到这些小页框上,并且同一个小页框可以被多个进程的虚拟地址映射。

总结:本文图片是基于知识而呈现出递进关系对Linux进程内核的框架了解有很大的帮助,我相信拥有一些进程基础阅读此文是畅通无阻的。


图片是精华所在,细心打磨每张图片、排版文章框架。

(*^▽^*)
♬吐血整理求关注♬

2.1计算机物理内存

物理内存是计算机系统中实际的、可寻址的硬件存储介质(通常是DRAM),用于临时存放CPU当前正在执行的程序指令和处理的数据。其每个字节都有唯一一个物理地址(Physical Address,PA)。

物理地址:也同样的拥有真实的内存地址,例如从0x00000000 到0xFFFFFFFF。

像我的笔记本:

16G-0.3G就是实际上分配的物理内存大小。

本质:硬件资源由操作系统统一管理,是所有进程的共享仓库。关机数据清空。

相关推荐
NigulasiLiu1 小时前
CompletionService并发编排消费任务
java
风曦Kisaki1 小时前
# Linux运维Day02:LNMP架构部署、动静分离原理、Nginx地址重写、systemd服务管理
linux·运维·架构
大明者省1 小时前
乌邦托服务器系统www不同文件夹bird、infra建立隔离的虚拟环境
linux·运维·服务器
MXsoft6181 小时前
**降本增效两不误:精细化运维助力业务持续增长**
运维
csbysj20201 小时前
SQL UNION 操作符详解
开发语言
Volunteer Technology1 小时前
Spring AI MCP案例
java·开发语言·数据库
kobe_OKOK_1 小时前
ubuntu server设置 NTP 服务器
linux·服务器·ubuntu
团象科技1 小时前
跨境业务运维压力攀升,云原生运维补齐 AI 出海底层支撑短板
运维·人工智能·云原生
紫琪软件工作室1 小时前
SpringBoot Java邮件发送工具类
java·spring boot·spring