Linux内核学习轨迹第五部:内核内存分配接口全场景使用与选型(第五小节)

Linux内核学习轨迹第五部:内核内存分配接口全场景使用与选型(第五小节) 内核内存分配接口全场景使用与选型内核提供了多种内存分配接口分别适用于不同的场景、上下文、内存大小需求很多内核驱动的bug、系统崩溃都是因为选择了错误的分配接口。本章节我们完整拆解内核中常用的内存分配接口对比它们的区别、适用场景、选型规则以及避坑指南。5.1 内核内存分配接口全景图Linux内核的内存分配接口按底层实现可以分为三大类基于伙伴系统的页分配接口以页为单位分配连续的物理内存适用于大内存分配是所有内存分配的底层基础基于SLUB分配器的小对象分配接口基于伙伴系统的页拆分小对象适用于小内存分配是内核最常用的通用分配接口非连续内存分配接口虚拟地址连续物理地址不连续适用于物理内存碎片化时的大内存分配不需要连续的物理页。接口底层实现分配单位物理地址连续性最大分配大小允许睡眠适用场景alloc_pages(gfp_mask, order)伙伴系统页4KB连续MAX_ORDER-1个页默认4MB由gfp_mask决定大内存分配、连续物理内存需求__get_free_pages(gfp_mask, order)伙伴系统页连续MAX_ORDER-1个页由gfp_mask决定大内存分配需要直接映射的虚拟地址get_zeroed_page(gfp_mask)伙伴系统单页连续1页由gfp_mask决定需要初始化为0的单页分配kmalloc(size, gfp_mask)SLUB分配器字节连续物理地址和虚拟地址都连续4MBMAX_ORDER个页由gfp_mask决定内核通用小内存分配最常用kzalloc(size, gfp_mask)SLUB分配器字节连续4MB由gfp_mask决定需要初始化为0的小内存分配kcalloc(n, size, gfp_mask)SLUB分配器字节连续4MB由gfp_mask决定数组分配初始化为0防止整数溢出krealloc(ptr, size, gfp_mask)SLUB分配器字节连续4MB由gfp_mask决定重新分配内存调整大小vmalloc(size)伙伴系统页表映射字节虚拟地址连续物理地址不连续理论上可达VMALLOC区大小几十GB允许物理内存碎片化时的大内存分配不需要连续物理地址vzalloc(size)伙伴系统页表映射字节虚拟地址连续物理地址不连续几十GB允许需要初始化为0的非连续大内存分配dma_alloc_coherent(dev, size, dma_handle, gfp_mask)伙伴系统字节连续DMA安全取决于Zone大小由gfp_mask决定设备驱动的DMA一致性内存分配5.2 基于伙伴系统的页分配接口这类接口直接从伙伴系统分配连续的物理页是所有内存分配的底层基础适用于大内存分配、需要连续物理内存的场景比如设备驱动的DMA、大页分配、内核栈分配等。5.2.1 核心接口详解1.alloc_pages(gfp_mask, order)原型struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)核心作用分配2^order个连续的物理页返回对应的struct page结构体指针参数gfp_mask分配标志位决定了分配的行为、允许的睡眠、分配的Zone等比如 GFP_KERNEL、GFP_ATOMICorder分配的阶数2^order个页最大为MAX_ORDER-1默认10对应1024页4MB返回值成功返回page结构体指针失败返回NULL适用场景需要操作struct page结构体的场景比如页缓存、大页、slab分配器的底层页分配。2.__get_free_pages(gfp_mask, order)原型unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)核心作用和alloc_pages类似分配2^order个连续的物理页返回页的内核虚拟地址直接映射区区别alloc_pages返回page结构体__get_free_pages返回虚拟地址不需要手动转换限制只能从ZONE_NORMAL分配返回的是内核直接映射的虚拟地址不能分配高端内存适用场景需要直接访问内存的大内存分配不需要操作page结构体。3.get_zeroed_page(gfp_mask)原型unsigned long get_zeroed_page(gfp_t gfp_mask)核心作用分配1个物理页把页的内容初始化为0返回虚拟地址等价于__get_free_pages(gfp_mask | __GFP_ZERO, 0)适用场景需要初始化为0的单页分配避免内存中的脏数据泄露。5.2.2 页释放接口__free_pages(struct page *page, unsigned int order)释放分配的连续页对应alloc_pagesfree_pages(unsigned long addr, unsigned int order)释放分配的连续页对应__get_free_pages注意释放时必须使用分配时的order否则会导致内存 corruption、内核崩溃。5.3 基于SLUB分配器的小对象分配接口这类接口基于SLUB分配器实现是内核中最常用的通用内存分配接口适用于小内存分配类似于用户态的malloc/free性能高内存利用率好。5.3.1 核心接口详解1.kmalloc(size, gfp_mask)原型void *kmalloc(size_t size, gfp_t gfp_mask)核心作用分配size字节的内存返回虚拟地址物理地址和虚拟地址都是连续的分配逻辑找到大于等于size的最小的kmalloc缓存从SLUB缓存中分配对象最大分配大小默认是4MBMAX_ORDER个页超过这个大小会直接调用alloc_pages分配初始化分配的内存不会被初始化内容是随机的必须手动初始化后再使用适用场景内核中绝大多数的小内存分配是最通用的内核内存分配接口。2.kzalloc(size, gfp_mask)原型void *kzalloc(size_t size, gfp_t gfp_mask)核心作用和kmalloc完全一致只是分配的内存会被初始化为0等价于kmalloc(size, gfp_mask | __GFP_ZERO)适用场景需要初始化为0的内存分配避免手动memset同时防止脏数据泄露是内核中最常用的安全分配接口。3.kcalloc(n, size, gfp_mask)原型void *kcalloc(size_t n, size_t size, gfp_t gfp_mask)核心作用分配n个size字节的数组内存初始化为0同时会检查n*size的整数溢出优势如果n*size超过了SIZE_MAX会返回NULL避免整数溢出导致的内存越界是数组分配的安全接口适用场景数组分配尤其是动态大小的数组防止整数溢出漏洞。4.krealloc(ptr, size, gfp_mask)原型void *krealloc(const void *ptr, size_t size, gfp_t gfp_mask)核心作用重新分配内存调整已分配内存的大小类似于用户态的realloc核心规则如果ptr为NULL等价于kmalloc(size, gfp_mask)如果size为0等价于kfree(ptr)返回NULL如果新的大小大于原大小会分配新的内存复制原内容释放原内存如果新的大小小于原大小会截断原内容返回原指针或新指针适用场景动态调整内存大小比如动态数组、缓冲区扩容。5.kfree(const void *objp)原型void kfree(const void *objp)核心作用释放kmalloc/kzalloc/kcalloc/krealloc分配的内存注意kfree(NULL)是安全的不会有任何操作绝对不能释放已经释放过的内存否则会导致内存 corruption、内核崩溃不能释放非kmalloc分配的内存。5.3.2 核心避坑指南绝对不要访问kmalloc分配的超出size的内存kmalloc分配的内存是按固定大小对齐的比如分配100字节实际分配128字节但访问超出100字节的部分会导致越界访问破坏SLUB的元数据引发内核崩溃。必须初始化kmalloc分配的内存kmalloc分配的内存内容是随机的未初始化就使用会导致信息泄露、逻辑错误甚至安全漏洞优先使用kzalloc。中断上下文必须使用GFP_ATOMIC标志中断上下文、自旋锁持有期间不能使用GFP_KERNEL必须使用GFP_ATOMIC否则会导致睡眠引发内核死锁。不要用kmalloc分配大内存超过1页4KB的内存分配优先使用alloc_pages或者vmallockmalloc分配大内存会加剧物理内存碎片化分配失败概率高。5.4 非连续内存分配接口vmallocvmalloc分配的内存虚拟地址是连续的但物理地址是不连续的通过内核页表把分散的物理页映射到连续的虚拟地址空间适用于物理内存碎片化时的大内存分配不需要连续的物理地址。5.4.1 核心接口详解1.vmalloc(unsigned long size)原型void *vmalloc(unsigned long size)核心作用分配size字节的内存返回内核虚拟地址虚拟地址连续物理地址不连续分配逻辑从内核的vmalloc区分配连续的虚拟地址空间调用alloc_pages逐个分配物理页不需要连续修改内核页表把物理页映射到连续的虚拟地址空间返回虚拟地址的起始地址允许睡眠vmalloc会修改内核页表可能会阻塞睡眠不能在中断上下文、自旋锁持有期间使用最大分配大小理论上可以分配整个vmalloc区的大小x86_64架构下是几十TB远大于kmalloc的最大分配大小初始化分配的内存不会被初始化内容是随机的。2.vzalloc(unsigned long size)原型void *vzalloc(unsigned long size)核心作用和vmalloc完全一致分配的内存会被初始化为0等价于vmalloc分配后逐页memset为0因为物理页是不连续的不能一次性初始化适用场景需要初始化为0的非连续大内存分配。3.vfree(const void *addr)原型void vfree(const void *addr)核心作用释放vmalloc/vzalloc分配的内存释放对应的虚拟地址空间、物理页、页表映射注意vfree(NULL)是安全的不能用kfree释放vmalloc分配的内存也不能用vfree释放kmalloc分配的内存否则会导致内核崩溃vfree可能会阻塞睡眠不能在中断上下文使用。5.4.2 vmalloc与kmalloc的核心区别特性kmallocvmalloc虚拟地址内核直接映射区虚拟地址和物理地址是线性映射的vmalloc区虚拟地址和物理地址无线性关系通过页表映射物理地址连续的不连续的最大分配大小4MBMAX_ORDER个页几十TB受vmalloc区大小限制分配开销极低快速路径无锁不需要修改页表高需要分配虚拟地址、分配物理页、修改内核页表、刷新TLB访问性能极高直接映射TLB命中率高略低页表映射是间接的TLB miss概率更高睡眠由gfp_mask决定GFP_ATOMIC不允许睡眠必须允许睡眠不能在中断上下文使用适用场景小内存分配默认小于4KB需要连续物理地址性能敏感场景大内存分配物理内存碎片化场景不需要连续物理地址5.4.3 避坑指南不要用vmalloc分配小内存vmalloc的分配开销很高有大量的元数据开销修改页表、刷新TLB小内存分配用kmalloc性能高几个数量级。绝对不能在中断上下文使用vmalloc/vfreevmalloc和vfree都会修改内核页表可能会阻塞睡眠中断上下文使用会导致内核死锁、崩溃。不要把vmalloc分配的内存用于DMADMA需要连续的物理地址vmalloc分配的内存物理地址是不连续的用于DMA会导致数据错误、设备崩溃DMA内存必须用dma_alloc_coherent分配。vmalloc分配的内存不能直接用virt_to_phys转换物理地址virt_to_phys只能用于直接映射区的内存vmalloc的内存是页表映射的必须用vmalloc_to_pfn获取物理页号再转换物理地址。5.5 特殊场景的内存分配接口5.5.1 DMA内存分配接口dma_alloc_coherent设备驱动中DMA操作需要物理地址连续、cache一致的内存必须用DMA专用的分配接口不能用kmalloc/vmalloc。原型void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)核心作用分配DMA一致性内存物理地址连续cache和内存一致不需要手动刷cache参数dev设备结构体指针size分配的内存大小dma_handle输出参数返回内存的DMA物理地址用于设备的DMA操作gfp分配标志位通常用GFP_KERNEL | GFP_DMA32返回值内存的内核虚拟地址驱动可以直接访问释放接口dma_free_coherent(dev, size, vaddr, dma_handle)。5.5.2 高端内存映射接口32位系统中高端内存超过1GB的物理内存不能直接映射到内核虚拟地址空间需要用kmap/kunmap接口临时映射到内核地址空间x86_64架构下不需要因为虚拟地址空间足够大所有物理内存都有直接映射。核心接口void *kmap(struct page *page)把高端内存的page映射到内核虚拟地址空间返回虚拟地址void kunmap(struct page *page)解除映射void *kmap_atomic(struct page *page)原子映射不允许睡眠可用于中断上下文void kunmap_atomic(void *addr)解除原子映射。5.6 内存分配接口选型最佳实践我们总结了不同场景下的接口选型规则覆盖内核开发、驱动开发的绝大多数场景小内存分配小于4KB常规内核上下文优先使用kzalloc安全、高效、自动初始化避免内存泄露和脏数据如果不需要初始化用kmalloc。数组分配动态大小优先使用kcalloc自动检查整数溢出初始化为0避免安全漏洞。大内存分配大于4KB需要连续物理地址使用alloc_pages/__get_free_pages直接从伙伴系统分配连续页避免kmalloc加剧碎片化。大内存分配不需要连续物理地址物理内存碎片化使用vmalloc/vzalloc不需要连续的物理页分配成功率高。中断上下文、自旋锁持有期间不能睡眠必须使用GFP_ATOMIC标志用kmalloc/alloc_pages绝对不能使用GFP_KERNEL、vmalloc、vfree。设备驱动的DMA操作必须使用dma_alloc_coherent不能用kmalloc/vmalloc保证物理地址连续、cache一致。频繁分配/释放固定大小的对象创建自定义的kmem_cache性能更高内存利用率更好更容易调试。需要初始化为0的内存优先使用带z前缀的接口kzalloc/vzalloc/kcalloc避免手动memset减少代码量防止遗漏初始化。