Bootstrap

大话C语言:第27篇 内存模型

1 存储硬件概述

现代计算机遵循冯诺依曼体系结果,存储分为:

  • 外部存储器:长期存放数据,掉电不丢失数据。例如,硬盘、flash、rom、u 盘、光盘、磁带。

  • 内部存储器:暂时存放数据,掉电数据丢失。例如,DDR内存条

应用程序从外部存储器加载至内部存储器过程包括:

  • 用户启动应用程序:用户通过点击图标、从命令行运行或其他方式启动应用程序。

  • 操作系统响应:操作系统接收到启动请求,并开始处理;如果应用程序尚未在内存中,操作系统会查找应用程序在磁盘上的位置(通常是可执行文件)。

  • 加载程序(Loader)工作:加载程序首先读取应用程序的头部信息,这通常包括程序所需的各种资源、依赖项和代码段的位置。

  • 读取代码和数据:加载程序从磁盘读取应用程序的代码段、数据段和其他必要的段。这些段通常包括程序的指令(即代码)和程序运行所需的数据。

  • 分配内存空间:操作系统为应用程序分配所需的内存空间。这通常涉及到管理物理内存和虚拟内存。

  • 加载至内存:将从磁盘读取的代码和数据段加载到分配的内存空间中。如果内存空间不足以容纳整个应用程序,操作系统会使用虚拟内存技术,将部分应用程序存储在磁盘上,并在需要时将其交换到RAM中。

  • 设置程序计数器和其他寄存器:操作系统设置程序计数器(PC)以指向程序的入口点(即程序开始执行的指令)。还会设置其他必要的寄存器,以支持程序的执行。

  • 执行:一旦程序在内存中准备好,操作系统会将控制权交给应用程序,程序开始执行。

2 物理内存和虚拟内存

  • 物理内存:实实在在存在的存储设备

  • 虚拟内存:内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间)。

其中,虚拟内存的工作原理是将进程使用的内存分为多个页面(通常为4KB或8KB),每个页面都有一个唯一的虚拟地址。当进程需要访问某个页面时,操作系统会检查该页面是否已经在物理内存中。如果已经在内存中,则直接访问该页面;如果不在内存中,则操作系统会将该页面从磁盘上的虚拟内存中读取到内存中,并将其映射到进程的虚拟地址空间中。

操作系统会在物理内存和虚拟内存之间做映射。

其中,虚拟内存主要有以下几种类型:

  • 分页式虚拟存储:物理内存和虚拟内存都被划分为固定大小的页面(Page)。当程序需要更多内存时,操作系统将不常用的页面移动到硬盘上的交换空间,并将需要的页面加载到物理内存中。这种方式实现了内存的动态分配和页面的调度,但需要频繁地进行页面调入和调出,可能影响系统性能。

  • 段式虚拟存储:程序的地址空间被划分为多个逻辑段,每个段可以具有不同的长度和访问权限。当程序需要内存时,操作系统将程序的逻辑段映射到物理内存或硬盘上的交换空间。这种方式更灵活,可以根据程序的需要分配不同大小的内存空间,但需要额外的管理和调度。

  • 请求分页式虚拟存储:结合了分页式和段式虚拟存储的特点。在这种方式中,程序的地址空间被划分为多个段,每个段又被划分为多个页面。

3 逻辑地址和物理地址

  • 逻辑地址:是指程序在运行过程中使用的地址,也称为虚拟地址(Virtual Address)。它是由CPU生成的,用于访问内存中的数据。逻辑地址的大小和位数取决于处理器的架构和操作系统的设计,通常是一个定长的二进制数值。在执行指令时,CPU通过将逻辑地址转化为物理地址来获取数据。

  • 物理地址:是指内存中实际的地址,也称为实地址(Real Address)。物理地址表示内存模块中每个存储单元(通常是字节)的唯一标识符,因此具有唯一性,且直接与内存相关联。物理地址通常是一个以十六进制表示的数字,它确定了计算机中的实际内存位置。

