malloc申请内存过程
用户调用 malloc(size)
|
v
glibc 检查内部内存池
|-- 有足够空闲内存 --> 直接分配并返回地址
|
|-- 内存不足 --> 触发系统调用
|-- 小内存 --> brk() 扩展堆
|-- 大内存 --> mmap() 创建匿名映射
|
v
内核更新虚拟内存结构
|
v
(可能延迟)物理内存分配(通过缺页异常)
|
v
glibc 管理新内存
|
v
malloc 返回地址
1. 用户调用 malloc
当用户调用 malloc(size)
时,实际调用的是 C 标准库(如 glibc)提供的函数。此时,glibc 的内存分配器(如 ptmalloc2) 会尝试从用户空间的堆内存池中分配内存,而不是直接与内核交互。这是为了减少系统调用的开销。
内存池(Heap):glibc 会预先通过系统调用(如
brk
或mmap
)向内核申请一块较大的内存区域(称为“堆”),然后自行管理这块内存的分配和释放。快速分配:如果请求的内存大小在内存池的空闲块中可以满足,glibc 会直接返回一个空闲块的地址,无需触发系统调用。
2. 内存不足时触发系统调用
如果 glibc 内存池中没有足够的空闲内存,它会通过以下两种系统调用之一向内核申请更多内存:
(1) brk
系统调用
用途:调整堆的结束地址(
program break
),扩展堆的大小。适用场景:小内存分配(通常小于
MMAP_THRESHOLD
,默认 128KB)。过程:
glibc 调用
brk()
或sbrk()
,请求内核将堆的结束地址上移。内核更新进程的
vm_area_struct
(描述堆的虚拟内存区域结构)。新的虚拟地址空间被标记为可用,但物理内存可能尚未分配(按需分配)。
(2) mmap
系统调用
用途:创建匿名内存映射(Anonymous Mapping),独立于堆。
适用场景:大内存分配(通常大于
MMAP_THRESHOLD
)。过程:
glibc 调用
mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
。内核在进程的虚拟地址空间中创建一个新的内存映射区域。
物理内存的分配可能延迟到实际访问时(通过缺页异常处理)。
3. 内核处理系统调用
当系统调用(brk
或 mmap
)被触发后,控制权从用户空间切换到内核空间,内核执行以下操作:
(1) 虚拟内存管理
内核更新进程的虚拟内存描述符(
mm_struct
),调整堆或新增一个vm_area_struct
。虚拟地址空间被标记为可用,但物理内存可能尚未分配(Lazy Allocation)。
(2) 物理内存分配
当进程首次访问新分配的虚拟内存时,会触发 缺页异常(Page Fault)。
内核的缺页处理程序(如
handle_mm_fault
)为虚拟地址分配物理页框,并建立页表映射。
4. 返回用户空间
系统调用返回后,glibc 获得新的内存区域,并将其纳入内存池管理。
malloc
返回一个指向分配内存的指针,用户程序可以正常使用。
关键点
glibc 的内存池机制:减少频繁系统调用的开销。
brk vs mmap:小内存用堆扩展(
brk
),大内存用匿名映射(mmap
)。按需分配(Lazy Allocation):内核可能延迟物理内存的分配,直到实际访问。
缺页异常:用户首次访问内存时触发,内核分配物理页框。
内核常见的内存分配函数及其使用场景
1. kmalloc()
- 用途:分配 小块内存(通常小于
PAGE_SIZE
,即 4KB 左右)。 - 语法:
void *kmalloc(size_t size, gfp_t flags);
- 参数:
size
:要分配的内存大小(字节)。flags
:内存分配标志位(如GFP_KERNEL
、GFP_ATOMIC
等)。
- 特点:
- 基于 Slab 分配器,效率高,适合小块内存分配。
- 返回的内存地址是物理上连续的。
- 示例:
void *ptr = kmalloc(1024, GFP_KERNEL); if (!ptr) { // 分配失败处理 }
2. kzalloc()
- 用途:与
kmalloc
类似,但分配后会 自动清零内存。 - 语法:
void *kzalloc(size_t size, gfp_t flags);
- 示例:
void *ptr = kzalloc(1024, GFP_KERNEL); // 内存初始化为0
3. vmalloc()
- 用途:分配 大块或非连续的内存(通常大于
PAGE_SIZE
)。 - 语法:
void *vmalloc(unsigned long size);
- 特点:
- 分配的内存是 虚拟地址连续,但物理地址可能不连续。
- 内部通过
get_free_pages()
分配物理页,然后映射到虚拟地址空间。
- 适用场景:需要分配大块内存(如动态加载模块、大缓冲区)。
- 示例:
void *ptr = vmalloc(1024 * 1024); // 分配1MB内存
4. alloc_pages()
- 用途:按 页(Page) 单位分配内存。
- 语法:
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
- 参数:
gfp_mask
:分配标志位。order
:分配的页数为2^order
(例如order=0
表示1页,order=1
表示2页,依此类推)。
- 特点:
- 返回的是
struct page
指针,需通过page_address()
获取实际的虚拟地址。 - 适合需要直接操作物理页的场景(如设备驱动)。
- 返回的是
- 示例:
struct page *page = alloc_pages(GFP_KERNEL, 0); // 分配1页内存 void *ptr = page_address(page);
5. get_zeroed_page()
- 用途:分配 单页内存 并清零。
- 语法:
unsigned long get_zeroed_page(gfp_t gfp_mask);
- 返回值:虚拟地址(失败返回
0
)。 - 示例:
void *ptr = (void *)get_zeroed_page(GFP_KERNEL); if (!ptr) { // 分配失败处理 }
6. dma_alloc_coherent()
- 用途:为 DMA操作 分配物理地址连续的内存。
- 语法:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);
- 参数:
dev
:设备结构体指针。dma_handle
:返回物理地址(供设备使用)。
- 特点:
- 内存是物理连续的,适合硬件设备直接访问。
- 示例:
dma_addr_t dma_addr; void *ptr = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL);
7. kmem_cache_alloc()
- 用途:从 Slab 缓存 中分配内存(适合频繁分配/释放相同大小的对象)。
- 语法:
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags);
- 示例:
struct kmem_cache *my_cache = KMEM_CACHE(my_obj, 0); my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
关键标志位(gfp_t
)
内存分配的标志位决定了分配行为,常见标志包括:
- **
GFP_KERNEL
**:- 普通内核上下文,允许休眠(不可用于中断或软中断)。
- **
GFP_ATOMIC
**:- 中断或硬中断上下文,不允许休眠(分配失败概率较高)。
- **
GFP_NOIO
**:- 不允许发起 I/O 操作(如从磁盘读取空闲页)。
- **
GFP_NOFS
**:- 不允许发起文件系统操作。
注意事项
检查分配结果:
所有分配函数都可能失败(返回NULL
或0
),必须检查返回值。if (!ptr) { // 处理内存不足的情况 }
释放内存:
使用对应的释放函数:kfree()
:释放kmalloc
/kzalloc
分配的内存。vfree()
:释放vmalloc
分配的内存。free_pages()
:释放alloc_pages
分配的页。dma_free_coherent()
:释放dma_alloc_coherent
分配的内存。
物理地址连续性:
kmalloc
和dma_alloc_coherent
分配的内存是物理连续的。vmalloc
分配的内存是虚拟地址连续,物理地址可能不连续。
总结
函数 | 适用场景 | 特点 |
---|---|---|
kmalloc /kzalloc |
小块内存(<4KB) | 物理连续,基于 Slab 分配器 |
vmalloc |
大块或非连续内存 | 虚拟地址连续,物理地址可能不连续 |
alloc_pages |
按页分配 | 直接操作物理页 |
dma_alloc_coherent |
DMA 操作(需物理连续) | 物理连续,适合硬件设备 |
根据具体需求选择合适的函数,并注意内存释放和错误处理!