linux驱动-内存分配

时间:2022-10-11 23:36:55

linux地址类型:

先看图解:
linux驱动-内存分配
linux地址空间主要分两类:虚拟地址和物理地址。
图中假设虚拟地址和物理地址都是从0x00000000到0xFFFFFFFF(这里只是为了好理解,实际要根据具体情况配置)。当然物理地址要根据实际硬件,但虚拟地址不变,32位处理器4G寻址空间,0xC0000000(宏CONFIG_PAGE_OFFSET定义,)是内核空间和用户空间分界点。

内核空间和用户空间占比:一般来说Linux 内核按照 3:1 的比率来划分虚拟内存(X86等):3 GB 的虚拟内存用于用户空间,1GB 的内存用于内核空间。当然有些体系结构如MIPS使用2:2 的比率来划分虚拟内存:2 GB 的虚拟内存用于用户空间,2 GB 的内存用于内核空间,另外像ARM架构的虚拟空间是可配置(1:3、2:2、3:1)。

物理地址分类:

  • 内存物理地址(Physical addresses):
    实际RAM空间地址。

  • 总线地址(Bus addresses):
    寄存器地址(I/O地址)

虚拟地址分类:

  • 用户空间虚拟地址(User virtual addresses):
    图中绿色部分(映射到蓝色部分上,不是线性的)

  • 内核虚拟地址(Kernel virtual addresses):
    内核虚拟地址映射不一定是线性的,但也包括线性的。图中白色、灰色、红色都是内核虚拟地址,映射到黄色部分上。

  • 内核逻辑地址(Kernel logical addresses):
    内核逻辑地址,是线性映射的,内核逻辑地址和物理地址差值绝对值是个固定的值。红色、灰色部分都是。

linux内核地址空间划分:
内核地址(内核虚拟地址)划分为三个部分::ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。这种划分也是基于虚拟地址划分的。

如图:

  • 白色为ZONE_HIGHMEM(高端内存)
  • 灰色为ZONE_NORMAL(内核镜像,内核逻辑地址空间等都在这段)
  • 红色为ZONE_DMA(依赖处理器,这段也是线性映射的内核逻辑地址)

linux低内存:
ZONE_DMA和ZONE_NORMAL就是低内存,一直映射,是线性的,分配在这两个段的空间,就是低内存,其地址就是内核逻辑地址。

linux内核高端地址:

  • (1)linux内核code总是运行在虚拟地址的顶端(即对于32位处理器,内核最大地址总是0xFFFFFFFF)。0xFFFFFFFF是寻址空间的最大地址,那么从图中内核空间的映射可以看出,内核最多能访问到物理地址的0xC000000处,而超出0xC000000的物理地址空间,内核就无法访问。linux为了能访问到超出0xC000000的这部分物理地址空间,划分出了ZONE_HIGHMEM(图中白色部分)存放映射表(映射图中蓝色部分),当内核要访问超出0xC000000的这部分物理地址空间,就通过映射表来访问,所以ZONE_HIGHMEM就是内核高端地址(高端内存,实际就是内核用来访问用户空间,可以说高端内存就是用户空间)

  • (2)我们知道应用程序运行在绿色部分的用户空间,绿色部分是映射到蓝色部分的,应用程序运行的物理地址空间是0xC0000000-0xFFFFFFFF,而应用程序对x0(虚拟地址)访问,其实访问的是物理地址的0xC0000000,这样可以推理出,应用程序是无法访问黄色部分内核空间,实现了用户空间的应用程序无法访问内核空间

高端内存:
内核将高端内存划分为3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
linux驱动-内存分配
对应高端内存的3部分,高端内存映射有三种方式:

  • 1、非连续映射地址空间(noncontiguous memory allocation)
    申请的非连续空间的映射表,存放在VMALLOC_START~VMALLOC_END这段空间里。像vmalloc,vmap等函数申请的空间,映射表都是存放在这个空间(vmalloc、vmap在include/linux/vmalloc.h里声明,这个头文件声明的函数,其申请的空间,映射表基本上是存放在这段)。
    vmap需要用alloc_pages_node、alloc_pages、alloc_page(定义在incluce/linux/gfp.h)等获取page,然后作为vmap的参数建立映射,vmap返回内核虚拟地址。

  • 2、永久内核映射(permanent kernel mapping)
    内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。(映射表,存放在这段空间)
    用函数kmap()来映射,同样kmap也是需要用alloc_pages_node、alloc_pages、alloc_page(定义在incluce/linux/gfp.h)等获取page,然后作为kmap的参数建立映射,kmap返回内核虚拟地址。

    • 3、临时映射(temporary kernel mapping)
      内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。(映射表,存放在这段空间)

