会吃番茄耶
26-06-12 22:09 微博认证:超话粉丝大咖(赵梦超话)

# 【嵌入式每日一题】内存、虚拟内存、内存映射

* **可以从两个层面来理解:**
* **谁在管理计算机资源**:内核、用户态、内核态
* **程序怎么看内存**:内存、虚拟内存、内存映射

它们共同一个完整的系统结构。

---

# 一、内核

**内核(kernel)** 是操作系统最核心的部分,管理着整个系统。

它负责:

```text
CPU 调度
内存管理
进程和线程
文件系统
设备驱动
网络通信
中断处理
权限控制
```

比如你写一个程序想做这些事:

* 读文件
* 创建进程
* 申请更多内存
* 访问网卡
* 操作摄像头
* 向串口发数据

这些都不能直接随便做,必须通过**内核**。

可以把内核理解成:所有硬件和关键资源的**唯一合法管理者**。

---

# 二、用户态和内核态

CPU 运行程序时,不是所有代码权限都一样高。通常至少分成两种运行级别:

* **用户态**
* **内核态**

## 1. 用户态

普通应用程序运行在**用户态**。

比如:

* 你的 C 程序
* 浏览器
* shell
* 视频播放器
* 大多数业务代码

用户态的特点是:

* 权限受限
* 不能直接操作硬件
* 不能直接访问内核数据
* 不能随便切换页表、改中断、操作设备寄存器

原因也很简单,如果每个程序都能随便控制硬件、修改内存、终止别的进程,系统会陷入混乱。

---

## 2. 内核态

操作系统内核运行在**内核态**。

内核态的特点是:

* 权限最高
* 可以访问所有内存
* 可以操作硬件
* 可以处理中断
* 可以调度进程
* 可以管理页表、文件系统、驱动等

---

## 3. 为什么要分用户态和内核态?

核心原因是 **隔离和保护**

比如某个普通程序写错了:

```c
int *p = (int *)0x12345678;
*p = 1;
```

不能因为写错一个地址,就使得整个操作系统搞崩。
所以用户态程序只能在受限范围内活动,其他交给内核态执行。

---

# 三、用户态和内核的联系

通过 **系统调用(system call)**。

比如:

```c
read()
write()
open()
fork()
mmap()
brk()
```

这些操作本质上都需要内核参与。

流程大概是:

```text
用户程序(用户态)

系统调用

CPU 切换到内核态

内核执行真正的资源操作

返回结果

回到用户态
```

比如你调用:

```c
write(fd, buf, len);
```

表面上是一个函数调用,但底层是:

* 从用户态切到内核态
* 内核检查 fd 是否合法
* 内核找到对应文件/设备
* 内核把数据写出去
* 再返回用户态

所以:

> **用户态是“提出请求”,内核态是“真正执行关键操作”。**

---

# 四、内存是什么?

这里先说最原始的“内存”。

**内存(RAM)** 就是机器上的物理存储器,用来临时存放正在运行的程序和数据。

例如:

* 程序代码装进内存后才能运行
* 变量要放在内存里
* 图像缓冲区要放在内存里
* 内核本身也在内存里

它是真实存在的硬件。

可以理解成:

> 内存 = 一大排真实的存储单元,每个单元都有物理地址。

比如物理地址可能像这样:

```text
0x00000000
0x00000004
0x00000008
...
```

---

# 五、虚拟内存

这一步是最关键的。现代操作系统不会让每个进程直接看到真实的物理内存地址,而是给每个进程提供一个**虚拟地址空间**。

也就是说:

> 程序看到的地址,通常不是物理地址,而是**虚拟地址**。

比如你在 C 里看到指针:

```c
int *p = malloc(100);
printf("%p\n", p);
```

这个地址一般是**虚拟地址**,不是实际的物理地址。

---

## 1. 为什么需要虚拟内存?

因为它有几个巨大好处。

### 第一,隔离进程

假设没有虚拟内存,所有进程都直接访问物理内存,那么:

* 进程 A 可能覆盖进程 B 的数据
* 程序崩溃会更容易拖垮整个系统
* 地址冲突严重

有了虚拟内存之后,每个进程都觉得自己拥有一套独立地址空间。

例如:

```text
进程A 看到 0x400000 是自己的代码
进程B 也看到 0x400000 是自己的代码
```

虽然虚拟地址一样,但它们最终映射到不同的物理内存。

---

### 第二,方便内存管理

程序不需要关心:

* 自己到底放在物理内存哪里
* 是否连续
* 是否被换页
* 是否和别的程序冲突

这些都由内核和硬件(MMU)负责。

