🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习
🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发
❄️作者主页:一个平凡而乐于分享的小比特的个人主页
✨收录专栏:Linux,本专栏目的在于,记录学习Linux操作系统的总结
欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖

malloc/calloc/kmalloc/vmalloc 详解
一、用户空间内存分配
malloc:动态内存分配的基石
c
void *malloc(size_t size);
内部实现机制:
用户进程地址空间
┌─────────────────────┐
│ 栈(stack) │ ← 向下生长
├─────────────────────┤
│ ↓ │
│ 空 │
│ 间 │
├─────────────────────┤
│ 内存映射区(mmapped)│ ← 大内存分配(>128KB)
│ │ 直接映射到文件/匿名页
├─────────────────────┤
│ ↑ │
│ 堆(heap) │ ← 小内存分配(≤128KB)
│ │ 通过brk调整堆顶
├─────────────────────┤
│ 未初始化数据(BSS) │
├─────────────────────┤
│ 已初始化数据 │
├─────────────────────┤
│ 代码段(text) │
└─────────────────────┘
两种分配策略对比:
| 特性 | brk分配(小内存≤128KB) | mmap分配(大内存>128KB) |
|---|---|---|
| 分配位置 | 堆区域 | 内存映射区(堆栈之间) |
| 系统调用 | brk()/sbrk() |
mmap() |
| 内存连续性 | 连续扩展堆 | 独立映射段 |
| 释放行为 | 放回malloc内存池 | 直接归还OS |
| 内存碎片 | 容易产生碎片 | 较少碎片 |
| 性能开销 | 较小 | 较大(用户/内核切换) |
内存管理链表结构:
内存块链表
┌─────┬─────┬─────────────┐
│元数据│used│ 用户数据区 │ ← 分配的内存块
├─────┼─────┼─────────────┤
│size │ 1 │ data... │
└─────┴─────┴─────────────┘
▲
│
┌─────┬─────┬─────────────┐
│元数据│used│ 用户数据区 │
├─────┼─────┼─────────────┤
│size │ 0 │ (空闲) │ ← 空闲块
└─────┴─────┴─────────────┘
malloc函数家族
c
// 1. malloc - 基本分配
void *ptr = malloc(100); // 分配100字节,内容未初始化
// 2. calloc - 分配并清零
void *ptr = calloc(10, sizeof(int)); // 分配10个int,全部初始化为0
// 相当于: malloc(10 * sizeof(int)) + memset(ptr, 0, 10 * sizeof(int))
// 3. realloc - 调整大小
void *new_ptr = realloc(ptr, 200); // 调整到200字节
// 可能原地扩展、移动数据、或分配新区域
// 4. alloca - 栈分配(不常用)
void *ptr = alloca(100); // 在栈上分配,函数返回自动释放
内存分配流程示例:
c
// 场景:分配小内存
int *arr1 = malloc(1024); // 1KB → 使用brk分配
// 场景:分配大内存
int *arr2 = malloc(1024*256); // 256KB → 使用mmap分配
// 初始化数组
int *zeros = calloc(100, sizeof(int));
// zeros[0]~zeros[99] 都是0
// 扩展内存
zeros = realloc(zeros, 200 * sizeof(int));
// 前100个元素保留,后100个未初始化
二、内核空间内存分配
内核地址空间布局
内核虚拟地址空间
┌─────────────────────┐
│ vmalloc区域 │ ← vmalloc分配
│ │ 虚拟连续,物理不一定连续
├─────────────────────┤
│ 持久内核映射区 │
├─────────────────────┤
│ 固定映射区 │
├─────────────────────┤
│ kmalloc区域 │ ← kmalloc分配
│ │ 虚拟和物理都连续
├─────────────────────┤
│ 低端内存直接映射区 │
│ (3:1或2:2划分) │
└─────────────────────┘
kmalloc vs vmalloc 详细对比
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 内存类型 | 物理连续内存 | 虚拟连续,物理可分散 |
| 分配大小 | 有限制(通常≤4MB) | 可分配较大内存 |
| 性能 | 快(直接映射) | 慢(建立页表) |
| 适用场景 | DMA、硬件交互 | 大块内存、模块加载 |
| CPU缓存 | 可能利用缓存 | 可能刷新TLB |
| 内存来源 | 内核slab分配器 | 获取独立页并映射 |
| 对齐保证 | 按大小对齐 | 页对齐 |
| 睡眠可能 | 可指定GFP_KERNEL允许睡眠 | 可能引起睡眠 |
kmalloc 深入解析
c
#include <linux/slab.h>
// GFP标志示例
void *ptr;
// 常用分配方式
ptr = kmalloc(1024, GFP_KERNEL); // 可能睡眠
ptr = kmalloc(1024, GFP_ATOMIC); // 原子分配,不睡眠
ptr = kzalloc(1024, GFP_KERNEL); // 分配并清零(类似calloc)
// 硬件相关分配
ptr = kmalloc(1024, GFP_DMA); // DMA可用内存
ptr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// 释放内存
kfree(ptr);
kmalloc内存来源 - Slab分配器:
物理内存页
┌─────────────────┐
│ Slab缓存 │ ← 专用缓存(如task_struct)
├─────────────────┤
│ 通用缓存(2^n) │ ← kmalloc的8,16,32,...,4096字节缓存
│ 8B│16B│32B│...│
└─────────────────┘
│
▼
用户请求128字节 → 从128B缓存slab分配
vmalloc 深入解析
c
#include <linux/vmalloc.h>
// vmalloc使用示例
void *ptr;
ptr = vmalloc(1024 * 1024); // 分配1MB
ptr = vzalloc(1024 * 1024); // 分配并清零
// 使用后释放
vfree(ptr);
vmalloc分配过程:
1. 在vmalloc区域找虚拟地址空间
┌─────────┐
│ 虚拟地址 │← 找到连续虚拟空间
└─────────┘
2. 分配物理页框(可能不连续)
┌───┐ ┌───┐ ┌───┐
│ 4K │ │ 4K │ │ 4K │ ← 分散的物理页
└───┘ └───┘ └───┘
3. 建立页表映射
虚拟地址 → 页表 → 物理地址
三、实战场景分析
场景1:用户空间程序开发
c
// 推荐实践
#define SMALL_SIZE (1024) // 1KB - brk分配
#define LARGE_SIZE (200 * 1024) // 200KB - mmap分配
void process_data() {
// 小内存:用calloc自动初始化
int *buffer = calloc(SMALL_SIZE, sizeof(int));
// 大内存:用malloc + 手动初始化
char *large_buf = malloc(LARGE_SIZE);
if (large_buf) {
memset(large_buf, 0, LARGE_SIZE);
}
// 调整内存
buffer = realloc(buffer, SMALL_SIZE * 2);
free(buffer);
free(large_buf);
}
场景2:内核驱动开发
c
// 设备驱动中的内存使用
struct my_device {
dma_addr_t dma_handle;
void *cpu_addr;
void *large_buffer;
};
static int my_probe(struct device *dev) {
struct my_device *md = kmalloc(sizeof(*md), GFP_KERNEL);
// DMA内存:必须物理连续
md->cpu_addr = dma_alloc_coherent(dev,
DMA_SIZE,
&md->dma_handle,
GFP_KERNEL);
// 大缓冲区:不需要物理连续
md->large_buffer = vmalloc(LARGE_BUFFER_SIZE);
return 0;
}
static void my_remove(struct device *dev) {
// 按分配方式反向释放
vfree(md->large_buffer);
dma_free_coherent(dev, DMA_SIZE,
md->cpu_addr, md->dma_handle);
kfree(md);
}
场景3:性能敏感场景
c
// 网络数据包处理(不能睡眠)
void process_packet(struct sk_buff *skb) {
// 原子分配,不会导致进程睡眠
void *temp = kmalloc(TEMP_SIZE, GFP_ATOMIC);
// 快速处理...
kfree(temp);
}
// 模块初始化(可以睡眠)
static int __init my_module_init(void) {
// 常规分配,允许页面回收
void *data = kmalloc(DATA_SIZE, GFP_KERNEL);
// 大内存分配
void *lookup_table = vmalloc(LOOKUP_TABLE_SIZE);
// 初始化...
return 0;
}
四、内存分配流程图
用户空间分配(malloc)
│
├── size ≤ 128KB?
│ ├── 是 → 使用brk()扩展堆
│ │ 从空闲链表查找合适块
│ │ 或分裂大块内存
│ │
│ └── 否 → 使用mmap()创建映射
│ 在堆栈间找空闲区域
│ 建立匿名内存映射
│
├── 更新内存管理链表
├── 返回虚拟地址
└── 首次访问时触发缺页中断
↓
物理内存实际分配
内核空间分配
│
├── 需要物理连续?
│ ├── 是 → kmalloc()
│ │ 从slab缓存分配
│ │ 检查GFP标志
│ │ 返回直接映射地址
│ │
│ └── 否 → vmalloc()
│ 找虚拟地址空间
│ 分配分散物理页
│ 建立页表映射
│
└── 返回内核虚拟地址
五、关键要点总结
-
malloc的智能策略:根据大小自动选择brk或mmap,平衡性能和碎片
-
kmalloc的局限性:物理连续性要求限制了最大分配大小,但性能优异
-
vmalloc的灵活性:可分配大内存但性能较低,适合不要求物理连续的场景
-
选择原则:
- 用户空间:让malloc自动选择策略
- 内核小内存:kmalloc + 合适GFP标志
- 内核大内存/不要求连续:vmalloc
- DMA操作:kmalloc with GFP_DMA 或 dma_alloc_coherent
-
内存管理本质:所有分配最初都只是虚拟地址,访问时才通过缺页中断获取物理页
理解这些分配器的差异和适用场景,能够帮助开发者在不同环境下做出最优的内存管理决策。