这块空间具有如下特点:
(1)每个 CPU 占用一块空间
(2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page(用来存放映射表)。

当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。

同样kmap_atomic也是需要用alloc_pages_node、alloc_pages、alloc_page(定义在incluce/linux/gfp.h)等获取page,然后作为kmap_atomic的参数建立映射,kmap_atomic返回内核虚拟地址。

像include/linux/percpu.h里面alloc_percpu为每个CPU分配一个变量,也是存放在这个段。

有了上面这些理解,可以开始内存分配的函数接口API。
还可以参考下列博客:
linux 用户空间与内核空间——高端内存详解
Linux 内存管理之highmem简介
kmalloc linux内存管理

linux kernel内存分配函数:

kernel内存分配时涉及内核虚拟地址、内核逻辑地址、页(page)、页帧号(pfn)。其中内核虚拟地址和内核逻辑地址前面已经描述过,现在来看看下page,pfn。

页(page):一页用struct page这个结构体来描述的,一页的空间大小用宏PAGE_SIZE(一般4k)配置。

在include/asm-generic/page.h中定义了页大小:

#define PAGE_SHIFT 12
#ifdef __ASSEMBLY__
#define PAGE_SIZE (1 << PAGE_SHIFT)
#else
#define PAGE_SIZE (1UL << PAGE_SHIFT)
#endif
#define PAGE_MASK (~(PAGE_SIZE-1))

这段code,定义的PAGE_SIZE就是4K。

页帧号(pfn):将内核虚拟地址右移PAGE_SHIFT得到的地址,就是页帧号。页帧号和页大小紧密联系的。

在头文件include/linux/mm_types.h(其实linux/mm.h包含了mm_types.h,平时一般包含mm.h就可以了)中定义了struct page结构体,下面只例出重要的几个说明:
atomic_t count:这个页的引用数. 当这个 count 掉到 0, 这页被返回给空闲列表.
void *virtual:这页的内核虚拟地址, 如果它被映射; 否则, NULL. 低内存页一直被映射; 高内存页常常不是. 这个成员不是在所有体系上出现; 它通常只在页的内核虚拟地址无法轻易计算时被编译. 如果你想查看这个成员, 正确的方法是使用 page_address 宏。
unsigned long flags:一套描述页状态的一套位标志. 这些包括 PG_locked, 它指示该页在内存中已被加锁, 以及 PG_reserved, 它防止内存管理系统使用该页.

  • 1、内核虚拟地址,页,页帧号之间转换函数接口:

定义的头文件include/asm-generic/page.h

#ifndef __ASSEMBLY__

#define __va(x) ((void *)((unsigned long) (x)))
#define __pa(x) ((unsigned long) (x))

#define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT)
#define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT)

#define virt_to_page(addr) pfn_to_page(virt_to_pfn(addr))
#define page_to_virt(page) pfn_to_virt(page_to_pfn(page))

#ifndef page_to_phys
#define page_to_phys(page) ((dma_addr_t)page_to_pfn(page) << PAGE_SHIFT)
#endif

#define pfn_valid(pfn) ((pfn) >= ARCH_PFN_OFFSET && ((pfn) - ARCH_PFN_OFFSET) < max_mapnr)

#define virt_addr_valid(kaddr) (((void *)(kaddr) >= (void *)PAGE_OFFSET) && \
((void *)(kaddr) < (void *)memory_end))

#endif /* __ASSEMBLY__ */

virt_to_page(addr): 采用一个内核逻辑地址并返回它的被关联的struct page 指针.
pfn_to_virt(pfn):将页帧号转换成内核虚拟地址。
pfn_to_page (pfn): 为给定的页帧号返回 struct page 指针. 如果需要, 它在传递给 pfn_to_page 之前使用 pfn_valid 来检查一个页帧号的有效性.
pfn_valid(pfn): 检查pfn的有效性,有效pfn返回true,无效pfn返回false。

