【Linux基础系列之】内存管理(2)-高端内存


(一)常用的内存分配函数及区别

malloc/calloc/realloc/alloca :

这都是用户空间的分配函数,返回的是虚拟地址空间地址;

  malloc调用形式为(类型*)malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址。malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。

  calloc调用形式为(类型*)calloc(n,size):在内存的动态存储区中分配n块长度为“size”字节的连续区域,返回首地址。calloc在动态分配完内存后,自动初始化该内存空间为零。

  realloc调用形式为(类型*)realloc(*ptr,size):将ptr内存大小增大到size。(也可以缩小,缩小的内容消失)。

  realloc调用形式为(类型*)realloc(*ptr,size):将malloc申请的ptr内存大小增大到size。(也可以缩小,缩小的内容消失)。

  alloca调用形式为(类型*)alloca(size):向栈申请内存,因此无需释放;

brk/mmap :

  brk系统调用,可以让进程的堆指针增长一定的大小,逻辑上消耗掉一块本进程的虚拟地址区间,malloc向OS获取的内存大小比较小时,将直接通过brk调用获取虚拟地址,结果是将本进程的brk指针推高。

  mmap系统调用,可以让进程的虚拟地址区间里切分出一块指定大小的虚拟地址区间vma_struct,并返回给用户态进程,被mmap映射返回的虚拟地址,逻辑上被消耗了,直到用户进程调用unmmap才回收回来。malloc向系统获取比较大的内存时,会通过mmap直接映射一块虚拟地址区间。mmap系统调用用处非常多,比如一个进程的所有动态库文件.so的加载,都需要通过mmap系统调用映射指定大小的虚拟地址区间,然后将.so代码动态映射到这些区域,以供进程其他部分代码访问;另外,多进程通讯,也可以使用mmap。

kmalloc/vmalloc:

这是分配的内核空间的内存;

  kmalloc和vmalloc是分配的是内核的内存,kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,内存只有在要被DMA访问的时候才需要物理上连续;kmalloc能分配的大小有限,vmalloc能分配的大小相对较大;vmalloc比kmalloc要慢;


(二)高端内存


  在32位的系统上,内核占有从第3GB~第4GB的线性地址空间,共1GB大小,内核将其中的前896MB与物理内存的0~896MB进行直接映射,即线性映射,也就是说存在一个线性关系:virtual address = physical address + PAGE_OFFSET,这里的PAGE_OFFSET为3G;因此内核将自己的最后128M的线性地址空间腾出来,用以完成对高端内存的暂时性映射。

这里写图片描述

  上图是将内存划分为各个区域,内核空间1G线性地址的布局,直接映射区为PAGE_OFFSET~PAGE_OFFSET+896MB,直接映射的物理地址末尾对应的线性地址保存在high_memory变量中。直接映射区后边有一个8MB的保护区,目的是用来”捕获”对内存的越界访问。

  然后是非连续内存区,范围从VMALLOC_START~VMALLOC_END,出于同样的原因,每个非连续内存区之间隔着4KB。永久内核映射区从PKMAP_BASE开始,大小为2MB(启动PAE)或4MB。后边是固定映射区,范围是FIXADDR_START~FIXADDR_TOP。

这里写图片描述

  线性地址与页表之间的映射是固定不可变的,而页表到具体的物理页框之间的映射是可以改变的,内核正是利用页表到物理页框之间的映射的可变性来为高端内存建立“临时”的映射;于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做永久内核映射、临时内核映射以及非连续内存分配;

(1) 永久内核映射

  永久内核映射允许内核建立高端页框到内核地址空间的长期映射,被分配额高端内存区叫做持久映射区,其起始地址放在pkmap_page_table,页表中的表项数由LAST_PKMAP宏产生,这样的话内核一次访问2MB(启动PAE:页表项为512,一个页表大小为:4k*512 = 2MB)或4MB的高端内存,pkmap_count数组包含LAST_PKMAPGE个计数器,pkmap_page_table页表中每一项都有一个。

