zoukankan      html  css  js  c++  java
  • Linux内存管理和应用

    [作者:byeyear。首发于cnblogs,转载请注明。联系:east3@163.com]

          本文对Linux内存管理使用到的一些数据结构和函数作了简要描述,而不深入到它们的内部。对这些数据结构和函数有了一个总体上的了解后,再针对各项分别作深入了解的时候,也许会简单一些。

    Linux内存访问限制(仅针对32位系统)

    • 默认情况下Linux的内核空间映射到4G虚拟地址的最高1G(即0xC0000000 - 0xFFFFFFF)

    在x86中,这个偏移量表示为TASK_SIZE,也表示为PAGE_OFFSET。

    对于其他arch,TASK_SIZE和PAGE_OFFSET之间可能存在HOLE。

    • 如果不考虑HIGH_MEMORY,Kernel中内存物理地址和逻辑地址(此时就是虚拟地址)有线性对应关系

    有两个函数:virt_to_phys和phys_to_virt用于在内核物理地址和逻辑地址间进行转换。

    • 所以,32位系统中,Linux能直接访问的物理内存最大为1G

         PHY_ADDR[0] -> LOG_ADDR[3G]
         PHY_ADDR[1G] -> LOG_ADDR[4G]

    • 除去保留给vmalloc、kmap和其他一些用途的地址空间,可直接访问的物理内存最大为896M
    • 进程的虚拟地址空间仍然是4G。0-TASK_SIZE(3G)给user space,各个processor不同;kernel space的1G对每个processor都是一样的。
    • 后面我们会说到如何访问1G以上的物理内存。

    一些数据结构

    • struct vm_area_struct

          该结构用来描述进程虚拟地址空间中一段连续的虚拟内存。但是我们知道,进程所占用的虚拟地址空间通常是不连续的(例如要分为代码、Heap和stack;或者考虑经常申请/释放内存的情况),所以一个进程需要多个struct vm_area_struct来描述其virtual memory area。根据进程需要的该结构体数量的大小,这一堆的vm_area_struct将组织成Linear或Linear+AVL的形式。

    • struct mm_struct

         这个结构用来管理上文所述那一堆的vm_area_struct。在这个结构中有个指针,指向那堆struct vm_area_struct的链表头,另有一个指针指向AVL tree的root(如果有的话)。而mm_struct本身又是task_struct里的一个成员。

    • zone和node

         在多CPU系统中,每个CPU对应一个node用于描述该CPU所“拥有”的内存区域。这里说的“拥有”是指与该CPU具有最佳亲缘性的内存区域,该CPU访问这些内存时将获得最高的性能。一块CPU也可以访问*非*它所“拥有”的内存区域,但访问“非拥有内存区域”的性能不如访问“拥有的内存区域”的性能。

         每个node又拆分成多个zone,不同的zone有不同的属性。例如分配给ISA总线设备进行DMA的内存必须在16M以下(天知道现在的计算机上还有木有ISA设备了),这些内存就归为ZONE_DMA。High Mem归属于ZONE_HIGHMEM。不具有特殊性质的内存都归结到ZONE_NORMAL。

         描述node和zone的结构分别为typedef struct pglist_data pg_data_t和struct zone。

         每个pg_data_t里有一个struct zone数组,数组的元素个数就是ZONE的类型数。

         每个pg_data_t里还有一个struct zonelist结构的数组,数组的每个元素都用来描述一种特定的“分配策略”。所谓“分配策略”,就是在分配内存的时候根据分配需要(kmalloc的第二参数就是用来描述这方面的需要的)确定从哪个zone开始,以及可以使用哪些zone。用于ISA DMA的内存必须在16M以下,所以需要一种“策略”;从High Mem中分配内存必须在1G以上,也需要一种策略;普通的没有特殊要求的内存分配也需要一种策略。有多少种策略,struct zonelist数组中就有多少个元素。

    • struct zonelist

         该结构用于描述某种特定的分配“策略”,其主要成员是一个数组struct zone *zones[MAX_ZONES_PER_ZONELIST + 1],[...]中的“+1”用来仿照字符串结尾的方式确定数组元素的实际个数。该数组的元素是所有可用于某个特定“策略”的zone们。假定可用于ISA DMA的zone是zoneA,zoneB,zoneC,分配顺序也是从A开始,那么这个数组中的三个元素就是指向zoneA,zoneB,zoneC的指针。

    • struct page

    Linux会为它所管辖的物理内存的每个Page Frame建立一个struct page数据结构,而不管这个Page Frame是否是HIGH_MEMORY。

    • high_memory

          这是一个变量,表示能够直接管辖的物理内存尾。如果物理内存小于896M,那么它就是实际物理内存大小;否则就是896M。

    vmalloc .vs. kmap

          vmalloc用于分配一段虚拟地址空间连续、但是物理地址空间未必连续的内存。vmalloc所能使用的虚拟地址空间在high_memory之上(high_memory + 8M)。由于vmalloc分配得到的内存在虚拟地址和物理地址之间不存在线性关系,所以vmalloc可以从HIGH_MEMORY处分配page frame,而不局限于896M以下。事实上,vmalloc优先考虑HIGH_MEMORY。这里顺便说下,ioremap所得到的虚拟地址也占用vmalloc可用的虚拟地址空间。一般情况下,调用vmalloc能够分配到的内存空间可以远大于一次kmalloc所能得到的。不过vmalloc内部仍然依赖kmalloc逐个分配page frame。

          kmap用于在page frame和虚拟地址之间建立映射。kmap本身并不分配物理内存,在使用前需要使用alloc_page先分配page_frame。对于Low memory,kmap将直接返回page frame对应的物理地址。

          kmap和vmalloc在应用上的最大区别在于,kmap往往应用于“空间分配”与“地址映射”可以分开的场合。考虑下面的场景:

            a) App想要通过网络发送数据;

            b) Linux将用户数据复制到内核buffer;

            c) NIC driver通过DMA发送数据。

          在上述场景的步骤b)中,Linux将执行类似下面的代码(仅作示意,不代表真实代码):

    while(more_data_to_copy) {
        struct page* page = alloc_page(...);
        void* vadr = kmap_atomic(page);
        copy_to_kernel(vadr, user_buf, size);
        kunmap_atomic(page);
        link_page_to_list(page);
    }

          上面的代码中,一页数据拷贝完成后该页的地址映射就可以立即解除,因为后续的DMA操作是不需要用到内核虚拟地址的。在linux源代码的net目录中的代码大量使用了kmap(_atomic)。这样做(将空间分配和地址映射分离)的好处是可以节约大量的页表(考虑网络服务器中存在大量待收发数据的情况),可以很好的利用HIGH_MEMORY。快速、少量或不可休眠的页映射使用kmap_atomic,其他情况可使用kmap。

    vmalloc与page fault

          执行vmalloc(包括ioremap)时页表的设置是写在init_mm中的,这样当某个进程试图通过驱动访问该虚拟地址时必然发生page fault。do_page_fault函数判断出page fault发生在vmalloc region,于是从init_mm中将pgd/pud/pmd复制到当前进程,但pte是不复制的——内核地址空间的pte由所有进程共享。如果在将来的某个时刻映射解除,修改的也是init_mm的页表设置。后续对该地址的访问(假设仍为同一进程)在pgd、pud、pmd这三层仍然能够通过(进程有自己的pgd/pud/pmd),但到pte这一层就会发生page fault(init_mm已删除该pte)。类似的情况还有ioremap[注]。

         [注] 根据64位atom上安装32位Linux(kernel 2.6.32)的测试结果,页表是3级,各级位宽2/9/9/12,第一级将内存分为4个1G区间,进程pgd的第四条目总是指向init_mm的pmd。这样,只要ioremap已建立好,init_mm对应的pmd就一定有效,也就不会发生page fault。(这种情况下vmalloc的效果未确认)

    DirectIO

    对于DirectIO来说,数据传输直接发生在设备与应用程序缓冲区之间。因此,一般来说DirectIO往往和DMA配合使用。DirectIO的步骤大体如下:

    • 应用程序申请内存

          最简单的当然是malloc或new之类;如果你要求获得的内存对齐于页边界,可以用anonymous mmap——使用mmap函数分配虚拟内存,并将mmap函数的flags参数设定为MAP_ANONYMOUS,同时将fd设置为-1(对linux非必须,仅作兼容性考虑)。具体请man mmap。或者使用valloc函数。

    • Driver为其分配物理pages

          应用程序调用malloc或new或valloc得到的虚拟地址,在初始状态是没有物理页面和其对应的。我们如果要对其做DMA,需要为其分配物理内存,并将其lock,不允许换出。kernel提供get_user_pages函数做这些工作。如果需要的内存空间很大,get_user_pages的执行过程会比较长。

    • DMA

          一般来说get_user_pages得到的物理页面是不连续的,所以我们需要dma_map_sg。

    • 进一步说明

          因为DirectIO的数据传输直接发生在设备和应用程序缓冲区之间,所以一般来说Driver是不需要访问缓冲区内容的。如果Driver也需要访问缓冲区内容,需要通过kmap获得这些物理页面对应的内核虚拟地址。

          没有对应的put_user_pages函数;你需要自己做个循环调用page_cache_release。

    • IBM有篇关于DirectIO的文章写得不错。

    应用程序直接访问设备内存

    • 应用程序要访问设备内存(例如PCI的memory bar),有两种方法:一种是使用ioremap(或ioremap_nocache),将设备内存物理地址映射到内核虚拟地址,App使用ioctl或read/write通过驱动访问;
    • 另一种方法是使用mmap,让App直接访问设备内存。使用这种方法,App需要通过mmap系统调用开辟一块虚拟地址空间,并在驱动中实现file_operations中的mmap函数。App调用mmap时,fd参数设置为打开设备的fd。这块虚拟地址空间大小就是设备内存的大小。驱动在mmap的实现中,调用remap_pfn_range为App开辟的那块虚拟地址空间建立与设备物理内存的映射。和DirectIO相比,由于待映射的物理内存已存在(即设备内存),不需要分配并锁定page,所以速度很快。
    • 驱动不需要实现unmap。
    • 可能需要对映射得到的内存禁止cache。
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    if(rempa_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot)) return - EAGAIN;
    • 应用程序调用如下:
    void *p = mmap(0, 
                   DEV_MEM_LEN,          // 设备内存长度
                   prot, 
                   flags,
                   dev_fd,
                   DEV_MEM_PHY);         // 设备内存物理地址

          注意:如果你需要对mmap映射区进行写操作,在建立设备文件的时候将文件属性设置为rw。

    使用系统内存作为设备内存

    • 在嵌入式系统中,我们也许对系统有较大的控制权,可以使用mem=启动参数。这个时候我们可以保留一部分系统内存作为设备内存,设备直接将数据DMA到保留下的系统内存中(这块系统内存在mem=启动参数设定的内存大小之外,因此不会被Linux看到)。然后使用上文所说的“应用程序直接访问设备内存”的方法获取数据。

    内核物理页面的分配

          内核中有多种情况需要分配物理页面,比如处理page fault,比如driver需要临时或永久存储区,等等,一般来说都是通过kmalloc或辅助函数完成(核心数据结构是struct page)。管理内核物理页面的模块并不关心是谁要用物理页面,你要就给你(如果有),你还就收下。至于分配出去后怎么用,那是调用者的事。

          相对而言,应用程序中调用malloc仅仅是划出一段虚拟内存,但并不立即分配对应的物理内存空间。只有当有人读或写了这个页面,才会通过page fault为其分配物理页面,并且随时可能被换出。即,应用程序在用户空间所使用的物理页面都是在内核态分配的,纯用户态代码永远无法获得一块实际的物理内存:这是理所当然的事,因为物理页面的使用必须全部由内核所掌控。

    物理页面交换

          分配到的物理页面只有被挂在某个可换出的链表上,才会被页面换出代码扫描到。所以,如果你的驱动通过kmalloc获得了一些物理页面自用,它们是不可能被换出的——因为没有挂在某个可换出链表上。上文中通过get_user_pages得到的页面也是不会换出的。另一方面,发生page fault后分配到的物理页面是会被内核挂到某个可换出链表上的。

  • 相关阅读:
    统计nginx日志里访问次数最多的前十个IP
    while 格式化输出 运算符 字符编码
    Python 软件安装
    Python 基础
    Typora 基础的使用方法
    Django ORM (四) annotate,F,Q 查询
    Django 惰性机制
    Django ORM (三) 查询,删除,更新操作
    Django ORM (二) 增加操作
    Django ORM (一) 创建数据库和模型常用的字段类型参数及Field 重要参数介绍
  • 原文地址:https://www.cnblogs.com/byeyear/p/3215287.html
Copyright © 2011-2022 走看看