# 嵌入式每日一题:【动态内存】
# malloc、brk/sbrk/mmap、库函数和系统调用
## 一、malloc的作用
在 C 语言中,`malloc()` 用来在**堆区**动态申请内存。
例如:
```c
int *p = malloc(sizeof(int) * 100);
```
这表示申请一块可以存放 100 个 `int` 的内存。
但是要注意:
> `malloc()` 本身不是系统调用,而是 C 标准库函数。
> 它内部会根据需要,通过 `brk/sbrk` 或 `mmap` 向 Linux 内核申请更大的内存区域。
也就是说:
```text
应用程序
↓
malloc/free 库函数
↓
brk/sbrk/mmap/munmap 系统调用或系统接口
↓
Linux 内核
↓
物理内存 / 虚拟内存管理
```
---
# 二、brk 和 sbrk 是什么?
## 1. 先理解 program break
Linux 进程的虚拟地址空间中有一块区域叫**堆区**。
堆区的末尾位置叫:
```text
program break
```
可以理解为:
```text
堆区的边界 / 堆的结束地址
```
当程序需要更多堆内存时,可以把这个边界往高地址移动。
示意图:
```text
低地址
┌──────────────┐
│ 代码段 text │
├──────────────┤
│ 数据段 data │
├──────────────┤
│ BSS 段 │
├──────────────┤
│ 堆 heap │ ← malloc 常用区域
│ │
│ │
└──────────────┘ ← program break
↑
brk/sbrk 调整这个位置
```
---
## 2. brk()
`brk()` 的作用是:**把堆的结束位置设置到某个指定地址**。
函数原型通常是:
```c
#include
int brk(void *addr);
```
含义:
```text
brk(addr)
```
表示尝试把 program break 设置到 `addr` 这个地址。
如果成功,堆区边界就移动到这个位置。
但是实际程序中很少直接使用 `brk()`,因为你需要自己计算地址,容易出错。
---
## 3. sbrk()
`sbrk()` 的作用是:**在当前堆边界基础上增加或减少一段空间**。
函数原型:
```c
#include
void *sbrk(intptr_t increment);
```
例如:
```c
void *old = sbrk(0);
```
表示查看当前堆的结束地址。
```c
void *p = sbrk(4096);
```
表示把堆扩大 4096 字节,并返回扩大前的堆尾地址。
也就是说,这 4096 字节大概就是你新获得的空间。
---
## 4. sbrk 示例
```c
#include
#include
int main()
{
void *p1 = sbrk(0);
printf("当前堆顶: %p\n", p1);
void *p2 = sbrk(4096);
printf("申请前堆顶: %p\n", p2);
void *p3 = sbrk(0);
printf("申请后堆顶: %p\n", p3);
return 0;
}
```
可能输出:
```text
当前堆顶: 0x55a0c0a2c000
申请前堆顶: 0x55a0c0a2c000
申请后堆顶: 0x55a0c0a2d000
```
可以看到,堆顶地址增加了 `0x1000`,也就是 4096 字节。
---
## 5. brk/sbrk 的特点
`brk/sbrk` 的特点是:
```text
1. 主要用于扩展传统堆区
2. 分配出来的内存通常是连续增长的
3. 适合中小块内存管理
4. 只能调整堆顶,不适合灵活释放中间某一块内存
```
例如:
```text
堆区:
[A][B][C][D]
```
如果你想释放中间的 B,`brk/sbrk` 本身并不能直接把 B 还给内核。
所以 `malloc/free` 需要在用户态维护复杂的内存管理结构。
---
# 三、mmap 是什么?
`mmap()` 是 Linux 中非常重要的系统调用,它可以把一段内存映射到进程的虚拟地址空间。
它可以用于:
```text
1. 文件映射
2. 匿名内存映射
3. 共享内存
4. 大块动态内存分配
```
在 `malloc()` 中,`mmap()` 常用于申请比较大的内存块。
---
## 1. 匿名 mmap
如果只是想申请一块内存,不对应具体文件,可以使用匿名映射。
示例:
```c
#include
#include
#include
int main()
{
size_t size = 4096;
void *p = mmap(
NULL,
size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0
);
if (p == MAP_FAILED) {
perror("mmap");
return -1;
}
printf("mmap addr: %p\n", p);
int *arr = (int *)p;
arr[0] = 123;
arr[1] = 456;
printf("%d %d\n", arr[0], arr[1]);
munmap(p, size);
return 0;
}
```
编译运行:
```bash
gcc mmap_test.c -o mmap_test
./mmap_test
```
---
## 2. mmap 参数解释
```c
void *mmap(
void *addr,
size_t length,
int prot,
int flags,
int fd,
off_t offset
);
```
常用参数含义:
| 参数 | 含义 |
| -------- | ---------------------------- |
| `addr` | 希望映射到哪个虚拟地址,通常填 `NULL`,让内核决定 |
| `length` | 映射大小 |
| `prot` | 权限,例如可读、可写、可执行 |
| `flags` | 映射方式 |
| `fd` | 文件描述符,匿名映射时填 `-1` |
| `offset` | 文件偏移,匿名映射时填 `0` |
常用权限:
```c
PROT_READ // 可读
PROT_WRITE // 可写
PROT_EXEC // 可执行
```
常用 flags:
```c
MAP_PRIVATE // 私有映射
MAP_SHARED // 共享映射
MAP_ANONYMOUS // 匿名映射,不关联文件
```
---
# 四、malloc 和 brk/sbrk/mmap 的关系
可以这样理解:
## 1. malloc 小块内存时
例如:
```c
malloc(100);
malloc(200);
malloc(300);
```
`malloc` 通常不会每次都调用系统调用。
因为系统调用开销比较大。
它可能会先通过 `brk/sbrk` 向内核申请一大块堆空间,然后在用户态自己切分:
```text
malloc 向内核申请一大块堆空间
↓
[ 大块堆空间 ]
↓
malloc 自己切成小块
↓
[100字节][200字节][300字节][空闲块...]
```
这样效率更高。
---
## 2. malloc 大块内存时
例如:
```c
malloc(10 * 1024 * 1024);
```
对于比较大的内存申请,`malloc` 可能直接使用 `mmap()`。
因为 `mmap()` 申请的大块内存可以比较方便地通过 `munmap()` 归还给内核。
示意图:
```text
小块 malloc:
malloc → brk/sbrk → 扩展堆区 → malloc 内部切分管理
大块 malloc:
malloc → mmap → 单独映射一块虚拟内存
free → munmap → 直接归还内核
```
不过具体多大算“大块”,取决于 C 库实现和运行环境,不应该死记固定值。
---
# 五、free 后内存一定还给系统吗?
不一定。
例如:
```c
p = malloc(100);
free(p);
```
`free()` 只是告诉 `malloc`:
```text
这块内存我不用了
```
但这块内存可能仍然被 `malloc` 保留在用户态内存池里,供下一次 `malloc` 复用。
所以:
```text
free 后,内存不一定马上还给操作系统
```
对于小块内存,`free()` 后通常只是回到 malloc 的空闲链表中。
对于大块 `mmap()` 分配的内存,`free()` 可能会调用 `munmap()` 直接还给内核。
---
# 六、brk/sbrk 和 mmap 的区别
| 对比项 | brk/sbrk | mmap |
| ---------- | ---------- | ------------------ |
| 作用 | 调整堆区边界 | 创建一段新的虚拟内存映射 |
| 内存位置 | 传统堆区,连续增长 | 可映射到虚拟地址空间任意合适位置 |
| 适合场景 | 中小块动态内存 | 大块内存、文件映射、共享内存 |
| 释放方式 | 通常只能从堆顶收缩 | 可以 `munmap()` 单独释放 |
| 灵活性 | 较低 | 较高 |
| malloc 中用途 | 常用于管理普通堆内存 | 常用于大块分配 |
简单记:
```text
brk/sbrk:把原来的堆往后扩
mmap:重新映射一块新的虚拟内存区域
```
---
# 七、为什么不直接用 brk/sbrk/mmap,而用 malloc?
因为 `malloc()` 帮我们做了很多管理工作。
如果你直接用 `brk/sbrk/mmap`,你需要自己处理:
```text
1. 内存块切分
2. 内存块合并
3. 空闲链表管理
4. 内存对齐
5. 多线程安全
6. 内存碎片
7. 大小块分配策略
```
而 `malloc/free` 已经把这些封装好了。
例如:
```c
int *p = malloc(sizeof(int) * 100);
free(p);
```
这比你自己用 `sbrk()` 管理安全得多。
所以一般应用程序中:
```text
普通动态内存:用 malloc/free
大块特殊映射:用 mmap/munmap
共享内存/文件映射:用 mmap
不要随便直接用 brk/sbrk
```
---
# 八、库函数和系统调用的区别
## 1. 系统调用是什么?
系统调用是应用程序请求 Linux 内核服务的接口。
例如:
```text
read()
write()
open()
close()
fork()
exec()
mmap()
brk()
```
这些操作需要内核参与,因为它们涉及:
```text
文件
进程
内存
设备
网络
权限
调度
```
系统调用会发生从用户态到内核态的切换:
```text
用户态程序
↓ 系统调用
内核态
↓ 完成操作
返回用户态
```
---
## 2. 库函数是什么?
库函数是 C 标准库或其他库提供的函数,通常运行在用户态。
例如:
```text
printf()
scanf()
malloc()
free()
fopen()
fread()
fwrite()
strlen()
strcpy()
```
库函数可能只是普通函数,也可能内部调用系统调用。
例如:
```text
printf() 最终可能调用 write()
malloc() 内部可能调用 brk/mmap
fopen() 内部可能调用 open()
fread() 内部可能调用 read()
```
---
## 3. 关系示例
### printf 和 write
你写:
```c
printf("hello\n");
```
`printf()` 是库函数。
它会先在用户态做格式化和缓冲,合适的时候再调用 `write()` 系统调用输出到终端。
关系大概是:
```text
printf()
↓
C 库缓冲区
↓
write()
↓
内核
↓
终端设备
```
---
### malloc 和 mmap/brk
你写:
```c
void *p = malloc(1024);
```
`malloc()` 是库函数。
它可能先从已有内存池里找一块给你。
如果内存池不够,才会调用:
```text
brk/sbrk 或 mmap
```
关系大概是:
```text
malloc()
↓
检查 malloc 内部空闲链表
↓
不够时调用 brk/mmap
↓
内核分配虚拟内存区域
```
---
# 九、库函数和系统调用对比
| 对比项 | 库函数 | 系统调用 |
| ------ | --------------------------- | ------------------------------ |
| 所在位置 | 用户态库 | 内核提供接口 |
| 是否进入内核 | 不一定 | 一定进入内核 |
| 开销 | 通常较小 | 较大,有用户态/内核态切换 |
| 可移植性 | 较好,如 C 标准库 | 与操作系统相关 |
| 功能层次 | 更高级、更好用 | 更底层 |
| 例子 | `printf`, `malloc`, `fopen` | `write`, `mmap`, `brk`, `open` |
一句话:
```text
库函数是对功能的封装,系统调用是访问内核服务的入口。
```
---
# 十、嵌入式 Linux 中怎么理解?
在嵌入式 Linux 中,内存资源通常比较紧张,所以理解 `malloc` 很重要。
例如图像处理程序中:
```c
unsigned char *frame = malloc(width * height * 3);
```
这时一帧图像可能很大。
如果频繁:
```c
malloc();
free();
malloc();
free();
```
可能导致:
```text
内存碎片
性能下降
分配失败
实时性变差
```
所以嵌入式中常见做法是:
```text
1. 程序初始化时一次性申请大块缓冲区
2. 运行过程中重复使用
3. 避免在实时循环中频繁 malloc/free
4. 大块连续缓冲可以考虑 mmap 或驱动分配
5. 多线程中注意 malloc 的锁竞争
```
例如:
```text
摄像头采集线程:
使用预分配 frame buffer
图像处理线程:
从队列中取 buffer 处理
发送线程:
处理完成后释放回 buffer 池
```
而不是每帧都:
```c
frame = malloc(size);
process(frame);
free(frame);
```
---
# 十一、总结概括
`malloc()` 是 C 标准库函数,不是系统调用。它用于在堆区动态申请内存。`malloc` 内部会维护内存池、空闲链表、内存对齐和碎片管理,当现有堆空间不够时,才会通过 `brk/sbrk` 或 `mmap` 向内核申请更多内存。
`brk/sbrk` 用于调整进程堆区的边界,也就是 program break。`brk` 是把堆顶设置到指定地址,`sbrk` 是在当前堆顶基础上增加或减少一段空间,常用于传统堆区扩展。`mmap` 用于在进程虚拟地址空间中创建新的内存映射,可以用于大块匿名内存、文件映射和共享内存。一般来说,小块动态内存可能由 `brk/sbrk` 扩展堆区后由 `malloc` 管理,大块内存可能由 `mmap` 单独映射,并在释放时通过 `munmap` 归还内核。
库函数运行在用户态,是对常用功能的封装,例如 `printf`、`malloc`、`fopen`;系统调用是用户程序请求内核服务的接口,例如 `write`、`mmap`、`brk`、`fork`。库函数不一定进入内核,但系统调用一定会进入内核,因此系统调用开销更大。简单来说,库函数更高级、更易用,系统调用更底层,是访问内核资源的入口。
发布于 日本