如下为结构图:

这里写图片描述

计数器为0
  对应的页表项没有映射任何高端内存页框,并且是可用的。

计数器为1
  对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的TLB表项还未被刷新。

计数器为n
  相应的页表项映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。

  为了记录高端内存框和永久内核映射区包含的线性地址之间的关系,内核使用了page_address_htable散列表,该表包含一个page_address_map数据结构,用于为高端内存中的每一个页框进行映射,它包含一个指向页描述符的指针和分配给该页框的线性地址;

  通过alloc_page(__GFP_HIGHMEM)分配到了一个属于高端内存区域的page结构,先在page_address_htable查找页框并返回它的线性地址,然后调用kmap(struct page*page)来建立与永久内核映射区的映射,需要注意一点的是,当永久内核映射区没有空闲的页表项可供映射时,请求映射的进程会被阻塞,因此永久内核映射请求不能发生在中断和可延迟函数中。通过kunmap解除映射;

 37 void *kmap(struct page *page)
 38 {
 39     might_sleep();
 40     if (!PageHighMem(page))
 41         return page_address(page);
 42     return kmap_high(page);//高端内存调用kmap_high;
 43 }
 44 EXPORT_SYMBOL(kmap);
280 void *kmap_high(struct page *page)
281 {
282     unsigned long vaddr;
283     
284     /*
285      * For highmem pages, we can't trust "virtual" until
286      * after we have the lock.
287      */
288     lock_kmap();
289     vaddr = (unsigned long)page_address(page);
290     if (!vaddr)
291         vaddr = map_new_virtual(page);
292     pkmap_count[PKMAP_NR(vaddr)]++;
293     BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
294     unlock_kmap();
295     return (void*) vaddr;
296 } 

  首先获得需要映射的page对应的线性地址,从page_address_htable中进行查找,如果已经映射过了肯定不为空,如果没有映射过则执行map_new_virtual进行映射,这个函数通过扫描pkmap_count的所有计数器直到找到一个找到一个空值。

(2) 临时内核映射

  临时内核映射和永久内核映射相比,其最大的特点就是不会阻塞请求映射页框的进程,因此临时内核映射请求可以发生在中断和可延迟函数中。

当一个进程申请在某个窗口创建映射,即使这个窗口已经在之前就建立了映射,新的映射也会建立并且覆盖之前的映射,所以说这种映射机制是临时的,并且不会阻塞当前进程。

 55 void *kmap_atomic(struct page *page)
 56 {
 57     unsigned int idx;
 58     unsigned long vaddr;
 59     void *kmap;
 60     int type;
 61 
 62     pagefault_disable();
 63     if (!PageHighMem(page))
 64         return page_address(page);
 65 
 66 #ifdef CONFIG_DEBUG_HIGHMEM
 67     /*
 68      * There is no cache coherency issue when non VIVT, so force the
 69      * dedicated kmap usage for better debugging purposes in that case.
 70      */
 71     if (!cache_is_vivt())
 72         kmap = NULL;
 73     else
 74 #endif
 75         kmap = kmap_high_get(page);
 76     if (kmap)
 77         return kmap;
 78 
 79     type = kmap_atomic_idx_push();
 80 
 81     idx = type + KM_TYPE_NR * smp_processor_id();
 82     vaddr = __fix_to_virt(idx);
 83 #ifdef CONFIG_DEBUG_HIGHMEM
 84     /*
 85      * With debugging enabled, kunmap_atomic forces that entry to 0.
 86      * Make sure it was indeed properly unmapped.
 87      */
 88     BUG_ON(!pte_none(get_fixmap_pte(vaddr)));
 89 #endif
 95     set_fixmap_pte(idx, mk_pte(page, kmap_prot));
 96 
 97     return (void *)vaddr;
 98 }
 99 EXPORT_SYMBOL(kmap_atomic);


