void *
kmalloc
(size_t size, int flags);
存在于<linux/slab.h>中。
Kmalloc提供了再设备驱动程序或者内核中动态分配内存的方式,kmalloc函数返回的是虚拟地址(线性地址),分配的内存在物理上是连续的,这对于要进行DMA的设备十分 真实的物理地址只有一个固定的偏移,因此存在比较简单的转换关系。Kmalloc最多只能分配32*PAGE_SIZE大小的内存,一般PAGE_SIZE=4KB,也就是128KB大小的内存,这128KB的内存还要有16个字节用来页面描述结构。
2.1参数含义
size: 需要分配的内存字节数
flags: 分配内存的类型
2.2参数描述
size_t 类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版。它是一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。
size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long unsigned int。size_t的设计就是为了适应多个平台的,增加程序在不同平台上的可移植性。
kmalloc的flags参数决定了要分配的内存的类型,kmalloc提供了多种内存分配行为,使用flags标志,可以控制kmalloc的行为。
所有的flags都定义在 <linux/gfp.h>中。
下表给出针对不同的flags的不同含义:
flags |
含义(说明,使用情况) |
GFP_KERNEL |
1. 最常用的flag,代表一个在内核空间运行的线程进行调用,只能在进程内核空间调用,就是说函数是代表一个进程在执行一个系统调用,如果内存不足则当前进行会进行睡眠。 2. 只能在用户进程上下文中使用,是最可靠的内存分配方式。 3. 并不是总是正确的内存分配方式,如果在进程上下文之外调用kmalloc则会出错。 4. 允许在分配内存时如果内存不足进行睡眠进行等待,因此这时分配函数必须是可以重入的。 5. 如果在处理进程上线文之外的请求,例如中断处理、tasklet以及内核时钟等,则不允许睡眠等待内存,应使用GFP_ATOMIC。 |
GFP_ATOMIC |
1. 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠. 2. 能够实现分配即使是最后一个空闲页,如果最后的页都不是空闲页,则分配失败。 3. 没有GFP_KERNEL分配内存可靠,应该有良好的内存溢出处理策略。 |
GFP_USER |
1. 用来为用户空间页来分配内存。 2. 可能睡眠。 |
GFP_HIGHUSER |
1. 和GFP_USER,如果存在高端内存则进行分配高端内存。 |
GFP_NOIO |
1. 这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求 2. GFP_NOIO 根本不允许任何 I/O 初始化。 |
GFP_NOFS |
1. 这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求 2. 一个 GFP_NOFS 分配不允许进行任何文件系统调用 |
GFP_DMA |
1. 分配给ISA DMA少于16M的内存。 2. 非常的不可靠,尽量不要用。 |
上面列出的flags能够与下面给出的flags通过“或”操作进行扩充:
flags |
含义(说明,使用情况) |
__GFP_DMA |
这个标志要求分配在能够 DMA 的内存区,是平台依赖的。 |
__GFP_HIGHMEM |
这个标志指示分配的内存可以位于高端内存. |
__GFP_COLD |
正常地, 内存分配器尽力返回"缓冲热"的页 -- 可能在处理器缓冲中找到的页. 相反, 这个标志请求一个"冷"页, 它在一段时间没被使用. 它对分配页作 DMA 读是有用的, 此时在处理器缓冲中出现是无用的. |
__GFP_NOWARN |
这个很少用到的标志阻止内核来发出警告(使用 printk ), 当一个分配无法满足. |
__GFP_HIGH |
这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页. |
__GFP_REPEAT __GFP_NOFAIL __GFP_NORETRY
|
这些标志修改分配器如何动作, 当它有困难满足一个分配. __GFP_REPEAT 意思是" 更尽力些尝试" 通过重复尝试 -- 但是分配可能仍然失败. __GFP_NOFAIL 标志告诉分配器不要失败; 它尽最大努力来满足要求. 使用 __GFP_NOFAIL 是强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它. 最后, __GFP_NORETRY 告知分配器立即放弃如果得不到请求的内存. |
Linux 内核把内存分为 3 个区段: 可用于DMA的内存(位于一个特别的地址范围的内存, 外设能够在这里进行 DMA 存取)、常规内存和高端内存(为了访问(相对)大量的内存而存在的一种机制)。目的是使每种电脑平台都必须知道如何将自己特定的内存范围归类到这三个区段中,而不是任何RAM都相同。所以就出现了上面flags中的__GFP_HIGH分配高端内存的这种flag。
但是,虽然在使用kmalloc的时候可以指定需要分配的内存的大小,但是实际使用的时候,内核只能分配一些预定义的、指定大小的字节数组。kmalloc 能够处理的最小内存块是 32 或 64 字节(体系结构依赖),而内存块大小的上限随着体系和内核配置而变化。考虑到移植性,不应分配大于 128 KB的内存。
1. Kmalloc与vmalloc是在系统的内核内存中进行内存分配,而malloc是在用户内存中进行分配;
2. Kmalloc保证分配的内存在物理上是连续的,vmalloc保证分配的内存在虚拟地址空间是连续的,但是物理地址是否连续不保证;
3. Kmalloc分配的内存大小有限,最大为128K,而vmalloc和malloc能分配的内存相对较大;
4. 内存在被DMA访问的时候需要使用kmalloc分配物理上连续的内存。
在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,vfree,或free_pages。 kmalloc函数返回的是虚拟地址(线性地址)。 kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要。 而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA。
内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。
很多硬件设备需要比较大的一块连续的内存用作DMA传送,这块内存需要一直驻留内存,不能被交换到文件中去,所以这个时候需要使用kmalloc在物理内存映射区域进行内存分配,这个时候分配的内存的地址在3G~3G+160M之间。
7.1分配内存
对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。
进程的4GB内存空间被人为的分为两个部分:用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。
内核空间中,从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用的 VMware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。对于 160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap 来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)
kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:
#define __pa(x)((unsigned long)(x)-PAGE_OFFSET)
extern inlineunsigned long virt_to_phys(volatile void * address)
{
return__pa(address);
}
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。
与之对应的函数为phys_to_virt(),将物理地址转化为内核虚拟地址:
#define __va(x)((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void* phys_to_virt(unsigned long address)
{
return__va(address);
}
7.2 LINUX内核物理内存分配函数分析
kmalloc() 函数本身是基于 slab 实现的。slab 是为分配小内存提供的一种高效机制。但 slab 这种分配机制又不是独立的,它本身也是在页分配器的基础上来划分更细粒度的内存供调用者使用。也就是说系统先用页分配器分配以页为最小单位的连续物理地址,然后 kmalloc() 再在这上面根据调用者的需要进行切分。
先来看一下 virt_to_phys() 与phys_to_virt 这两个函数。顾名思义,即是虚拟地址到物理地址和物理地址到虚拟地址的转换。函数实现十分简单,前者调用了__pa( address ) 转换虚拟地址到物理地址,后者调用 __va(addrress ) 将物理地址转换为虚拟地址。再看下__pa __va 这两个宏到底做了什么。
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsignedlong)(x)+PAGE_OFFSET))
通过上面可以看到仅仅是把地址加上或减去PAGE_OFFSET,而PAGE_OFFSET 在 x86 下定义为 0xC0000000。这里又引出了疑问,在 linux 下写过 driver 的人都知道,在使用 kmalloc() 与__get_free_page() 分配完物理地址后,如果想得到正确的物理地址需要使用 virt_to_phys() 进行转换。那么为什么要有这一步呢?我们不分配的不就是物理地址么?怎么分配完成还需要转换?如果返回的是虚拟地址,那么根据如上对 virt_to_phys() 的分析,为什么仅仅对 PAGE_OFFSET 操作就能实现地址转换呢?虚拟地址与物理地址之间的转换不需要查页表么?代着以上诸多疑问来看 VMM相关的引导代码。
直接从start_kernel() 内核引导部分来查找 VMM相关内容。可以看到第一个应该关注的函数是 setup_arch(),在这个函数当中使用paging_init() 函数来初始化和映射硬件页表(在初始化前已有 8M内存被映射,在这里不做记录),而 paging_init() 则是调用的pagetable_init() 来完成内核物理地址的映射以及相关内存的初始化。在pagetable_init() 函数中,首先是一些 PAE/PSE/PGE 相关判断与设置,然后使用 kernel_physical_mapping_init() 函数来实现内核物理内存的映射。在这个函数中可以很清楚的看到,pgd_idx是以PAGE_OFFSET为启始地址进行映射的,也就是说循环初始化所有物理地址是以 PAGE_OFFSET 为起点的。继续观察我们可以看到在 PMD 被初始化后,所有地址计算均是以 PAGE_OFFSET 作为标记来递增的。分析到这里已经很明显的可以看出,物理地址被映射到以 PAGE_OFFSET 开始的虚拟地址空间。这样以上所有疑问就都有了答案。kmalloc() 与__get_free_page() 所分配的物理页面被映射到了 PAGE_OFFSET 开始的虚拟地址,也就是说实际物理地址与虚拟地址有一组一一对应的关系。
综上所述,循环初始化所有物理地址都是以PAGE_OFFSET为起点的,物理地址被映射到以PAGE_OFFSET开始的虚拟地址空间,所以kmalloc所分配的物理页面被映射到以PAGE_OFFSET开始的虚拟地址,这样就和物理地址存在一个直接的一一对应的关系,所以通过kmalloc分配得到的虚拟地址减去PAGE_OFFSET就可以获得证实的物理地址,而不用查找页表进行转换。
1. 用于在设备驱动程序或者内核模块中动态的分配内存;
2. 最大只能分配128K内存,其中还有16字节用来记录页描述结构;
3. Kmalloc参数中flags参数决定了如何进行内存分配,不同的分配方式决定不同的实现方式;
4. 在kmalloc参数中指定分配的内存的大小,但是实际分配的内存可能比指定的大,因为kmalloc只能分配一些预定义的、指定大小的字节数组,能分配的最小的内存块是32或者64字节(操作系统依赖);
5. Kmalloc分配的内存物理地址连续的,特别适合用作DMA传送的硬件设备;
6. Kmalloc分配内存以后返回的虚拟地址,是物理地址加上一个PAGE_OFFSET的地址,获取物理地址只需要减去PAGE_OFFSET,而不用去查找页表;
7. Kmalloc分配的内存驻留内存,不会被交换到文件中;
8. Kmalloc分配的内存在内核空间中,存在与物理内存映射区域(3G~vmalloc_start);
9. Kmalloc用于小内存分配;