【linux】malloc函数申请过程理解

发布于:2025-04-07 ⋅ 阅读:(25) ⋅ 点赞:(0)

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)。

  • 过程

    1. glibc 调用 brk() 或 sbrk(),请求内核将堆的结束地址上移。

    2. 内核更新进程的 vm_area_struct(描述堆的虚拟内存区域结构)。

    3. 新的虚拟地址空间被标记为可用,但物理内存可能尚未分配(按需分配)。

(2) mmap 系统调用
  • 用途:创建匿名内存映射(Anonymous Mapping),独立于堆。

  • 适用场景:大内存分配(通常大于 MMAP_THRESHOLD)。

  • 过程

    1. glibc 调用 mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

    2. 内核在进程的虚拟地址空间中创建一个新的内存映射区域。

    3. 物理内存的分配可能延迟到实际访问时(通过缺页异常处理)。


3. 内核处理系统调用

当系统调用(brk 或 mmap)被触发后,控制权从用户空间切换到内核空间,内核执行以下操作:

(1) 虚拟内存管理
  • 内核更新进程的虚拟内存描述符(mm_struct),调整堆或新增一个 vm_area_struct

  • 虚拟地址空间被标记为可用,但物理内存可能尚未分配(Lazy Allocation)

(2) 物理内存分配
  • 当进程首次访问新分配虚拟内存时,会触发 缺页异常(Page Fault)

  • 内核的缺页处理程序(如 handle_mm_fault)为虚拟地址分配物理页框,并建立页表映射


4. 返回用户空间

  • 系统调用返回后,glibc 获得新的内存区域,并将其纳入内存池管理。

  • malloc 返回一个指向分配内存的指针,用户程序可以正常使用。

关键点

  1. glibc 的内存池机制:减少频繁系统调用的开销。

  2. brk vs mmap:小内存用堆扩展(brk),大内存用匿名映射(mmap)。

  3. 按需分配(Lazy Allocation):内核可能延迟物理内存的分配,直到实际访问。

  4. 缺页异常:用户首次访问内存时触发,内核分配物理页框。

内核常见的内存分配函数及其使用场景


1. kmalloc()

  • 用途:分配 小块内存(通常小于 PAGE_SIZE,即 4KB 左右)。
  • 语法
    void *kmalloc(size_t size, gfp_t flags);
    
  • 参数
    • size:要分配的内存大小(字节)。
    • flags:内存分配标志位(如 GFP_KERNELGFP_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**:
    • 不允许发起文件系统操作。

注意事项

  1. 检查分配结果
    所有分配函数都可能失败(返回 NULL 或 0),必须检查返回值。

    if (!ptr) {
        // 处理内存不足的情况
    }
    
  2. 释放内存
    使用对应的释放函数:

    • kfree():释放 kmalloc/kzalloc 分配的内存。
    • vfree():释放 vmalloc 分配的内存。
    • free_pages():释放 alloc_pages 分配的页。
    • dma_free_coherent():释放 dma_alloc_coherent 分配的内存。
  3. 物理地址连续性

    • kmalloc 和 dma_alloc_coherent 分配的内存是物理连续的。
    • vmalloc 分配的内存是虚拟地址连续,物理地址可能不连续。

总结

函数 适用场景 特点
kmalloc/kzalloc 小块内存(<4KB) 物理连续,基于 Slab 分配器
vmalloc 大块或非连续内存 虚拟地址连续,物理地址可能不连续
alloc_pages 按页分配 直接操作物理页
dma_alloc_coherent DMA 操作(需物理连续) 物理连续,适合硬件设备

根据具体需求选择合适的函数,并注意内存释放和错误处理!


网站公告

今日签到

点亮在社区的每一天
去签到