(二)非连续内存分配vmalloc

  非连续内存分配是指将物理地址不连续的页框映射到线性地址连续的线性地址空间,主要应用于大容量的内存分配。采用这种方式分配内存的主要优点是避免了外部碎片,而缺点是必须打乱内核页表,而且访问速度较连续分配的物理页框慢。

  非连续内存分配的线性地址空间是从VMALLOC_START到VMALLOC_END,共128M,每当内核要用vmalloc类的函数进行非连续内存分配,就会申请一个vm_struct结构来描述对应的vmalloc区vmalloc区为非连续物理内存分配区,首地址为VMALLOC_START,结束地址为VMALLOC_END;

由若干vmalloc子区域组成,每个vmalloc子区域间隔4KB,作为安全隔离区防止非法访问。用vm_struct表示每个vmalloc子区域,每次调用vmalloc()在内核成功申请一段连续虚拟内存后,都会对应一个vm_struct子区域;所有的vmalloc子区域组成一个链表,表头指针为 vmlist;

 29 struct vm_struct {
 30     struct vm_struct    *next;//链表的形式组织vm_struct;
 31     void            *addr;//addr指向该内存子区域首地址
 32     unsigned long       size;//大小;
 33     unsigned long       flags;
 34     struct page     **pages;//指针数组成员是struct page*类型指针,每个成员都关联一个映射到该虚拟内存区的物理页框;
 35     unsigned int        nr_pages;//关联的page总数;
 36     phys_addr_t     phys_addr;//通常为0,当使用ioremap()映射一个硬件设备的物理内存时才填充此字段;
 37     const void      *caller;//通常为0;
 38 };

vmalloc:

//kernel-3.18/mm/vmalloc.c:
1739 void *vmalloc(unsigned long size)
1740 {
1741     return __vmalloc_node_flags(size, NUMA_NO_NODE,
1742                     GFP_KERNEL | __GFP_HIGHMEM);//标志为highmem;
1743 }           
1744 EXPORT_SYMBOL(vmalloc);

__vmalloc_node_flags() -> __vmalloc_node() -> __vmalloc_node_range():

1651 void *__vmalloc_node_range(unsigned long size, unsigned long align,
1652             unsigned long start, unsigned long end, gfp_t gfp_mask,
1653             pgprot_t prot, int node, const void *caller)
1654 {
1655     struct vm_struct *area;
1656     void *addr;
1657     unsigned long real_size = size;
1658 
1659     size = PAGE_ALIGN(size);//页对齐,小于4k的size,分配4k大小;
1660     if (!size || (size >> PAGE_SHIFT) > totalram_pages)
1661         goto fail;
1662 
         //从MALLOC_START,VMALLOC_END范围内分配一段区域,填充到vm_struct结构体;
1663     area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED,
1664                   start, end, node, gfp_mask, caller);
1665     if (!area)
1666         goto fail;
1667    //为申请到的子内存区vm_struct 分配物理页框(physical 
        //frame),将不连续的physical frame分别映射到连续的vm_struct
        //子内存区中;
1668     addr = __vmalloc_area_node(area, gfp_mask, prot, node);//当__get_vm_area_node()创建完新的vm_struct子内存区后,需要通过__vmalloc_area_node()为这个字内存区域分配物理页;
1669     if (!addr)
1670         return NULL;
1671 
1677     clear_vm_uninitialized_flag(area);//已经初始化;
1678 
1684     kmemleak_alloc(addr, real_size, 2, gfp_mask);
1685 
1686     return addr;
1687 
1688 fail:
1689     warn_alloc_failed(gfp_mask, 0,
1690               "vmalloc: allocation failure: %lu bytes\n",
1691               real_size);
1692     return NULL;
1693 }

__get_vm_area_node():

