• malloc与kmalloc


    在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages.

       对于提供了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 inline unsigned 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);
       }
    virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。

    -------------------------------------------------------------------------------------

    1、kmalloc() 分配连续的物理地址,用于小内存分配。 

      2、__get_free_page() 分配连续的物理地址,用于整页分配。 

      至于为什么说以上函数分配的是连续的物理地址和返回的到底是物理地址还是虚拟地址,下面的记录会做出解释。 

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

      关于以上论述,我们可以查看 kmalloc() 的实现,kmalloc()函数的实现是在 __do_kmalloc() 中,可以看到在 __do_kmalloc()代码里最终调用了 __cache_alloc() 来分配一个 slab,其实 

      kmem_cache_alloc() 等函数的实现也是调用了这个函数来分配新的 slab。我们按照 __cache_alloc()函数的调用路径一直跟踪下去会发现在 cache_grow() 函数中使用了 kmem_getpages()函数来分配一个物理页面,kmem_getpages() 函数中调用的alloc_pages_node() 最终是使用 __alloc_pages() 来返回一个struct page 结构,而这个结构正是系统用来描述物理页面的。这样也就证实了上面所说的,slab 是在物理页面基础上实现的。kmalloc() 分配的是物理地址。 

      __get_free_page() 是页面分配器提供给调用者的最底层的内存分配函数。它分配连续的物理内存。__get_free_page() 函数本身是基于 buddy 实现的。在使用 buddy 实现的物理内存管理中最小分配粒度是以页为单位的。关于以上论述,我们可以查看__get_free_page()的实现,可以看到__get_free_page()函数只是一个非常简单的封状,它的整个函数实现就是无条件的调用 __alloc_pages() 函数来分配物理内存,上面记录 kmalloc()实现时也提到过是在调用 __alloc_pages() 函数来分配物理页面的前提下进行的 slab 管理。那么这个函数是如何分配到物理页面又是在什么区域中进行分配的?回答这个问题只能看下相关的实现。可以看到在 __alloc_pages() 函数中,多次尝试调用get_page_from_freelist() 函数从 zonelist 中取得相关 zone,并从其中返回一个可用的 struct page 页面(这里的有些调用分支是因为标志不同)。至此,可以知道一个物理页面的分配是从 zonelist(一个 zone 的结构数组)中的 zone 返回的。那么 zonelist/zone 是如何与物理页面关联,又是如何初始化的呢?继续来看 free_area_init_nodes() 函数,此函数在系统初始化时由 zone_sizes_init() 函数间接调用的,zone_sizes_init()函数填充了三个区域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。并把他们作为参数调用 free_area_init_nodes(),在这个函数中会分配一个 pglist_data 结构,此结构中包含了 zonelist/zone结构和一个 struct page 的物理页结构,在函数最后用此结构作为参数调用了 free_area_init_node() 函数,在这个函数中首先使用 calculate_node_totalpages() 函数标记 pglist_data 相关区域,然后调用 alloc_node_mem_map() 函数初始化 pglist_data结构中的 struct page 物理页。最后使用 free_area_init_core()函数关联 pglist_data 与 zonelist。现在通以上分析已经明确了__get_free_page() 函数分配物理内存的流程。但这里又引出了几个新问题,那就是此函数分配的物理页面是如何映射的?映射到了什么位置?到这里不得不去看下与 VMM 相关的引导代码。 

      在看 VMM 相关的引导代码前,先来看一下 virt_to_phys() 与phys_to_virt 这两个函数。顾名思义,即是虚拟地址到物理地址和物理地址到虚拟地址的转换。函数实现十分简单,前者调用了__pa( address ) 转换虚拟地址到物理地址,后者调用 __va(addrress ) 将物理地址转换为虚拟地址。再看下 __pa __va 这两个宏到底做了什么。 

      #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) 
      #define __va(x) ((void *)((unsigned long)(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与 VMALLOC_START 之间,后者为 vmalloc() 函数分配内存的启始地址)。这也就解释了为什么 virt_to_phys() 与 phys_to_virt() 函数的实现仅仅是加/减 PAGE_OFFSET 即可在虚拟地址与物理地址之间转换,正是因为了有了这种映射,且固定不变,所以才不用去查页表进行转换。这也同样回答了开始的问题,即 kmalloc() / __get_free_page() 分配的是物理地址,而返回的则是虚拟地址(虽然这听上去有些别扭)。正是因为有了这种映射关系,所以需要将它们的返回地址减去 PAGE_OFFSET 才可以得到真正的物理地址。

    另一篇更容易理解的:

    kmalloc, vmalloc分配的内存结构 zz
    2008-01-20 16:05

    进程空间:| <-用户空间-> | <-内核空间-> |
    内核空间:| <-物理内存映射区-> | <-vmalloc区域-> |

    ==============原文================================ 

       对于提供了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 inline unsigned 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);
       }
    virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。

    而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。

    我们用下面的程序来演示kmalloc、get_free_page和vmalloc的区别:
    #include <linux/module.h>
    #include <linux/slab.h>
    #include <linux/vmalloc.h>
    MODULE_LICENSE("GPL");
    unsigned char *pagemem;
    unsigned char *kmallocmem;
    unsigned char *vmallocmem;

    int __init mem_module_init(void)
    {
    //最好每次内存申请都检查申请是否成功
    //下面这段仅仅作为演示的代码没有检查
    pagemem = (unsigned char*)get_free_page(0);
    printk("<1>pagemem addr=%x", pagemem);

    kmallocmem = (unsigned char*)kmalloc(100, 0);
    printk("<1>kmallocmem addr=%x", kmallocmem);

    vmallocmem = (unsigned char*)vmalloc(1000000);
    printk("<1>vmallocmem addr=%x", vmallocmem);

    return 0;
    }

    void __exit mem_module_exit(void)
    {
    free_page(pagemem);
    kfree(kmallocmem);
    vfree(vmallocmem);
    }

    module_init(mem_module_init);
    module_exit(mem_module_exit);

    我们的系统上有160MB的内存空间,运行一次上述程序,发现pagemem的地址在0xc7997000(约3G+121M)、kmallocmem 地址在0xc9bc1380(约3G+155M)、vmallocmem的地址在0xcabeb000(约3G+171M)处,符合前文所述的内存布局。

    kmalloc参数详解

    #include <linux/slab.h> void *kmalloc(size_t size, int flags);

    给 kmalloc 的第一个参数是要分配的块的大小. 第 2 个参数, 分配标志, 非常有趣, 因为它以几个方式控制 kmalloc 的行为.
    最一般使用的标志, GFP_KERNEL, 意思是这个分配((内部最终通过调用 __get_free_pages 来进行, 它是 GFP_ 前缀的来源) 代表运行在内核空间的进程而进行的. 换句话说, 这意味着调用函数是代表一个进程在执行一个系统调用. 使用 GFP_KENRL 意味着 kmalloc 能够使当前进程在少内存的情况下睡眠来等待一页. 一个使用 GFP_KERNEL 来分配内存的函数必须, 因此, 是可重入的并且不能在原子上下文中运行. 当当前进程睡眠, 内核采取正确的动作来定位一些空闲内存, 或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存.
    GFP_KERNEL 不一直是使用的正确分配标志; 有时 kmalloc 从一个进程的上下文的外部调用. 例如, 这类的调用可能发生在中断处理, tasklet, 和内核定时器中. 在这个情况下, 当前进程不应当被置为睡眠, 并且驱动应当使用一个 GFP_ATOMIC 标志来代替. 内核正常地试图保持一些空闲页以便来满足原子的分配. 当使用 GFP_ATOMIC 时, kmalloc 能够使用甚至最后一个空闲页. 如果这最后一个空闲页不存在, 但是, 分配失败.
    其他用来代替或者增添 GFP_KERNEL 和 GFP_ATOMIC 的标志, 尽管它们 2 个涵盖大部分设备驱动的需要. 所有的标志定义在 <linux/gfp.h>, 并且每个标志用一个双下划线做前缀, 例如 __GFP_DMA. 另外, 有符号代表常常使用的标志组合; 这些缺乏前缀并且有时被称为分配优先级. 后者包括:
    GFP_ATOMIC
    用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
    GFP_KERNEL
    内核内存的正常分配. 可能睡眠.
    GFP_USER
    用来为用户空间页来分配内存; 它可能睡眠.
    GFP_HIGHUSER
    如同 GFP_USER, 但是从高端内存分配, 如果有. 高端内存在下一个子节描述.
    GFP_NOIO
    GFP_NOFS
    这个标志功能如同 GFP_KERNEL, 但是它们增加限制到内核能做的来满足请求. 一个 GFP_NOFS 分配不允许进行任何文件系统调用, 而 GFP_NOIO 根本不允许任何 I/O 初始化. 它们主要地用在文件系统和虚拟内存代码, 那里允许一个分配睡眠, 但是递归的文件系统调用会是一个坏注意.
    上面列出的这些分配标志可以是下列标志的相或来作为参数, 这些标志改变这些分配如何进行:
    __GFP_DMA
    这个标志要求分配在能够 DMA 的内存区. 确切的含义是平台依赖的并且在下面章节来解释.
    __GFP_HIGHMEM
    这个标志指示分配的内存可以位于高端内存.
    __GFP_COLD
    正常地, 内存分配器尽力返回"缓冲热"的页 -- 可能在处理器缓冲中找到的页. 相反, 这个标志请求一个"冷"页, 它在一段时间没被使用. 它对分配页作 DMA 读是有用的, 此时在处理器缓冲中出现是无用的. 一个完整的对如何分配 DMA 缓存的讨论看"直接内存存取"一节在第 1 章.
    __GFP_NOWARN
    这个很少用到的标志阻止内核来发出警告(使用 printk ), 当一个分配无法满足.
    __GFP_HIGH
    这个标志标识了一个高优先级请求, 它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.
    __GFP_REPEAT
    __GFP_NOFAIL
    __GFP_NORETRY
    这些标志修改分配器如何动作, 当它有困难满足一个分配. __GFP_REPEAT 意思是" 更尽力些尝试" 通过重复尝试 -- 但是分配可能仍然失败. __GFP_NOFAIL 标志告诉分配器不要失败; 它尽最大努力来满足要求. 使用 __GFP_NOFAIL 是强烈不推荐的; 可能从不会有有效的理由在一个设备驱动中使用它. 最后, __GFP_NORETRY 告知分配器立即放弃如果得不到请求的内存.
    kmalloc 能够分配的内存块的大小有一个上限. 这个限制随着体系和内核配置选项而变化. 如果你的代码是要完全可移植, 它不能指望可以分配任何大于 128 KB. 如果你需要多于几个 KB, 但是, 有个比 kmalloc 更好的方法来获得内存, 我们在本章后面描述.
    这方面的原因:
    kmalloc并不直接从分页机制中获得空闲页面而是从slab页面分配器那儿获得需要的页面,slab的实现代码限制了最大分配的大小为128k,即131072bytes,理论上你可以通过更改slab.c中的 cache_sizes数组中的最大值使得kmalloc可以获得更大的页面数,不知道有没有甚么副效应或者没有必要这样做,因为获取较大内存的方法有很多,想必128k是经验总结后的合适值。
    alloc_page( )可以分配的最大连续页面是4M吧。MAX_ORDER =10
    46 static inline struct page * alloc_pages(unsigned int gfp_mask, unsigned int order) 
    47 { 
    48 /* 
    49 * Gets optimized away by the compiler. 
    50 */ 
    51 if (order >= MAX_ORDER) 
    52 return NULL; 
    53 return _alloc_pages(gfp_mask, order); 
    54 } 

    alloc_pages最大分配页面数为512个,则可用内存数最大为2^9*4K=2M
  • 相关阅读:
    C++学习之【使用位操作符求素数分析】
    LeetCodeOJ刷题之13【Roman to Integer】
    QT学习之文件系统读写类
    让免费版MarkdownPad2使用Pro版本的功能
    QT学习之窗口右键菜单
    react 16.3+ 新生命周期 作业
    react 16.3+ 新生命周期
    node层设置proxy不生效的原因
    Javascript权威指南——读书笔记
    react踩坑
  • 原文地址:https://www.cnblogs.com/Ph-one/p/6101520.html
Copyright © 2020-2023  润新知