1、内存基础概念:内存分类、内存地址、字节对齐、内存单位
做调试这么多年,我发现自己干的活,十次里有八次跟内存脱不了干系。你要是不把基础概念吃透,遇到诡异 bug 就得抓瞎。这一章咱们先把地基打牢。
1.1 内存分类
内存不是只有一种。从掉电能不能保持数据来分,就是两大阵营:易失性内存和非易失性内存。说白了,一个怕断电,一个不怕。
易失性内存
最常见的就是 RAM(Random Access Memory)。RAM 又分两种:
- SRAM(静态 RAM)——速度快,功耗高,成本也高。一般 CPU 的一级、二级缓存都用它。我记得在优化一个网络设备驱动时,硬是把关键数据结构钉在了 SRAM 上,吞吐量直接翻了一倍。
- DRAM(动态 RAM)——需要定期刷新,否则数据就丢了。价格便宜,容量大。我们的主内存基本都是 DRAM,像 DDR4、DDR5 就是它的进化版。
非易失性内存
断电后数据还在的那一类。你看:
- ROM(Read-Only Memory)——出厂就写死了,只能读不能写。
- PROM / EPROM / EEPROM——可编程、可擦除,但现在基本被 Flash 替代了。
- Flash(闪存)——分为 NOR Flash 和 NAND Flash。NOR 可以随机读取,适合存代码;NAND 容量大、密度高,适合存文件和数据。
我个人习惯把内存看作「数据临时工」和「数据永久工」—— RAM 是临时工,干活快但下班就忘;ROM / Flash 是永久工,记性好但速度慢。你写嵌入式代码时,用哪种内存,直接决定了你的系统功耗和启动时间。
1.2 内存地址
每个字节都有自己的地址,就像每家每户的门牌号。但这里有两个概念必须拎清楚:物理地址和虚拟地址。
- 物理地址——CPU 地址总线直接送到内存控制器的真实地址。裸机或内核态代码经常直接操作它。
- 虚拟地址——应用程序看到的地址。通过 MMU(内存管理单元)映射到物理地址。为什么这么做?方便隔离、方便分段、方便换页。
你想想看,一个 32 位系统,虚拟地址空间是 4GB。但实际上物理内存可能只有 512MB。这中间怎么玩?就是靠页表映射。我在项目中遇到过因为页表没刷干净导致内核 panic,查了三天才发现是 TLB 残项问题。
| 概念 | 作用 | 谁在管 |
|---|---|---|
| 物理地址 | 真实硬件单元访问 | CPU 总线 / 内存控制器 |
| 虚拟地址 | 进程隔离、简化编程 | MMU + 操作系统 |
| 逻辑地址 | 段式管理中的偏移 | 编译器和链接器 |
千万不要在用户态直接读写物理地址。除非你调用了
mmap 并指定了 /dev/mem,而且得保证你的代码只运行在调试环境——我曾经手贱在生产环境这么干过,机器直接挂了,恢复花了两小时。
1.3 字节对齐
这个东西看着简单,但带来的坑比想象的多。字节对齐指的是:数据的起始地址必须是其大小的整数倍。
- 为什么要有对齐?——CPU 读内存是按「字」或「双字」读的。不对齐的话,一次读操作可能变成两次读,甚至触发异常。
- 对齐规则:
char(1 字节):任意地址short(2 字节):地址必须是 2 的倍数int(4 字节):地址必须是 4 的倍数double(8 字节):地址必须是 8 的倍数
看一个例子:
struct Example {
char a; // 偏移 0
int b; // 偏移 4(不是因为直接 1+4,而是编译器填充 3 字节)
short c; // 偏移 8
// 最后填充 2 字节,保证结构体大小是 4 的倍数
};
// sizeof(struct Example) = 12
实际编译时,编译器会在 a 后面自动塞 3 个填充字节(padding),让 b 对齐到 4 字节边界。如果你用 #pragma pack(1) 强行取消对齐,那访问 b 时 CPU 就可能访问两次总线——性能下降事小,在 ARM 平台上直接抛异常。
我曾经调试过一个跨平台网络的 bug:结构体在 x86 上没 padding,但 ARM 上编译器自动加了 padding,导致两端发过来的协议报文二进制不匹配。排查了一整个下午,最后用
offsetof 宏一个个字段检查才定位到。从那之后,我所有的网络协议结构体都会加 __attribute__((packed))。
1.4 内存单位
常见的单位就那么几个,但混淆的也不少:
| 单位 | 大小 | 典型场景 |
|---|---|---|
| bit(位) | 0 或 1 | 寄存器标志位、通信波特率 |
| Byte(字节) | 8 bits | 文件大小、内存最小可寻址单位 |
| KB(千字节) | 1024 Bytes | 小缓冲区、缓存行 |
| MB(兆字节) | 1024 KB | 图片、音频文件 |
| GB(吉字节) | 1024 MB | 内存容量、磁盘分区 |
| TB(太字节) | 1024 GB | 大数据存储、服务器内存池 |
注意:硬件厂商(比如硬盘)有时用 1000 进制,软件用 1024 进制。这就是为什么你买的 1TB 硬盘,在操作系统里只显示 931GB 左右——不是缩水,是定义不同。我建议你写代码时始终用 sizeof 和 uint64_t 来避免这种歧义。
一句话总结:搞内存调试,先搞清楚你用的是哪种内存、地址怎么映射、结构体对齐对不对、单位有没有弄混。这四个基础概念就像工具箱里的四把螺丝刀——用得熟,后面所有章节都顺手。