程序地址空间(基于c++和linxu的一些个人笔记

程序地址空间(基于c++和linxu的一些个人笔记)

我们之前讲C语言的时候,老师给大家画过这样的空间布局图:

┌─────────────────┐ 高地址 │ 内核空间 1G │ ├─────────────────┤ │ 栈 │ ↓ 向下增长 ├─────────────────┤ │ ↓ │ │ │ │ ↑ │ ├─────────────────┤ │ 共享库/mmap │ ├─────────────────┤ │ 堆 │ ↑ 向上增长 ├─────────────────┤ │ 未初始化数据BSS │ ├─────────────────┤ │ 初始化数据段 │ ├─────────────────┤ │ 代码段 │ └─────────────────┘ 低地址(用户空间 3G) 

可是我们对他并不理解!可以先对其进行各区域验证:


为什么字符串常量是只读的

在 C/C++ 中,字符串字面量(如 "hello")存储在只读内存区域,尝试修改会导致段错误。

原因:

  • 编译器优化:相同字符串只存一份,节省内存
  • 安全考虑:防止意外修改导致程序崩溃

存储位置: 通常放在代码段(.text 或 .rodata),这些段是只读的

#include<stdio.h>#include<string.h>intmain(){// 下面是错误的char*str1 ="hello"; str1[0]='J';// 崩溃!字符串常量不可修改printf("%s\n", str1);// 方法:拷贝到可修改内存char*str2 =strdup("hello"); str2[0]='J';// 可以!printf("%s\n", str2);// 输出 "Jello"free(str2);return0;}

C/C++ 变量存储位置表

内存区域存储内容示例特点
内核空间系统调用、内核数据-用户态不可直接访问
命令行/环境变量程序参数、环境变量argc, argv, environ程序启动时传入
栈区局部变量函数参数返回地址int x = 10; void func(int a)自动分配释放,向下增长(LIFO)
共享库动态链接库的代码和共享内存mmap(), .so文件多进程可共享
堆区动态分配的内存malloc(), new手动管理,向上增长,容易碎片化
BSS段未初始化全局变量和初始化为0的静态变量int global; static char buf[1024];程序加载时自动清零
数据段已初始化全局变量和静态变量(非零值)int count = 10; static int x = 5;占用可执行文件空间
代码段可执行代码和字符串常量int main() {...}, "MAX = 100"只读可执行
变量类型声明方式存储位置生命周期可见性
全局变量static int x = 10;(函数外)数据段(已初始化)或 BSS(未初始化)整个程序运行期间当前文件内
静态局部变量static int y = 20;(函数内)数据段或 BSS整个程序运行期间仅函数内
普通全局变量int z = 30;(函数外)数据段或 BSS整个程序运行期间全局可见(可用extern跨文件访问)
局部变量int a;(函数内)函数调用期间仅函数内

关键点:

  • 静态变量(无论局部还是全局)都存储在相同的内存区域(数据段或BSS)
  • 区别在于可见性(作用域),而不是存储位置
  • 静态局部变量只在函数内可见,但生命周期是整个程序

那程序地址空间真的存在吗?

不是内存,应该叫做虚拟地址空间。其地址是虚拟地址,不是真正的物理地址。

怎么证明: 父子进程修改一个变量有写时拷贝,结果两个的数据内容不一样但是地址一样,如果这是物理内存的话,明显不符合。所以只能是虚拟的。但是它具体是什么都不知道,C/C++ 里用到的都是虚拟地址,不是实际的内存地址。


关键解释

虚拟地址空间: 每个进程都有自己的虚拟地址空间(如32位系统下的4GB空间)

这是操作系统提供的抽象:

  • 进程"看到"的是连续的地址空间
  • 实际上这些地址被映射到物理内存的不同位置,甚至可能部分在磁盘上(页面交换)
  • 不是物理排布:物理内存不可能为每个进程都分配完整的地址空间
  • 例如:多个4GB进程不可能同时完全装入2GB物理内存

操作系统使用分页/分段技术,按需将虚拟地址映射到物理内存。

概念 vs 语言: 地址空间是计算机科学的概念,不是编程语言特有的概念。它是操作系统和硬件协作提供的抽象层。


虚拟地址与进程地址空间

大富翁例子

虚拟地址空间相当于大饼:每个进程都以为自己有4GB的物理内存,每个进程都以为自己在独占。

那操作系统给进程画了大饼,也要把大饼管理起来,就又到了先描述、再组织

  • struct饼:时间,内容……;然后组织起来

所以为了能把虚拟地址空间管理起来,虚拟地址空间就是一个数据结构,在Linux里叫 struct mm_struct

所以在我们创建进程的时候,要给进程画一个 mm_struct 这个虚拟地址(大饼)。task_struct 也是结构体变量,里面有指针指向对方。


虚拟地址空间怎么实现?

我们看起来有代码区,堆,栈……

它是一个从0到全的一共2的32次方个地址;同时它也划分了很多区域。

那这些区域要怎么理解?小女孩在桌子上画线本质就是区域划分,那怎么用计算机量化这个区域划分?

小女孩区域分化,用计算机量化一下!
structDesktop{int zs_start;// 张三区域开始int zs_end;// 张三区域结束int ls_start;// 李四区域开始int ls_end;// 李四区域结束};structDesktop area ={0,49,50,99};

所以区域划分只需要定好区域的开始和结束就可以; 就上面的每一个刻度就是地址,比如0处,1处等等;虽然地址空间没说明地址,但是只要知道可以使用的区域就可以。

桌子就是地址空间,宽度是2的32次方,刻度是地址空间上的地址,一张桌子上的小朋友先不管,每一个小朋友都有自己的区域。所以结构体大饼要有什么属性?

本质:是一个数据结构!!

structmm_struct{long code_start;// 代码段开始long code_end;// 代码段结束long init_start;// 初始化数据开始long init_end;// 初始化数据结束long uninit_start;// 未初始化数据开始long uninit_end;// 未初始化数据结束long heap_start;// 堆开始long heap_end;// 堆结束long stack_start;// 栈开始long stack_end;// 栈结束// ……pgd_t*pgd;// 页表根指针structvm_area_struct*mmap;// VMA链表};

就是每一个区域的开始虚拟地址和结束虚拟地址;调整区域就是修改开始和结束。

所以地址空间本质上就是一个数据结构,PCB里有一个指针会指向自己进程的地址空间。

它这个空间是可变的,比如你写的代码占100字节,那它就开辟一百字节的代码区就可以,与此同时,物理空间也开辟同样大小的,然后通过页表映射;而怎么改变区域大小,就是调整区域划分。


mm_struct 的初始化

1. 开辟空间

2. 初始化的值从哪里来?

地址空间是个对象,但是对象要被初始化,那它怎么初始化?相当一部分就是程序加载到内存的时候,在加载的时候来的。

程序在磁盘上是 ELF 格式的可执行文件,里面已经记录了:

  • 代码段多大、从哪开始
  • 数据段多大、从哪开始
  • 入口地址是什么

加载程序时,操作系统读取这些信息,填到 mm_struct 里。


一个进程一个虚拟地址空间,一个进程一套页表

虚拟地址空间对应的宽度是1字节;32位下有2的32次方个地址就是4GB,64位下有2的64次方个地址,宽度是1字节嘛,就这样乘。

用户空间3GB,然后内核空间1GB。

页表的作用

在这里说一个情况,你在程序里定义一个全局变量,然后在内存(物理地址)上就有了这个变量,在地址空间上也要对应有一个全局变量,这个全局变量的虚拟起始地址;与此同时,每个进程创建的时候要构建一个页表,一个进程一个地址空间,一个进程一个页表,页表的作用是:

页表 ┌──────────┬──────────┐ │ 0x111111 │ 0x112233 │ ├──────────┼──────────┤ │ ... │ ... │ └──────────┴──────────┘ 

它就长这样,然后左侧是变量的虚拟地址,右侧是变量的物理地址;当我们进程要访问虚拟地址,操作系统就查表来找到内存地址来访问,所以页表是做内存映射的。

我们在语言层用到的都是虚拟地址,我的代码里每一个变量都有地址,包括代码本身都有地址,都是虚拟地址,然后通过映射找到内存地址。

地址的理解

但是刚刚我们知道,程序地址空间的宽度是1字节,那四个字节代表是有四个地址,那么我们得到的是哪个地址,是四个地址里最小的地址;那一个地址只能访问一个字节,所以有了类型,所以有了起始地址加偏移量就可以访问到完整地址。


fork 与写时拷贝

子进程的很多东西都是包括 task_struct 拷贝父进程的,然后改一改;子进程也要有自己的虚拟地址空间;也有自己对应的页表,因为子进程有自己的代码;那子进程的页表和虚拟地址空间都拷贝父进程。

但是页表拷贝就是浅拷贝了,所以为什么打印的地址是一样的,而且为什么全局变量被父子共享;代码也共享了;

要是子进程要修改全局变量,操作系统就会在内存开辟一个新空间然后把变量复制过去,子进程的映射也改变到新创建的变量,就可以看起来是一个地址(虚拟地址)但是值不同,因为真实的物理地址不一样;这就是写时拷贝。

与此同时用户只能看到虚拟地址看不到物理地址。


那为什么要有虚拟地址空间?

1. 将地址从无序变有序

物理内存可能是碎片化的、散落各处的。虚拟地址空间让每个进程看到的是连续、统一的地址布局。

2. 权限保护

页表当中还有权限,如果你要对一个代码区写入但是没权限,去查页表,操作系统就不会让你完成,就对代码进行保护。即地址转化过程中,可以对你的地址和操作进行合法性判定。

几个问题:

a. 什么是野指针

char*str ="helloworld";*str ='H';// 崩溃!

a. 野指针其实就是已经被释放的空间你去访问,然后查页表要去访问,结果操作系统为了保护代码,可能会直接把你的进程挂掉,导致程序崩溃;不一定程序奔溃,做个了解。

b. 这个叫字符串常量,是不可被修改的;那会发生什么,可以编译过,运行的时候崩掉了,字符串在字符常量区,它的权限是只读,没权限,所以页表转换失败。

3. 缺页中断(按需加载)

还没讲,大概意思就是物理空间如果需要的很大,可以先加载一点然后映射到页表,用的时候动态加载。

不需要一次性把整个程序加载到内存,用到哪页加载哪页,节省内存。

4. 让进程管理和内存管理进行一定程度的解耦合

进程只管虚拟地址,内存管理只管物理页的分配回收,两边通过页表连接,互不干扰。


澄清一些问题!!!

1. 我么可以不加载对应的代码和数据,只有 task_struct,mm_struct…,页表;

可以。创建进程时先建好"骨架",代码和数据可以按需加载(Demand Paging)。

2. 创建进程,先有 PCB 和 mm_struct…才会有代码;

对。顺序是:分配 task_struct → 分配 mm_struct → 建立页表 → 映射代码段/数据段 → 按需加载。

3. 如何理解进程挂起?

进程挂起就是先找到对应的进程,把页表标记为"不在内存",把物理页内容换出到磁盘(swap区)。这个时候虚拟地址还在,mm_struct 还在,只是物理页暂时不在内存里。恢复时再换回来。


堆区不止一个地址

堆区不止一个吧?不止一个虚拟地址吧?因为我需要不断地去 new 啊,然后它每次之间都是离散的呀?

没事的,mm_struct 里维护了一个 vm_area_struct 链表,有指向下一个节点的指针,然后分很多堆区也没事,一份堆区对应一个 vm_area_structvm_area_struct 里有这个堆区的开始和结束地址。

structvm_area_struct{unsignedlong vm_start;// 区域起始地址unsignedlong vm_end;// 区域结束地址structvm_area_struct*vm_next;// 下一个VMAunsignedlong vm_flags;// 权限标志// ...};

就是一个地址区域相当于一个节点,整个是一个链表,然后节点里有指向下一个节点的指针。

mm_struct └── mmap ──► [VMA: 代码段] ──► [VMA: 数据段] ──► [VMA: 堆] ──► [VMA: 栈] ──► NULL 

总结

  1. 虚拟地址空间是操作系统给进程制造的"幻觉",让每个进程以为自己独占一大片连续内存。
  2. 内核用 mm_struct 描述虚拟地址空间,记录各区域的起止地址。
  3. vm_area_struct 链表管理各个区域(代码段、数据段、堆、栈……)。
  4. 页表负责把虚拟地址翻译成物理地址,同时做权限检查。
  5. 这套机制实现了:
    • 进程隔离(互不干扰)
    • 简化编程(不用关心物理地址)
    • 灵活分配(虚拟连续,物理可以不连续)
    • 写时拷贝(fork 高效)
    • 权限保护(防止非法访问)
    • 按需加载(节省内存)

Read more

Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成

Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 bybit 的鸿蒙化适配指南 - 实现高性能交易数据获取、支持 WebSockets 实时订单簿与加密货币交易接口集成 前言 在进行 Flutter for OpenHarmony 的金融科技(FinTech)应用开发时,对接主流交易所的实时数据和交易功能是核心需求。bybit 是一个专为 Bybit 交易所设计的异步 Dart SDK。它封装了 REST API 调用和复杂的 WebSockets 订阅逻辑。本文将探讨如何在鸿蒙系统下构建低延迟、高可靠的加密资产交易终端。 一、原原理分析 / 概念介绍 1.1 基础原理 bybit 库基于 http 处理基础请求,并利用 web_socket_

By Ne0inhk
YOLO26来了,更好、更快、更小的 YOLO 模型,使用YOLO26训练自己的数据集和推理(附YOLO26网络结构图),租用 GPU 服务器训练教程,YOLO26创新点解析

YOLO26来了,更好、更快、更小的 YOLO 模型,使用YOLO26训练自己的数据集和推理(附YOLO26网络结构图),租用 GPU 服务器训练教程,YOLO26创新点解析

目录 * 摘要 * YOLO26更新点 * ⚡⚡C3k2 小优化 * ☑️ YOLO26 C3k2代码 * ☑️ YOLO11 C3k2代码 * ⚡⚡移除分布焦点损失(DFL) * ⚡⚡端到端、无需 NMS 推理 * ⚡⚡ProgLoss 与 STAL * ☑️ProgLoss * ☑️STAL (小目标感知标签分配) * ⚡⚡MuSGD优化器 * 从机器人到制造业:YOLO26 的用例 * 🐴一、YOLO26 源码下载与模型下载 * ⚡⚡YOLO26模型结构图 * ⚡⚡1.源码下载 * ⚡⚡2.官网的预训练模型下载 * 🐴二、数据集准备 * ⚡⚡LabelImg & Labelme * ☑️ LabelImg(仅限矩形检测框) * ☑️ Labelme * ⚡⚡ X-AnyLabeling * ⚡⚡ 旋转框 (OBB) 标注工具:roLabelImg * ⚡⚡1.目标检测数据集标注软件 * ⚡⚡2.voc数据集格式转换

By Ne0inhk
鸿蒙APP开发从入门到精通:性能优化与Next原生合规

鸿蒙APP开发从入门到精通:性能优化与Next原生合规

《鸿蒙APP开发从入门到精通》第11篇:性能优化与Next原生合规 🏎️✅ 内容承接与核心价值 这是《鸿蒙APP开发从入门到精通》的第11篇——性能优化与Next原生合规篇,承接第10篇的「AI原生与用户增长」,100%复用项目架构,为后续第12篇的电商购物车全栈项目最终上线铺垫性能优化与Next原生合规的核心技术。 学习目标: * 掌握鸿蒙APP性能优化的定义与架构; * 实现启动优化、渲染优化、网络优化等性能优化功能; * 理解Next原生合规的原理与实现方式; * 开发代码规范、权限合规、数据合规等合规功能; * 优化性能与合规的用户体验(响应速度、内存占用、电池消耗)。 学习重点: * 鸿蒙APP性能优化的开发流程; * 性能优化的分类与使用场景; * 启动优化、渲染优化、网络优化的实现; * Next原生合规的设计与实现。 一、 性能优化基础 🎯 1.1 性能优化定义 性能优化是指对应用进行优化,提高应用的响应速度、降低内存占用、减少电池消耗等,主要包括以下方面: * 启动优化:优化应用的启动时间; * 渲染优化:优化应用的界

By Ne0inhk
Flutter 组件 cron_parser 的适配 鸿蒙Harmony 实战 - 驾驭 Cron 定时任务预测算法、实现鸿蒙端高精度调度中心与冲突检测方案

Flutter 组件 cron_parser 的适配 鸿蒙Harmony 实战 - 驾驭 Cron 定时任务预测算法、实现鸿蒙端高精度调度中心与冲突检测方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 cron_parser 的适配 鸿蒙Harmony 实战 - 驾驭 Cron 定时任务预测算法、实现鸿蒙端高精度调度中心与冲突检测方案 前言 在鸿蒙(OpenHarmony)生态的智能家居、自动运维以及金融级批处理应用中,“定时触发”是业务逻辑的绝对核心。面对“每周一至周五凌晨 3 点半同步数据”、“每个月最后一个周六执行对账”这种复杂的调度需求,如果仅仅靠手动写 Timer 或者复杂的 if-else 日期判断,不仅代码极度臃肿,更容易产生极难排查的逻辑死角。 我们需要一种标准化的、具备“时间旅行”预判能力的调度描述语言。 cron_parser 是一套完美支持标 Cron 表达式语法(如 * * * * *)的核心解析库。它不仅能判断某个瞬间是否符合规则,更能预判下一次、

By Ne0inhk