news 2026/5/15 14:53:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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一个地址问题

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

#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; }

输出结果:

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

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

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

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

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

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

1.1.2页表

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

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

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

简单页表结构理解:

1.2单进程分析

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

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

1.2.1进程-页表-磁盘联系

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

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

1.3父子进程分析

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

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

1.3.1内核代码引用

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

// 简化的逻辑 //分配新页表 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"即可

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

1.4.1mm_struct

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

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

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

图解:

源码引用:

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就是实际上分配的物理内存大小。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/15 14:52:46

docker Record of daily problems

不设置docker logs 的日志是无限的 cat /etc/docker/daemon.json {"data-root": "/data/docker","log-driver": "json-file","log-opts": {"max-file": "3","max-size": "100m"},&…

作者头像 李华
网站建设 2026/5/15 14:45:42

如何快速掌握SpleeterGui:Windows平台AI音乐分离的完整指南

如何快速掌握SpleeterGui&#xff1a;Windows平台AI音乐分离的完整指南 【免费下载链接】SpleeterGui Windows desktop front end for Spleeter - AI source separation 项目地址: https://gitcode.com/gh_mirrors/sp/SpleeterGui 你是否曾梦想将歌曲中的人声与伴奏完美…

作者头像 李华
网站建设 2026/5/15 14:41:04

Log4cpp在Windows下编译踩坑全记录:从VS2017到VS2022的snprintf冲突解决指南

Log4cpp在Windows平台编译实战&#xff1a;从源码冲突到跨平台日志方案 当C开发者需要在项目中引入可靠的日志系统时&#xff0c;Log4cpp常被视为首选方案之一。这个源自Java界著名日志框架Log4j的C实现&#xff0c;提供了线程安全、灵活配置和多种输出方式等特性。然而在实际编…

作者头像 李华
网站建设 2026/5/15 14:32:04

如何在Hermes Agent中自定义接入Taotoken服务

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 如何在Hermes Agent中自定义接入Taotoken服务 对于使用Hermes Agent框架的开发者而言&#xff0c;灵活接入不同的模型服务是构建智…

作者头像 李华