如果是ARM体系的CPU,在arch/arm/include/asm/memory.h中还有如下的一些函数和宏(依赖CPU体系结构):
phys_addr_t virt_to_phys(const volatile void *x):将一个内核虚拟地址转换成映射的物理地址(内核空间一直被映射)。
void *phys_to_virt(phys_addr_t x):将一个物理地址转换成内核虚拟地址。
page_to_phys(page):获取一个页映射的物理起始地址。

总之,记住内核只能访问内核虚拟地址,要根据各种实际情况用这些函数转换。

  • 2、内存分配一些标记:
    下面的标记定义在include/linux/gfp.h中:
    (1)GFP_ATOMIC:用来从中断处理和进程上下文之外的其他代码中分配内存。从不睡眠。可以用在具有原子性的地方,分配函数。
    (2)GFP_KERNEL:内核内存的正常分配。可能睡眠。在具有原子性的地方,不能用。
    (3)GFP_USER:用来为用户空间页来分配内存; 它可能睡眠。在具有原子性的地方,不能用。
    (4)GFP_HIGHUSER:如同 GFP_USER, 但是从高端内存分配, 如果有. 高端内存在下一个子节描述.在具有原子性的地方,不能用。
    (5)GFP_NOIO、GFP_NOFS:这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求. 一个 GFP_NOFS 分配不允许进行任何文件系统调用, 而 GFP_NOIO 根本不允许任何I/O 初始化. 它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠,但是递归的文件系统调用会是一个坏注意.
    一般用得比较多的是GFP_KERNEL,GFP_ATOMIC。

    上面这些宏,可以和下面说的宏或起来:
    (1)__GFP_DMA:这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的。
    (2)__GFP_HIGHMEM:这个标志指示分配的内存可以位于高端内存.
    (3)__GFP_HIGH:这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.
    (4) __GFP_REPEAT:如果分配不到,重复尝试,但是分配可能仍然失败.
    (5) __GFP_NOFAIL:标志告诉分配器不要失败; 它尽最大努力来满足要求.
    (6)__GFP_NOFAIL:强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它.
    (7)__GFP_NORETRY:告知分配器立即放弃如果得不到请求的内存.

  • 3、从高端内存分配page:
    从高端内存分配,主要用:

#include <linux/gfp.h>

中的函数来分配。
(1)struct page *alloc_pages_node(int nid, gfp_t gfp_mask,unsigned int order)
功能:分配指定page数
第一个参数:nid 是要分配内存的 NUMA 节点 ID,通常用numa_node_id()获得。
第二个参数:gfp_mask是标志,如GFP_KERNEL、GFP_ATOMIC等标志。
第三个参数:分配页数,页数等于2的order次方,如要分配8页,那么2的3次方等于8,所以order为3。一般可以用函数get_order(定义在incluce/asm-generic/getorder.h)来获得order的值,get_order(8)等于2,这个函数取以2位底的对数。
返回值:返回page指针(page用链表链接起来的,这里返回的是page头指针)

释放函数: void __free_pages(struct page *page, unsigned int order);

(2)alloc_pages(gfp_mask, order)
功能:分配指定page数,这个函数是对alloc_pages_node封装。
第一个参数:gfp_mask是标志,如GFP_KERNEL、GFP_ATOMIC等标志。
第二个参数:分配页数,页数等于2的order次方,如要分配8页,那么2的3次方等于8,所以order为3。一般可以用函数get_order(定义在ncluce/asm-generic/getorder.h)来获得order的值,get_order(8)等于2,这个函数取以2位底的对数。order不能超过11,如果为10或者11时,分配很容易失败。order最大值依赖于体系。
返回值:返回page指针
释放函数:__free_pages(struct page *page, unsigned int order);

(3)alloc_page(gfp_mask)
功能:分配一个页(是对alloc_pages封装)
第一个参数:gfp_mask是标志,如GFP_KERNEL、GFP_ATOMIC等标志。
返回值:返回page指针
释放函数:__free_page(page)

(4)vmalloc
这个函数定义在include/linux/vmalloc.h中
void *vmalloc(unsigned long size)
功能:在高端内存分配空间,函数内部是通过将不连续的page,用页的方式连接起来,形成看似连续的空间,单其实地址空间不连续的内存。vmalloc分配的可能在内核空间,可能在高内存。大空间分配,用它。原子上下文不能用。
第一个参数:要分配的空间大小,单位字节
返回值:内核虚拟地址
释放函数:void vfree(const void *addr),addr是vmalloc返回的内核虚拟地址。