---

### 第三,提供更大的“看起来的空间”

例如机器可能只有 8GB 物理内存,但每个 64 位进程都可以看到一个巨大的虚拟地址空间。

当然,这不代表物理内存真的有那么多,而是说:

> 操作系统给程序提供了一个统一、连续、好管理的“逻辑空间”。

---

# 六、虚拟地址是怎么变成物理地址的?

靠 **MMU(内存管理单元)** 和 **页表**。

## 1. 页的概念

虚拟内存和物理内存一般都按“页”来管理,比如常见一页 4KB。

于是:

* 虚拟地址空间被分成一页一页
* 物理内存也被分成一页一页

然后内核维护一张表,叫**页表**,记录:

> 某个虚拟页,对应哪个物理页

---

## 2. 地址转换过程

程序访问一个地址,比如:

```text
虚拟地址 0x12345678
```

CPU 不会直接拿它去访问 RAM。
而是:

1. 先交给 MMU
2. MMU 查页表
3. 找到对应的物理页
4. 拼出物理地址
5. 再访问实际内存

所以程序看到的是:

```text
虚拟地址
```

硬件真正访问的是:

```text
物理地址
```

---

# 七、虚拟地址空间长什么样?

对一个进程来说,虚拟地址空间通常可以粗略理解成这样:

```text
高地址
┌──────────────────────┐
│ 栈 stack │
│ 局部变量/函数调用 │
│ 向下增长 │
├──────────────────────┤
│ │
│ 内存映射区 mmap区 │
│ 文件映射/共享库/匿名映射│
│ │
├──────────────────────┤
│ 堆 heap │
│ malloc 动态分配区 │
│ 向上增长 │
├──────────────────────┤
│ BSS 段 │
│ 未初始化全局/静态变量 │
├──────────────────────┤
│ data 段 │
│ 已初始化全局/静态变量 │
├──────────────────────┤
│ text 代码段 │
│ 程序机器指令 │
└──────────────────────┘
低地址
```

这不是唯一精确布局,但足够理解大部分问题。

---

# 八、这些区域分别是什么?

## 1. text 段

存程序的机器指令,也就是编译后的代码。

比如你的函数最终会放到代码段里。

---

## 2. data 段

存**已初始化且初值非 0** 的全局变量、静态变量。

例如:

```c
int g = 10;
static int s = 20;
```

---

## 3. BSS 段

存**未初始化**或者**初始化为 0** 的全局变量、静态变量。

例如:

```c
int a;
static int b;
int c = 0;
```

程序加载时系统把这块区域清零。

---

## 4. heap 堆

动态分配内存的地方,`malloc/new` 常用这里。

堆通常向高地址增长。

---

## 5. stack 栈

函数调用、局部变量、返回地址等通常在这里。

栈通常向低地址增长。

---

## 6. mmap 区

内存映射区域,常用于:

* 共享库加载
* 大块匿名内存
* 文件映射
* 共享内存

---

# 九、内存映射是什么?

**内存映射(memory mapping)** 本质上是:

> 把某个对象“映射”到进程虚拟地址空间中的一段区域,让程序可以像访问普通内存一样访问它。

这里的“某个对象”可以是:

* 一个文件
* 一块匿名内存
* 一段共享内存
* 设备寄存器对应区域

---

## 1. 为什么叫“映射”?

因为它不是简单“复制一份数据过来”,而是建立一种地址对应关系。

例如:

```text
进程虚拟地址 0x70000000 ~ 0x70000fff

文件的某一段内容
```

或者:

```text
进程虚拟地址 0x70000000 ~ 0x70000fff

一组物理页
```

这样程序访问这段虚拟地址时,就等价于访问被映射的对象。

---

## 2. 文件映射例子

假设你把一个文件用 `mmap()` 映射进来:

```text
磁盘文件的一部分

映射到进程虚拟地址空间

程序像访问数组一样访问它
```

好处是:

* 不必每次都 `read()`
* 可以按内存方式访问
* 内核负责按页装入和回写

---

## 3. 匿名映射例子

如果不用文件,也可以映射一块匿名内存:

```c
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
```

这时得到的是一块普通可读写内存,但它来自内存映射机制,而不是传统堆增长。

这也是很多大块 `malloc` 背后的实现方式之一。

---

# 十、虚拟内存和内存映射是什么关系?

可以这样理解:

* **虚拟内存**:整个进程看到的地址空间机制
* **内存映射**:在虚拟地址空间里建立某段地址和某个对象之间的对应关系

也就是说:

> 内存映射是虚拟内存机制的一种具体使用方式。