其中,应用程序主要使用的是逻辑地址;逻辑地址是程序代码中使用的地址,由程序员或操作系统生成。在 32 位系统下,每个进程(运行着的程序)的寻址范围是 0x00000000 ~0xff ff ff ff,空间大小为4G。

4 C语言程序内存布局

4.1 系统空间

存放在整个内核的代码和所有的内核模块,用来内核空间执行 Linux 系统调用。

4.2 栈区

存局部变量、函数,调用函数时会开辟栈区,函数结束时就自动回收,遵循后进先出的原则,从高地址向低地址增长。

栈区主要特点包括:

  • 栈内存由编译器在程序编译阶段完成。

  • 函数返回后该函数的栈空间消失,所以函数中返回局部变量的地址都是非法的。

  • 栈区存放局部变量。

  • 堆区的空间是由下往上增长的。

  • 自动分配内存,{}内有效,离开{}自动释放。

  • 未初始化的值为随机值。

  • 未初始化的静态局部变量,其值为0

4.3 堆区

malloc、realloc、calloc等开辟的内存就在堆,从低地址向高地址增长,由程序员分配和释放,系统不自动回收,所以一定要记得申请了就要释放,以免溢出。动态内存是开发者手动分配的,是堆分配的。

堆的主要特点:

  • 堆内存是在程序执行过程中分配的,用于存放进程运行中被动态分配的的变量。

  • 函数返回这段内存不会消失。

  • 动态申请的内存,程序员自己管理,用完要free,否则内存泄漏。

  • 堆区的空间是由下往上增长的。

  • 内存里面的内容为随机值,一般用memset函数清0。

注意,

  • 避免分配大量的小内存块。分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的 系统开销大。

  • 仅在需要时分配内存。只要使用完堆上的内存块,就需要及时释放它(如果使用动态分配内存,需要遵守原则: 谁分配,谁释放), 否则可能出现内存泄漏

4.4 数据段

数据段包含程序中已经初始化的全局变量和静态变量。这些变量在程序运行期间一直存在,并且它们的值在程序开始执行之前就已经确定。

数据段可以进一步细分为已初始化数据段(包含有明确初始值的全局变量和静态变量)和未初始化数据段(也称为BSS段,包含未明确初始化的全局变量和静态变量,通常初始化为0)。

数据段分为:

  • rodata段:Read-Only Data段的缩写,是程序内存中的一个特定区域,用于存放只读数据,也就是那些不可修改的常量数据。这些数据在程序执行期间不会发生变化,因此被设计为只读,以防止程序意外地修改它们。rodata段常被称为常量区,存放的是诸如整数常量、字符串常量等。

  • bss段:程序内存布局中的一个特定段,它主要用于存放程序中未初始化的全局变量和静态变量。这些变量在编译时并没有明确赋予初值,因此在程序加载到内存时,系统会自动将bss段的内存空间清零,以保证这些变量在使用前具备确定的初值。

  • data段:主要用于存储程序中已经初始化且初值不为0的全局变量和静态局部变量。这些数据在程序编译时就已经确定,并且在程序运行期间会保持不变,因此它们被存放在一个可读可写的内存区域中。

注意,data段与bss段在功能上有所区别:

  • bss段主要用于存储未初始化的全局变量和静态变量,这些变量在程序加载时会被自动初始化为0。而data段则专门用于存储已初始化的非零变量。这种分工使得内存管理更为高效和有序。

  • 在程序执行过程中,data段的内容是保持不变的。这是因为这些数据在程序编译时已经确定,并且在程序运行期间不会被修改。这使得.data段成为程序中的一个稳定的数据存储区域,为程序提供了可靠的数据支持。

4.5 文本段

文本段,也称为代码段或简称为文本,是目标文件或内存中的程序段之一,其中包含可执行指令。作为内存区域,可以将文本段放置在堆或堆栈下方,以防止堆和堆栈溢出覆盖它。

通常,文本段是可共享的,因此对于频繁执行的程序(例如文本编辑器、C 编译器、shell 等),只需要在内存中保存一个副本。此外,文本段通常是只读的,以防止程序意外修改其指令。

;