总结,从高端分配page,就是从用户空间分配空间来内核用。

  • 4、page映射成内核虚拟地址:

    在“从高端内存分配page”小节描述的,分配的page需要有下面接口来映射成内核虚拟地址(但不能用“内核虚拟地址,页,页帧号之间转换函数接口”这小节的接口来转换,它们是已经映射过后的地址转换)

定义头文件:

#include <linux/highmem.h>

void *kmap(struct page *page)
功能:将page映射,并返回内核虚拟地址。如果page位于内核空间(也有说低内存),将返回内核逻辑地址(不要忘了,内核逻辑地址也属于内核虚拟地址);page位于高内存, kmap 在内核地址空间的一个专用部分中(高内存中永久内核映射区)创建一个特殊的映射,返回内核虚拟地址。由于永久内核映射区空间有限,最好不要在它们上停留太长时间,尽快用kunmap来释放。注意这函数,可能睡眠的,不能用在有原子性地方,如中断服务程序,临界区等。
第一个参数:要映射的page指针。

void kunmap(struct page *page)
功能:释放kmap映射的page。(发现kunmap函数是空函数)
第一个参数:释放映射的page指针。

void *kmap_atomic(struct page *page)
功能:page映射成内核虚拟地址,page位于高内存还是低内存情况,和kmap一样。这个函数映射的page,其映射表,存放在高内存的临时映射区。kmap_atomic是不睡眠的,可以用在具有原子性的地方。
第一个参数:映射的page指针。
返回值:内核虚拟地址。

kunmap_atomic(addr)
功能:释放kmap_atomic映射的page
第一个参数:addr是kmap_atomic返回的内核虚拟地址。
返回值:没有,不管。

  • 5、低内存分配:
    (1)kmalloc函数
    定义头文件:
#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags)
功能:在低内存(内核逻辑地址)空间分配空间,分配的空间是连续的。当然用__GFP_HIGHMEM时,有可能在高内存中分配(个人认为,在内核空间不够时会)
第一个参数:要分配空间大小,单位字节。
第二个参数:标志,看“内存分配一些标记”小节。
返回值:返回内核逻辑地址。

kmalloc() 函数本身是基于 slab 实现的。slab 是为分配小内存提供的一种高效机制。但 slab 这种分配机制又不是独立的,它本身也是在页分配器的基础上来划分更细粒度的内存供调用者使用。也就是说系统先用页分配器分配以页为最小单位的连续物理地址,然后 kmalloc() 再在这上面根据调用者的需要进行切分。

(2)__get_free_pages
头文件:

#include <linux/gfp.h>

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
功能:在低内存中以页为单位,分配连续页空间,但不清零。当然用__GFP_HIGHMEM时,有可能在高内存中分配(个人认为,在内核空间不够时会)
第一个参数:标志,看“内存分配一些标记”小节。
第二个参数:分配页数,页数等于2的order次方,如要分配8页,那么2的3次方等于8,所以order为3。一般可以用函数get_order(定义在ncluce/asm-generic/getorder.h)来获得order的值,get_order(8)等于2,这个函数取以2位底的对数。order不能超过11,如果为10或者11时,分配很容易失败。order最大值依赖于体系。
返回值:内核逻辑地址
释放函数:void free_pages(unsigned long addr, unsigned int order),addr是__get_free_pages返回的逻辑地址。

其中kmalloc()函数内部会调用到__get_free_pages。

(3)get_zeroed_page
头文件同__get_free_pages定义在gfp.h中

unsigned long get_zeroed_page(gfp_t gfp_mask)
功能:在低内存分配一页大小连续地址空间,并且将这一页空间清零。
第一个参数:标志,看“内存分配一些标记”小节。
返回值:内核逻辑地址。
释放函数: free_page(addr),addr是返回的内核逻辑地址。

(4)__get_free_page
头文件gfp.h
__get_free_page(gfp_mask)
功能:在低内存分配一页大小连续地址空间,但不清零。
第一个参数:标志,看“内存分配一些标记”小节。
返回值:内核逻辑地址。
释放函数: free_page(addr),addr是返回的内核逻辑地址。

