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

时间:2022-11-24 08:14:06


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

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的线性地址空间腾出来,用以完成对高端内存的暂时性映射。

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

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

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

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

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

(1) 永久内核映射

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

如下为结构图:

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

计数器为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区域中, 分配虚拟内存的工作就完成了;