1328 static struct vm_struct *__get_vm_area_node(unsigned long size,
1329         unsigned long align, unsigned long flags, unsigned long start,
1330         unsigned long end, int node, gfp_t gfp_mask, const void *caller)
1331 {
1332     struct vmap_area *va;
1333     struct vm_struct *area;
1334 
1335     BUG_ON(in_interrupt());
1336     if (flags & VM_IOREMAP)
1337         align = 1ul << clamp(fls(size), PAGE_SHIFT, IOREMAP_MAX_ORDER);
1338 
1339     size = PAGE_ALIGN(size);
1340     if (unlikely(!size))
1341         return NULL;
1342 
1343     area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node);//申请一个vm_strcut空间;
1344     if (unlikely(!area))
1345         return NULL;
1346    
1347     /*
1348      * We always allocate a guard page.
1349      */
1350     size += PAGE_SIZE;//分配 一个guard页,防止内存越界访问,保护作用;
1351 
1352     va = alloc_vmap_area(size, align, start, end, node, gfp_mask)//引入了红黑树来组织这些结构,将在vmalloc整个非连续内存区域范围内查找一块size大小的子内存区,该函数先遍历整个vmap_area_list链表,依次比对链表中每个vmap_area子区域大小,直到找到合适的内存区域为止;
1353     if (IS_ERR(va)) {
1354         kfree(area);
1355         return NULL;
1356     }
1357    
1358     setup_vmalloc_vm(area, va, flags, caller);//将查找到的vmap_area 加载到vm_struct子内存中,然后将这个子vm_struct子内存区插入到整个vmlist链表中;
1359 
1360     return area;
1361 }

__vmalloc_area_node():

1578 static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
1579                  pgprot_t prot, int node)
1580 {
1581     const int order = 0;
1582     struct page **pages;
1583     unsigned int nr_pages, array_size, i;
1584     const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
1585     const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN;
1586 
1587     nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;/*得到要映射的页框数*/ 
1588     array_size = (nr_pages * sizeof(struct page *));
1589 
1590     area->nr_pages = nr_pages;
1591     /* Please note that the recursion is strictly bounded. */
1592     if (array_size > PAGE_SIZE) {/*如果array_size大于一个页框的大小,则递归调用__vmalloc_node来为pages分配空间*/
1593         pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM,
1594                 PAGE_KERNEL, node, area->caller);
1595         area->flags |= VM_VPAGES;
1596     } else {
1597         pages = kmalloc_node(array_size, nested_gfp, node);
1598     }
1599     area->pages = pages;
1600     if (!area->pages) {
1601         remove_vm_area(area->addr);
1602         kfree(area);
1603         return NULL;
1604     }
1605     /*为area中的每一个page分配一个物理页框*/
        //如果node<0,说明未指定物理内存所在节点,使用alloc_page()分配一个页框,否则通过alloc_page_node()在指定的节点上分配物理页框,然后把刚刚分配的page装载到pages[i]中.
1606     for (i = 0; i < area->nr_pages; i++) {
1607         struct page *page;
1608 
1609         if (node == NUMA_NO_NODE)
1610             page = alloc_page(alloc_mask);
1611         else
1612             page = alloc_pages_node(node, alloc_mask, order);
1613 
1614         if (unlikely(!page)) {
1615             /* Successfully allocated i pages, free them in __vunmap() */
1616             area->nr_pages = i;
1617             goto fail;
1618         }
1619         area->pages[i] = page;
1620         if (gfp_mask & __GFP_WAIT)
1621             cond_resched();
1622     }
1623 
1624     if (map_vm_area(area, prot, pages)) //完成pages数组中每个页框到连续vmalloc子区的映射关系. 
1625         goto fail;
1626     return area->addr;
1627 
1628 fail:
1629     warn_alloc_failed(gfp_mask, order,
1630               "vmalloc: allocation failure, allocated %ld of %ld bytes\n",
1631               (area->nr_pages*PAGE_SIZE), area->size);
1632     vfree(area->addr);
1633     return NULL;
1634 }

实现分为3部分:

  1. 首先, get_vm_area在vmalloc地址空间中找到一个适当的区域;
  2. 接下来从物理内存分配各个页,内存取自伙伴系统;
  3. 最后将这些页连续地映射到vmalloc区域中, 分配虚拟内存的工作就完成了;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值