6、后备缓存
(1)在驱动中,需要反复分配相同空间大小的内存情况,建立后备缓存效率更好。这种场景,也是建议的方法。

使用的函数都是定义在include/linux/slab.h中。

首先,创建后备缓存:

struct kmem_cache *kmem_cache_create(const char *name, size_t size, 
size_t offset,unsigned long flags,
void (*)(void *));

功能:创建kmem_cache结构体指针。
第一个参数:后备缓存的名字,name是字符串。
第二个参数:size大小空间,但并不是后备缓存总空间的大小,是后面用kmem_cache_alloc分配时,空间的大小。单位,字节。
第三个参数:分配空间对齐方式(字对齐,还是页对齐等),0表示默认对齐方式。
第四个参数:flags标志。
第五个参数:在分配和释放时调用,即调用kmem_cache_alloc和kmem_cache_free时。没有可以设置NULL。
返回值:返回分配kmem_cache结构体指针。失败为NULL

部分flags标志(都定义在slab.h中):
SLAB_HWCACHE_ALIGN:这个标志需要每个数据对象被对齐到一个缓存行; 实际对齐依赖主机平台的缓存分布. 这个选项可以是一个好的选择, 如果在 SMP 机器上你的缓存包含频繁存取的项. 但是, 用来获得缓存行对齐的填充可以浪费可观的内存量.

SLAB_CACHE_DMA:这个标志要求每个数据对象在 DMA 内存区分配.

其次,kmem_cache创建完成后,就可以用下列函数分配了:

void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags)

功能:分配固定大小空间的(由kmem_cache_create()参数size决定)
第一个参数:创建好的kmem_cache指针
第二个参数:flags标志,同kmalloc用的标志。
返回值:返回内核逻辑地址。失败为NULL。
释放函数:void kmem_cache_free(struct kmem_cache , void ),第一个参数就是创建好的kmem_cache指针,第二个参数是kmem_cache_alloc返回的值。

然后,模块卸载时,销毁后备缓存:

void kmem_cache_destroy(struct kmem_cache *);

功能:销毁后备缓存kmem_cache
第一个参数:创建的后备缓存kmem_cache结构体指针。

使用例子:

sturct kmem_cache *kmemca = NULL;
void *p = NULL;

int rr()
{
int i = 0;
if(kmemca )
{
p = kmem_cache_alloc(kmemca ,GFP_KERNEL);
for(i=0;i<1024/4;i++)
{
*(p+i) = 345;
}
}
else
return -1;

return 1;
}
void init()
{
kmemca = kmem_cache_create("kmc",1024,0,SLAB_HWCACHE_ALIGN,NULL);
}
void exite()
{
if(p)
kmem_cache_free(kmemca ,p);
if(kmemca)
kmem_cache_destroy(kmemca );

}

例子只是展示了使用接口的过程,没有严格的去编译过。

(2)当我们需要不分配失败的情况,那么需要创建内存池

使用函数定义在include/linux/mempool.h中。

首先,创建kmem_cache结构体指针
其次,创建内存池
用函数:

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn, void *pool_data);

功能:创建内存池
第一个参数:内存池应当一直保留的最小数量的分配的对象,即每次分配最小空间,不能低于这个数字,单位,字节。
第二个参数:自定义分配函数,不自定义用提供的mempool_alloc_slab()
第三个参数:自定义释放函数,不自定义用提供的mempool_free_slab()
第四个参数:就是创建的kmem_cache结构体指针。

void *mempool_alloc_slab(gfp_t gfp_mask, void *pool_data);
void mempool_free_slab(void *element, void *pool_data);

内存池创建完成。

用函数mempool_resize重新调整内存池大小:

int mempool_resize(mempool_t *pool, int new_min_nr);

第一个参数:mempool_create返回的地址。
第二个参数:将内存池重新调整到空间大小,单位字节。

当不用内存池时,销毁:

void mempool_destroy(mempool_t *pool);

参数:mempool_create返回的地址。

7、CPU变量
多核情况下,我们可以为每个CPU定义自己的变量。

这里只是认为per-cpu变量是一直特殊空间,列出来。很少使用到。不响应描述。
有兴趣的可以参考下面博客:
Linux内核同步机制之(二):Per-CPU变量
linux percpu机制解析