通用操作系统:DOS/WIN/UNIX
当我们在操作系统中“双击”一个程序图标,或者在命令行输入程序名并回车时,操作系统会启动一系列复杂而精妙的步骤,将这个存储在硬盘上的“静态”文件,转变为一个在内存中“动态”运行的程序——一个进程。
硬盘上的程序文件:静态的存在
- 在硬盘上,程序以可执行文件(如 Windows 上的
.exe
,Linux 上的 ELF 文件)的形式存在。 - 这个文件内部已经按照特定的格式(如PE或ELF)组织好了,其中包含了编译好的:
- 代码段数据 (Text Segment data): 程序的机器指令。
- 数据段数据 (Data Segment data): 已初始化全局/静态变量的初始值。
- BSS 段元数据: 未初始化全局/静态变量的名称和大小信息(不包含实际的零值,以节省文件空间)。
- 其他辅助信息:如符号表、重定位信息、动态链接库信息等。
- 在硬盘上,程序以可执行文件(如 Windows 上的
操作系统加载:构建虚拟内存空间
- 当程序被执行时,操作系统(尤其是内存管理单元 MMU)会为其创建一个新的、独立的、私有的虚拟地址空间。
- 操作系统并不会一次性将整个程序文件完整地载入物理内存,而是采用按需加载(demand paging) 或 页式内存管理 的策略。它会:
- 映射代码段: 将可执行文件中的代码段映射到进程虚拟地址空间的代码段区域。这块区域通常是只读的,且可能被多个运行相同程序的进程共享物理内存。
- 映射数据段: 将可执行文件中已初始化的数据段映射到进程虚拟地址空间的数据段区域。这块区域是可读写的,且是进程私有的。
- 映射BSS段: 根据可执行文件中BSS段的大小信息,在进程虚拟地址空间的BSS段区域分配一块内存,并由操作系统在加载时将其清零。这块区域也是可读写的,且是进程私有的。
- 初始化栈: 在进程虚拟地址空间的栈区域分配一块内存,并设置好栈顶指针,用于存储函数调用信息和局部变量。这块区域是可读写的,且是进程私有的。
- 初始化堆: 在进程虚拟地址空间的堆区域分配一块内存区域,但通常只是预留起始地址,实际的堆内存分配在程序运行时通过
malloc
/new
等函数按需增长。这块区域是可读写的,且是进程私有的。 - 加载共享库: 如果程序依赖动态链接库,操作系统会找到这些库,并将它们的代码和数据也加载(或映射)到进程的虚拟地址空间中。
程序运行:在内存中动态操作
- CPU执行指令: CPU开始从代码段的入口点(通常是
main
函数的起始指令)执行指令。 - 数据访问: 程序执行过程中需要访问变量时:
- 全局变量和静态变量: 它们已经分配在数据段或BSS段中,其地址是固定的。局部静态变量的内存也是预先分配在数据段或BSS段,但其逻辑初始化(特别是对于C++对象)只在第一次执行到其定义时进行。
- 局部(非静态)变量: 在函数调用时在栈上分配,函数返回时自动释放。
- 动态分配内存: 当程序需要更多内存时,会向操作系统请求(通过
malloc
/new
),内存会在堆上分配。这部分内存需要程序员手动管理释放。
- 虚拟到物理映射: 每当程序访问一个虚拟地址时,MMU会将这个虚拟地址翻译成对应的物理地址,然后CPU才能真正访问物理内存中的数据。
- CPU执行指令: CPU开始从代码段的入口点(通常是
核心思想:虚拟内存
整个过程的核心在于虚拟内存。它为每个程序提供了一个独立的、隔离的内存视图,使得程序可以简单地认为自己拥有全部内存。操作系统负责复杂的虚拟到物理的地址映射,从而实现了:
- 隔离性: 防止不同程序之间相互干扰。
- 安全性: 阻止非法内存访问。
- 灵活性: 允许程序使用比实际物理内存更大的地址空间。
- 效率: 通过共享只读代码和按需加载,优化了物理内存的使用。
通过这种方式,硬盘上的二进制文件被有效地转换为一个在内存中具有特定结构和行为的进程。
嵌入式设备:裸机/RTOS/Linux
对于嵌入式设备,内存管理和程序加载的机制与桌面/服务器操作系统有所不同,但核心思想是类似的。关键在于嵌入式设备的复杂度和其上运行的操作系统的类型。
嵌入式设备的内存管理特点
嵌入式设备可以大致分为几类:
- 裸机(Bare-metal)/无操作系统:
- 直接在硬件上运行程序,没有复杂的OS层。
- 常见的应用:简单的传感器节点、控制器、小型家电。
- 实时操作系统 (RTOS):
- 提供任务调度、中断管理、同步机制等基本OS功能,但通常不提供复杂的虚拟内存管理。
- 常见的应用:工业控制、汽车电子、医疗设备。
- 嵌入式Linux/Android等重量级操作系统:
- 提供了完整的Linux内核功能,包括MMU支持的虚拟内存。
- 常见的应用:智能手机、平板、智能电视、高性能网关。
不同类型嵌入式设备的详细说明
1. 裸机(Bare-metal)/无操作系统
- 程序加载:
- 通常程序(固件)在开发阶段就被编译、链接成一个单一的二进制文件(如
.bin
或.hex
文件)。
- 这个文件中包含了:
- 代码段(.text): 程序的机器指令。
- 数据段(.data): 已初始化的全局/静态变量的初始值。
- BSS 段信息: BSS 段的起始地址和大小(但没有实际的零值数据)。
- 启动代码(Startup Code): 这是最关键的部分,它会在上电后第一个执行。
- 这个文件会直接烧录到设备的非易失性存储器中,例如:
- Flash 内存(NAND Flash, NOR Flash): 程序通常从这里启动。
- ROM/EEPROM: 某些启动代码可能存储在这里。
- 上电后,CPU直接从Flash内存的起始地址(或通过Bootloader跳转)开始执行代码。
- 通常程序(固件)在开发阶段就被编译、链接成一个单一的二进制文件(如
- 内存布局:
- 没有虚拟内存。程序直接操作物理地址。
- 内存通常分为:
- Flash / ROM: 存储代码段和只读数据(例如常量字符串)。
- RAM (SRAM/DRAM):
- 代码段(如果有RAM执行需求): 有些优化会将频繁执行的代码从Flash复制到RAM中运行,以提高速度。
- 数据段: 存储已初始化的全局/静态变量。在程序启动时,这些初始值通常会从Flash复制到RAM中。
- BSS 段: 存储未初始化(或初始化为0)的全局/静态变量。在程序启动时,启动代码会负责将这块RAM区域清零。
- 堆: 如果程序使用
malloc
等动态内存分配,会从RAM中划分一块区域作为堆。 - 栈: 局部变量和函数调用信息。通常从RAM的最高地址向低地址增长。
- 各段是直接的物理内存区域,没有MMU进行地址转换。 内存空间相对有限且是静态划分好的。
- 局部静态变量: 它们会像全局变量一样,被分配到RAM中的数据段或BSS段,但其初始化(如果是C++对象)仍会在第一次执行到时发生,通过编译器生成的标志来确保。
- 启动代码(Startup Code)的执行
- 这是 BSS 段处理的真正“运行”开始的地方。启动代码通常是用汇编语言编写的,因为它需要在C代码运行之前完成一些底层设置。
- 设置堆栈指针: 启动代码的第一步通常是设置主栈指针,确保C代码可以正确地使用栈。
- 初始化数据段: 启动代码会将Flash中存储的 数据段(.data) 的初始值,复制到RAM中对应的位置。因为Flash是只读的,而数据段需要可读写。
- 清零 BSS 段: 这是处理 BSS 段的关键步骤。启动代码会获取 BSS 段的起始地址和大小(这些信息由链接器提供,并可能嵌入在启动代码或作为特定符号),然后通过一个循环操作,将 BSS 段对应的RAM区域的所有字节都清零(填充0x00)。
- 跳转到C代码入口: 完成了这些底层初始化后,启动代码会调用C语言的入口函数,通常是 main() 函数。
- C/C++ 程序开始执行
- 当 main() 函数被调用时,所有的全局变量和静态变量(包括数据段和BSS段中的)都已经准备就绪。
- BSS 段中的变量此时都已经被保证为0,程序可以安全地访问它们。
- 如果程序使用了动态内存分配(如 malloc),启动代码可能还会提前设置好堆的起始和结束地址。
2. 实时操作系统 (RTOS)
- 程序加载:
- RTOS内核本身和应用程序代码通常都烧录在Flash中。
- 上电后,Bootloader启动RTOS内核,然后RTOS内核会加载并启动应用程序(可能是一个或多个任务)。
- RTOS通常不提供完整的进程概念(如Linux),更多是任务(Task)。
- 内存布局:
- 大多数RTOS(例如FreeRTOS, RT-Thread, uC/OS-III)不使用MMU进行虚拟内存管理。任务共享同一个物理地址空间。
- 代码段、数据段、BSS 段: 应用程序的这些段通常也是直接映射到物理RAM中。
- 代码可能仍在Flash中执行,或复制到RAM。
- 数据段和BSS段都在RAM中。
- 堆: RTOS会提供自己的内存管理接口(如
pvPortMalloc
在FreeRTOS中),从一个全局的RAM池中分配堆内存。 - 栈: 每个任务都会有自己独立的栈。这些栈通常是预先在RAM中分配好的固定大小的区域,并在任务切换时进行保护。
- 由于没有MMU,任务之间的内存隔离性较弱。一个任务的错误访问可能破坏其他任务的数据。
- 局部静态变量: 同样位于数据段或BSS段,并遵循懒惰初始化原则。
3. 嵌入式Linux/Android等重量级操作系统
- 程序加载:
- 这与桌面/服务器Linux系统非常相似。操作系统(Linux内核)会管理复杂的启动流程,包括Bootloader、内核启动、文件系统挂载等。
- 用户程序(如APK在Android上,或普通应用程序在嵌入式Linux上)作为独立进程运行。
- 内存布局:
- 提供完整的虚拟内存管理。每个进程都有自己独立的虚拟地址空间,并通过MMU映射到物理内存。
- 所有我们在桌面系统上讨论的段(代码段、数据段、BSS 段、堆、栈)都在每个进程的虚拟地址空间中存在,且遵循相同的行为和特性。
- 隔离性强: 进程之间通过虚拟内存实现隔离,一个进程的内存错误通常不会影响其他进程。
- 物理内存共享: 多个进程运行相同的程序(如多个浏览器标签),它们的只读代码段可能会共享物理内存。
- 局部静态变量: 行为与桌面Linux完全一致,其内存位于进程的虚拟地址空间的特定段(数据/BSS),并保证在首次执行时逻辑初始化。
总结:
特性 / 设备类型 | 裸机/无OS | RTOS | 嵌入式Linux/Android |
---|---|---|---|
虚拟内存 | 无 | 通常无 | 有(全功能) |
地址空间 | 直接物理地址 | 共享物理地址空间 | 独立的虚拟地址空间 |
程序加载 | 直接从Flash启动/拷贝到RAM | 内核启动后加载任务/程序 | 内核启动后作为独立进程加载 |
内存隔离 | 无 | 任务间隔离性弱(无MMU) | 进程间隔离性强(有MMU) |
段的物理位置 | 代码在Flash/RAM,数据/BSS/堆/栈在RAM | 代码在Flash/RAM,数据/BSS/堆/栈在RAM | 虚拟地址映射到物理RAM,可能共享物理代码 |
局部静态变量行为 | 内存分在RAM数据/BSS,首次执行逻辑初始化 | 内存分在RAM数据/BSS,首次执行逻辑初始化 | 内存分在虚拟地址数据/BSS,首次执行逻辑初始化 |
因此,对于嵌入式设备,是否“和桌面一样”取决于其运行的操作系统是否支持虚拟内存。无操作系统或RTOS通常不具备虚拟内存,直接操作物理地址;而运行Linux等大型OS的嵌入式设备,其内存管理机制则与桌面系统基本相同。