---

# 十一、内核自己也用虚拟内存吗?

是的。
不仅用户进程有虚拟地址空间,**内核也有自己的地址空间映射**。

只是用户进程通常不能访问内核那部分区域。

从用户程序视角看,常常可以理解为:

```text
低/中间大块区域:用户空间
高地址某部分:内核空间
```

但具体布局依体系结构和内核配置而不同。

重点理解:

> 用户程序通常只能访问自己的用户空间,不能直接访问内核空间。

这也是“用户态受限”的重要体现。

---

# 十二、把这些概念串成一个完整流程

假设你写了这段代码:

```c
char *p = malloc(4096);
p[0] = 'A';
write(1, p, 1);
```

背后大致发生了什么?

## 第一步:`malloc(4096)`

* 你在用户态调用库函数 `malloc`
* `malloc` 可能从已有堆中分一块给你
* 或者不够时通过 `brk/mmap` 请求内核扩展虚拟内存
* 内核更新页表,建立虚拟页和物理页的映射

于是 `p` 拿到一个**虚拟地址**

---

## 第二步:`p[0] = 'A'`

* 程序在用户态写入这个虚拟地址
* CPU 通过 MMU 把虚拟地址翻译成物理地址
* 最终把字符写入对应物理内存

---

## 第三步:`write(1, p, 1)`

* 这是系统调用
* CPU 从用户态切到内核态
* 内核检查参数是否合法
* 内核从用户空间地址 `p` 读取 1 字节数据
* 内核把数据写到标准输出设备
* 返回用户态

这个流程把你提到的所有概念都串起来了:

* 用户态写代码
* 系统调用进内核态
* 内核管理资源
* 程序使用虚拟地址
* MMU 完成地址转换
* 页表维护映射关系
* `mmap/brk` 建立内存映射

---

# 十三、最核心的结构图

可以先记这个总图:

```text
┌────────────────────┐
│ 应用程序 │
│ (用户态运行) │
└─────────┬──────────┘

系统调用接口


┌────────────────────┐
│ 内核 │
│ (内核态运行) │
│ 管理进程/内存/文件/设备│
└─────────┬──────────┘

┌─────────┴─────────┐
│ │
▼ ▼
虚拟内存管理 硬件设备
(页表/MMU/映射) (CPU/内存/磁盘/网卡)
```

再看进程自己的内存结构:

```text
高地址
┌──────────────────────┐
│ 栈 stack │
├──────────────────────┤
│ mmap 映射区 │
├──────────────────────┤
│ 堆 heap │
├──────────────────────┤
│ BSS 段 │
├──────────────────────┤
│ data 段 │
├──────────────────────┤
│ text 段 │
└──────────────────────┘
低地址
```

---

# 十四、最简版定义汇总

## 1. 内核

操作系统最核心的部分,负责管理 CPU、内存、进程、文件、设备和网络。

## 2. 用户态

普通应用程序运行的受限状态,权限低,不能直接操作硬件和内核资源。

## 3. 内核态

内核运行的高权限状态,可以访问所有资源和硬件。

## 4. 内存

机器上的物理 RAM,是实际存在的硬件存储空间。

## 5. 虚拟内存

操作系统给每个进程提供的一套逻辑地址空间,程序看到的是虚拟地址,不是直接的物理地址。

## 6. 内存映射

在虚拟地址空间中建立地址和文件、物理页、匿名内存等对象之间的对应关系。

---

# 总结:

内核是操作系统最核心的部分,负责管理 CPU 调度、内存、进程、文件系统、设备驱动和网络等资源。为了保护系统,CPU 通常分为用户态和内核态。普通应用程序运行在用户态,权限受限,不能直接操作硬件或内核数据;内核运行在内核态,权限最高,可以执行系统级操作。用户程序如果需要访问文件、申请内存或操作设备,需要通过系统调用从用户态切换到内核态,由内核完成实际操作。

内存指的是物理 RAM,而虚拟内存是操作系统为每个进程提供的逻辑地址空间。程序中看到的地址通常是虚拟地址,CPU 通过 MMU 和页表把虚拟地址转换为物理地址,从而实现进程隔离和灵活的内存管理。进程的虚拟地址空间通常包括代码段、数据段、BSS 段、堆、内存映射区和栈等区域。内存映射是指把文件、匿名内存或共享内存等对象映射到进程的虚拟地址空间,使程序可以像访问普通内存一样访问这些内容。这样就形成了“用户程序运行在用户态,使用虚拟地址访问内存;内核运行在内核态,负责资源管理和地址映射”的整体结构。

发布于 日本