深入理解Linux虚拟内存管理(五)(下)

简介: 深入理解Linux虚拟内存管理(五)

深入理解Linux虚拟内存管理(五)(上):https://developer.aliyun.com/article/1597790

3、释放页面

(1)__free_pages

    这个函数的调用图如图 6.4 所示。很容易误解的是,alloc_pages() 对应的函数并不是 free_pages(),而是 __free_pages()free_pages() 是一个以地址为参数的辅助函数。

// mm/page_alloc.c
// 参数是我们将释放的 page 和块的幂次。
void __free_pages(struct page *page, unsigned int order)
{
  // 有效性检查。PageReserved()表示页面由引导内存分配器保留。put_page_testzero() 仅
  // 是一个对 atomic_dec_and_test() 的宏封装。它将使用计数减 1,保证它为 0。
  if (!PageReserved(page) && put_page_testzero(page))
  // 调用函数来完成所有的复杂工作。
    __free_pages_ok(page, order);
}
(a)⇐ mm.h
// include/linux/mm.h
#define put_page_testzero(p)  atomic_dec_and_test(&(p)->count)

#define PageLRU(page)   test_bit(PG_lru, &(page)->flags)
#define ClearPageUptodate(page) clear_bit(PG_uptodate, &(page)->flags)
#define PageDirty(page)   test_bit(PG_dirty, &(page)->flags)
#define SetPageDirty(page)  set_bit(PG_dirty, &(page)->flags)
#define ClearPageDirty(page)  clear_bit(PG_dirty, &(page)->flags)
#define PageLocked(page)  test_bit(PG_locked, &(page)->flags)
#define LockPage(page)    set_bit(PG_locked, &(page)->flags)
#define TryLockPage(page) test_and_set_bit(PG_locked, &(page)->flags)
#define PageChecked(page) test_bit(PG_checked, &(page)->flags)
#define SetPageChecked(page)  set_bit(PG_checked, &(page)->flags)
#define PageLaunder(page) test_bit(PG_launder, &(page)->flags)
#define SetPageLaunder(page)  set_bit(PG_launder, &(page)->flags)
#define ClearPageLaunder(page)  clear_bit(PG_launder, &(page)->flags)


/*
 * The first mb is necessary to safely close the critical section opened by the
 * TryLockPage(), the second mb is necessary to enforce ordering between
 * the clear_bit and the read of the waitqueue (to avoid SMP races with a
 * parallel wait_on_page).
 */
#define PageError(page)   test_bit(PG_error, &(page)->flags)
#define SetPageError(page)  set_bit(PG_error, &(page)->flags)
#define ClearPageError(page)  clear_bit(PG_error, &(page)->flags)
#define PageReferenced(page)  test_bit(PG_referenced, &(page)->flags)
#define SetPageReferenced(page) set_bit(PG_referenced, &(page)->flags)
#define ClearPageReferenced(page) clear_bit(PG_referenced, &(page)->flags)
#define PageTestandClearReferenced(page)  test_and_clear_bit(PG_referenced, &(page)->flags)
#define PageSlab(page)    test_bit(PG_slab, &(page)->flags)
#define PageSetSlab(page) set_bit(PG_slab, &(page)->flags)
#define PageClearSlab(page) clear_bit(PG_slab, &(page)->flags)
#define PageReserved(page)  test_bit(PG_reserved, &(page)->flags)

#define PageActive(page)  test_bit(PG_active, &(page)->flags)
#define SetPageActive(page) set_bit(PG_active, &(page)->flags)
#define ClearPageActive(page) clear_bit(PG_active, &(page)->flags)

#define PageLRU(page)   test_bit(PG_lru, &(page)->flags)
#define TestSetPageLRU(page)  test_and_set_bit(PG_lru, &(page)->flags)
#define TestClearPageLRU(page)  test_and_clear_bit(PG_lru, &(page)->flags)

#ifdef CONFIG_HIGHMEM
#define PageHighMem(page)   test_bit(PG_highmem, &(page)->flags)
#else
#define PageHighMem(page)   0 /* needed to optimize away at compile time */
#endif

#define SetPageReserved(page)   set_bit(PG_reserved, &(page)->flags)
#define ClearPageReserved(page)   clear_bit(PG_reserved, &(page)->flags)

(2)__free_pages_ok

    这个函数将完成实际的释放页面工作,并在可能的情况下合并伙伴。

// mm/page_alloc.c
/*
 * Freeing function for a buddy system allocator.
 * Contrary to prior comments, this is *NOT* hairy, and there
 * is no reason for anyone not to understand it.
 *
 * The concept of a buddy system is to maintain direct-mapped tables
 * (containing bit values) for memory blocks of various "orders".
 * The bottom level table contains the map for the smallest allocatable
 * units of memory (here, pages), and each level above it describes
 * pairs of units from the levels below, hence, "buddies".
 * At a high level, all that happens here is marking the table entry
 * at the bottom level available, and propagating the changes upward
 * as necessary, plus some accounting needed to play nicely with other
 * parts of the VM system.
 * At each level, we keep one bit for each pair of blocks, which
 * is set to 1 iff only one of the pair is allocated.  So when we
 * are allocating or freeing one, we can derive the state of the
 * other.  That is, if we allocate a small block, and both were   
 * free, the remainder of the region must be split into blocks.   
 * If a block is freed, and its buddy is also free, then this
 * triggers coalescing into a block of larger size.            
 *
 * -- wli
 */

static void FASTCALL(__free_pages_ok (struct page *page, unsigned int order));
// 参数是待释放页面块的开始和待释放页面的幂次数。
static void __free_pages_ok (struct page *page, unsigned int order)
{
  unsigned long index, page_idx, mask, flags;
  free_area_t *area;
  struct page *base;
  zone_t *zone;

  /*
   * Yes, think what happens when other parts of the kernel take 
   * a reference to a page in order to pin it for io. -ben
   */
  // 在标志 I/O 时,LRU 中的脏页面将仍然设置有 LRU 位。一旦 I/O 完成,它就会
  // 被释放,所以现在必须从 LRU 链表中移除。 
  if (PageLRU(page)) {
    if (unlikely(in_interrupt()))
      BUG();
    // mm/swap.c      
    lru_cache_del(page);
  }

  // 有效性检查。
  if (page->buffers)
    BUG();
  if (page->mapping)
    BUG();
  if (!VALID_PAGE(page))
    BUG();
  if (PageLocked(page))
    BUG();
  if (PageActive(page))
    BUG();
  // 由于页面现在空闲,没有在使用中,这个标志位表示页面已经被引用,而且是必须被
  // 清洗的脏页面。    
  page->flags &= ~((1<<PG_referenced) | (1<<PG_dirty));

  // 如果设置了该标志,这些已经释放了的页面将保存在释放它们的进程中。如果
  // 调用者是它自己在释放页面,而不是等待 kswapd 来释放,在分配页面时这里调用 
  // balance_classzone()。
  if (current->flags & PF_FREE_PAGES)
    goto local_freelist;
 back_local_freelist:
  // 页面所属管理区用页面标志位编码。宏 page_zone() 返回该管理区。
  zone = page_zone(page);
  // 有关掩码计算的讨论在随书附带的文档中。它基本上与伙伴系统的地址计算有关。
  mask = (~0UL) << order;
  // base 是这个 zone_mem_map 的起始端。对伙伴计算而言,它与地址 0 有关,这样地址
  // 就是 2 的幂。
  base = zone->zone_mem_map;
  // page_idx 视 zone_mem_map 为一个由页面组成的数组,这是映射图中的页索引。
  page_idx = page - base;
  // 如果索引不是 2 的幂,则肯定是某个地方出现严重错误,伙伴的计算将不会进行。
  if (page_idx & ~mask)
    BUG();
  // index 是 free_area->map 的位索引。   
  index = page_idx >> (1 + order);
  // area 是存储空闲链表和映射图的区域,其中映射图是释放页面的有序块。
  area = zone->free_area + order;

  // 管理区将改变,所以这里上锁。由于中断处理程序可能在这个路径上分配页面,所以
  // 这个锁是一个中断安全的锁。
  spin_lock_irqsave(&zone->lock, flags);
  // mask 计算的另一个副作用是 -mask 是待释放的页面数。测试结果却是如此
  zone->free_pages -= mask;

// 分配器将不断地试着合并块直到不能再合并,或者到达了可以合并的最高次。
// 对合并的每一次序块,mask 都将调整。当到达了可以合并的最高次的时候,while 循环将为 0
// 并退出。
//
  while (mask + (1 << (MAX_ORDER-1))) {
    struct page *buddy1, *buddy2;
  // 如果发生什么意外,mask 被损坏,这个检查将保证 free_area 不会超过末端读。
    if (area >= zone->free_area + MAX_ORDER)
      BUG();
  // 表示伙伴对的位置位。如果以前该位是 0,则两个伙伴都在使用中。因此,在这个伙
  // 伴释放后,另外一个正在使用中,所以不能合并。     
    if (!__test_and_change_bit(index, area->map))
      /*
       * the buddy page is still allocated.
       */
      break;
    /*
     * Move the buddy up one level.
     * This code is taking advantage of the identity:
     *  -mask = 1+~mask
     */
  // 这两个地址的计算在第 6 章讨论。     
    buddy1 = base + (page_idx ^ -mask);
    buddy2 = base + page_idx;
  // 有效性检查保证页面在正确的 zone_mem_map 中,而且实际上属于这个管理区。   
    if (BAD_RANGE(zone,buddy1))
      BUG();
    if (BAD_RANGE(zone,buddy2))
      BUG();

  // 伙伴已经被释放,所以这里将其从包含它的链表中移除。
    list_del(&buddy1->list);
// 准备检查待合并的高次伙伴。  
//
  // 将掩码左移 1 位到次 2^(k+1)  
    mask <<= 1;
  // area 是一个数组内指针,所以 area++ 移到下一个下标。   
    area++;
  // 高次位图的索引。   
    index >>= 1;
  // 待合并 zone_mem_map 中的页面索引。   
    page_idx &= mask;
  }

  // 由于尽可能多地合并已经完成,而且释放了一个新页面块,所以这里将其加入到该次
  // 的 free_list 中。
  list_add(&(base + page_idx)->list, &area->free_list);
  // 对管理区的改变已经完成,所以这里释放锁并返回。
  spin_unlock_irqrestore(&zone->lock, flags);
  return;

// 这是在页面没有释放到主页面池时的代码路径,它将页面保留给释放的进程。
 local_freelist:
  // 如果进程已经有保留页面,则这里不允许再保留页面,所以返回。这里很不寻
  // 常,因为 balance_classzone() 假设多于一个页面块可能从该链表上返回。这很有可能能过虑了,
  // 但是如果释放的第一个页面是同一次的,而 balance_classzone()请求管理区,则这里仍然可以
  // 工作。
  if (current->nr_local_pages)
    goto back_local_freelist;
  // 一个中断没有进程上下文,所以它必须象平常一样释放。现在还不明白这里的
  // 中断如何结束。这里的检查似乎是假的,而且不可能为真的。    
  if (in_interrupt())
    goto back_local_freelist;   

  // 将页面块加入到链表中处理 local_pages。
  list_add(&page->list, &current->local_pages);
  // 记录分配的次数,从而方便后面的释放操作。
  page->index = order;
  // 将 nr_local_pages 使用计数加 1。
  current->nr_local_pages++;
}


(a)⇐ zone_t

    每个管理区由一个 zone_t 描述,具体可参考 ⇒ 2.2 管理区2.6 页面映射到管理区

// include/linux/mm.h
extern struct zone_struct *zone_table[];

static inline zone_t *page_zone(struct page *page)
{
  return zone_table[page->flags >> ZONE_SHIFT];
}
(b)⇐ 概览

传送门 6.3 释放页面

(c)⇐ free_area_init_core

    这个函数负责初始化所有的区域,并在节点中分配它们的局部 Imem_map(类型 struct page *)。并初始化 pg_data_t 中字段 node_mem_mapnode_sizenode_start_paddrnode_start_mapnrnr_zonesnode_zones 以及全局 zone_table

传送门 free_area_init_core

(d)⇐ 伙伴算法

一文看懂物理内存分配算法(伙伴系统)

伙伴算法原理简介


4、释放辅助函数

    这些函数与页面分配的辅助函数非常相似,因为它们也不完成 “实际” 的工作,它们依赖于 __free_pages() 函数来完成实际的释放。

(1)free_pages

// mm/page_alloc.c
// 这个函数以一个地址,而不是以一个页面作为参数来进行释放操作。
void free_pages(unsigned long addr, unsigned int order)
{
  if (addr != 0)
  // 宏 virt_to_page() 返回 addr 的 struct page。
    __free_pages(virt_to_page(addr), order);
}
(a)⇐ page.h
// include/asm/page.h
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))

传送门 __free_pages

(2)__free_page

// include/linux/mm.h
// 这个宏仅调用函数 __free_pages() ,参数为 0 幂次和一个页面。
#define __free_page(page) __free_pages((page), 0)


传送门 __free_pages

(3)free_page

// include/linux/mm.h
// 这个小宏仅调用 free_pages() 。这个宏与 __free_page() 的主要区别在于这个函数以一个
// 虚拟地址为参数,而 __free_page() 以一个 struct page 为参数
#define free_page(addr) free_pages((addr),0)

传送门 free_pages


二、不连续内存分配

1、分配一块非连续的区域

(1)vmalloc

    这个函数的调用图如图 7.2 所示。下面宏之间的差别仅在于使用的 GFP_ 标志位 (见 6.4 节) 不同。size 参数是由 __vmalloc() 对齐的分页。


传送门 描述页目录

// include/linux/vmalloc.h

/*
 *  Allocate any pages
 */
 // 这个标志位表明在需要时使用 ZONE_NORMAL 或 ZONE_HIGHMEM。
static inline void * vmalloc (unsigned long size)
{
  return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}

/*
 *  Allocate ISA addressable pages for broke crap
 */
// 这个标志位表明仅从 ZONE_DMA 中分配。
static inline void * vmalloc_dma (unsigned long size)
{
  return __vmalloc(size, GFP_KERNEL|GFP_DMA, PAGE_KERNEL);
}

/*
 *  vmalloc 32bit PA addressable pages - eg for PCI 32bit devices
 */
// 仅 ZONE_NORMAL 中的物理页面会被分配。 
static inline void * vmalloc_32(unsigned long size)
{
  return __vmalloc(size, GFP_KERNEL, PAGE_KERNEL);
}
(a)⇐ PAGE_KERNEL
// include/asm-x86_64/pgtable.h
#define PAGE_KERNEL __PTE_SUPP(__PAGE_KERNEL|_PAGE_GLOBAL)
(b)⇐ 非连续内存分配

传送门 第7章 非连续内存分配

(2)__vmalloc

    这个函数有 3 个任务。将请求大小转化为页面数,调用 get_vm_area() 找到该请求的一个区域,使用 vmalloc_area_pages() 分配页面的 PTE

// mm/vmalloc.c
// 参数是要分配的大小,分配时使用的 GFP_ 标志位和对 PTE 采用何种保护手段。
void * __vmalloc (unsigned long size, int gfp_mask, pgprot_t prot)
{
  void * addr;
  struct vm_struct *area;

  // 将 size 与页面大小对齐。
  size = PAGE_ALIGN(size);
  // 有效性检查,保证大小不是 0,请求的大小不会大于已请求的物理页面数。
  if (!size || (size >> PAGE_SHIFT) > num_physpages)
    return NULL;
  // 利用 get_vm_area() 找到存放分配虚拟地址空间中的一块区域。   
  area = get_vm_area(size, VM_ALLOC);
  if (!area)
    return NULL;
  // addr 字段已经由 get_vm_area() 填充。   
  addr = area->addr;
  // 利用 __vmalloc_area_pages() 分配所需的 PTE 表项。如果失败,则返回一个非 0
  // 值-ENOMEM。
  if (__vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask,
         prot, NULL)) {
  // 如果分配失败,则这里释放所有的 PTE、页面和区域描述符。         
    vfree(addr);
    return NULL;
  }
  // 返回已分配的区域地址。
  return addr;
}
(a)⇐ PAGE_ALIGN
// include/asm-i386/page.h
#define PAGE_ALIGN(addr)  (((addr)+PAGE_SIZE-1)&PAGE_MASK)

#define VMALLOC_VMADDR(x) ((unsigned long)(x))

// ==================================================================
// include/linux/vmalloc.h
/* bits in vm_struct->flags */
#define VM_IOREMAP  0x00000001  /* ioremap() and friends */
#define VM_ALLOC  0x00000002  /* vmalloc() */


(b)⇒ get_vm_area

传送门 get_vm_area

(c)⇒ __vmalloc_area_pages

传送门 __vmalloc_area_pages

(3)get_vm_area

    为了给 vm_struct 分配一块区域,使用 kmalloc() 请求 slab 分配器提供所需的内存。然后线性查找 vm_struct 链表,找到一块满足请求的足够大的区域,且在区域尾部包括一个页面填充。

// mm/vmalloc.c
// 参数是必须是页面大小的倍数的请求区域的大小、区域标志位、VM_ALLOC 或 VM_IOREMAP。
struct vm_struct * get_vm_area(unsigned long size, unsigned long flags)
{
  unsigned long addr, next;
  struct vm_struct **p, *tmp, *area;

  // 允许为 vm_struct 描述符结构分配空间。
  area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL);
  if (!area)
    return NULL;

  // 填充请求,在区域间留出一页空隙,这里是保护防止覆写。
  size += PAGE_SIZE;
  // 保证 size 在溢出填充时不会为 0,如果中间出了什么错,这里将释放刚才分配的
  // area,并返回 NULL。
  if (!size) {
    kfree (area);
    return NULL;
  }

  // 在 vmalloc 地址空间的首部开始搜索。
  addr = VMALLOC_START;
  // 上锁链表。
  write_lock(&vmlist_lock);
// 遍历链表,寻找一块对请求足够大的区域。  
// vmlist = area0, area0->next = area1, area1->next = area2, area2->next = NULL
  for (p = &vmlist; (tmp = *p) ; p = &tmp->next) {
  // 检查保证没有到达可寻址区域范围的末端。
    if ((size + addr) < addr)
      goto out;
  // 如果所请求的区域不能在当前地址和下一个地址之间取出,查找结束。      
    if (size + addr <= (unsigned long) tmp->addr)
      break;
  // 保证地址不会超过 vmalloc 地址空间的末端。      
    next = tmp->size + (unsigned long) tmp->addr;
    if (next > addr) 
      addr = next;
    if (addr > VMALLOC_END-size)
      goto out;
  }
  //  复制区域信息。
  area->flags = flags;
  area->addr = (void *)addr;
  area->size = size;
  // 将新区域链入链表中。
  area->next = *p;
  *p = area;
  // 解锁链表并返回。
  write_unlock(&vmlist_lock);
  return area;

// 如果不能满足请求则到达这个标记。
out:
  // 解锁链表。
  write_unlock(&vmlist_lock);
  // 释放区域描述符使用的内存并返回。
  kfree(area);
  return NULL;
}
(a)⇒ kmalloc

传送门 kmalloc ?

(b)⇐ pgtable.h
// include/asm-i386/pgtable.h
/* Just any arbitrary offset to the start of the vmalloc VM area: the
 * current 8MB value just means that there will be a 8MB "hole" after the
 * physical memory until the kernel virtual memory starts.  That means that
 * any out-of-bounds memory accesses will hopefully be caught.
 * The vmalloc() routines leaves a hole of 4kB between each vmalloced
 * area for the same reason. ;)
 */
#define VMALLOC_OFFSET  (8*1024*1024)
#define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & \
            ~(VMALLOC_OFFSET-1))
#define VMALLOC_VMADDR(x) ((unsigned long)(x))
#if CONFIG_HIGHMEM
# define VMALLOC_END  (PKMAP_BASE-2*PAGE_SIZE)
#else
# define VMALLOC_END  (FIXADDR_START-2*PAGE_SIZE)
#endif

(4)vmalloc_area_pages

    这个函数只是对 __vmalloc_area_pages() 的封装。这个函数为了与旧的内核兼容才存在。改变名字是为了反映新函数 __vmalloc_area_pages() 可以使用一个页面数组来插入到页表中。

// mm/vmalloc.c
int vmalloc_area_pages(unsigned long address, unsigned long size,
           int gfp_mask, pgprot_t prot)
{
  // 以通用的参数调用 __vmalloc_area_pages() 。pages 数组传递为 NULL,因为后面将
  // 在需要时分配页面。
  return __vmalloc_area_pages(address, size, gfp_mask, prot, NULL);
}

(5)__vmalloc_area_pages

    这是标准的页面遍历函数的开始部分。这个顶层函数将遍历在一定地址范围内的所有 PGD。对每个 PGD,它将调用 pmd_alloc() 来分配一个 PMD 目录,然后调用 alloc_area_pmd() 分配目录。

    即按照虚拟地址的范围,建立相应的 pmdpte,以及 pte 指向的物理页。

// mm/vmalloc.c
// address 是 PMD 分配的起始地址。
// size 是区域的大小。
// gfp_mask 是 alloc_pages() 的所有 GFP_ 标志位。
// prot 是对 PTE 表项的保护方式。
// pages 是一个用于插入的页面数组,它不是一次性调用 alloc_ara_pte() 分配的。
//       只有 vmap()接口使用数组传递。
static inline int __vmalloc_area_pages (unsigned long address,
          unsigned long size,
          int gfp_mask,
          pgprot_t prot,
          struct page ***pages)
{
  pgd_t * dir;
  // 末尾地址是起始地址加上大小。
  unsigned long end = address + size;
  int ret;

  // 获取起始地址的 PGD 表项。
  dir = pgd_offset_k(address);
  // 上锁内核引用页表。
  spin_lock(&init_mm.page_table_lock);
// 对地址范围内的每个 PGD,这里分配一个 PMD 目录,然后调 alloc_area_pmd() 
  do {
    pmd_t *pmd;
  // 分配一个 PMD 目录。x86 两级页表 pgd 即为 pmd
    pmd = pmd_alloc(&init_mm, dir, address);
    ret = -ENOMEM;
    if (!pmd)
      break;

    ret = -ENOMEM;
  // 调用 alloc_area_pmd() ,它将为 PMD 中每个 PTE 槽分配一个 PTE。    
    if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot, pages))
      break;
  // address 变成下一个 PGD 表项的基地址。
    address = (address + PGDIR_SIZE) & PGDIR_MASK;
  // 将 dir 移至下一个 PGD 表项。    
    dir++;

    ret = 0;
  } while (address && (address < end));
  // 释放内核页表锁。
  spin_unlock(&init_mm.page_table_lock);
  // flush_cache_all() 将清理所有的 CPU 高速缓存。这是必要的,因为内核页表已经改变了。
  flush_cache_all();
  // 返回成功。
  return ret;
}
(a)⇐ pgtable.h
// include/asm-i386/pgtable.h
#define page_pte(page) page_pte_prot(page, __pgprot(0))

#define pmd_page(pmd) \
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))

/* to find an entry in a page-table-directory. */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))

#define __pgd_offset(address) pgd_index(address)

#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))

/* to find an entry in a kernel page-table-directory */
#define pgd_offset_k(address) pgd_offset(&init_mm, address)

#define __pmd_offset(address) \
    (((address) >> PMD_SHIFT) & (PTRS_PER_PMD-1))

/* Find an entry in the third-level page table.. */
#define __pte_offset(address) \
    ((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + \
      __pte_offset(address))
(b)⇒ pmd_alloc

传送门 pmd_alloc

(6)alloc_area_pmd

  这是在地址范围内为分配 PTE 表项而进行的标准页表遍历的第 2 个阶段。对 PGD 中给定地址的每个 PMD , pte_alloc() 将创建 PTE 目录,然后调用 alloc_area_pte() 分配物理页面。

// mm/vmalloc.c
//  pmd 是需要分配的 PMD。
//  address 是开始的起始地址。
//  size 是为 PMD 分配的区域大小。
//  gfp+mask 是给 alloc_pages() 的 GFP_flag 
//  prot 是对 PTE 表项的保护方式。
//  pages 是一个可选的页面数组,它用于替代单独地一次一页的分配。 
static inline int alloc_area_pmd(pmd_t * pmd, unsigned long address,
      unsigned long size, int gfp_mask,
      pgprot_t prot, struct page ***pages)
{
  unsigned long end;

  // 将PGD的起始地址页对齐。
  address &= ~PGDIR_MASK;
  // 计算分配的末端,或者PGD的末端,无论哪个先出现。
  end = address + size;
  if (end > PGDIR_SIZE)
    end = PGDIR_SIZE;
// 对给定地址范围内的每个PMD,这里分配一个PTE目录,然后调用 alloc_area_pte()   
  do {
  // 分配PTE目录。 
    pte_t * pte = pte_alloc(&init_mm, pmd, address);
    if (!pte)
      return -ENOMEM;
  // 调用alloc_area_pte,如果页面数组还没有提供pages,将分配物理页。      
    if (alloc_area_pte(pte, address, end - address,
          gfp_mask, prot, pages))
      return -ENOMEM;
  // address变成下一个PMD表项的基地址。     
    address = (address + PMD_SIZE) & PMD_MASK;
  // 将pmd移到下一个PMD表项。    
    pmd++;
  } while (address < end);
  // 返回成功。
  return 0;
}

(7)alloc_area_pte

    这是页表遍历的最后一个阶段。对给定 PTE 目录中和地址范围的各个 PTE ,分配一页以及相关的 PTE

// mm/vmalloc.c

static inline int alloc_area_pte (pte_t * pte, unsigned long address,
      unsigned long size, int gfp_mask,
      pgprot_t prot, struct page ***pages)
{
  unsigned long end;
  // 将地址与PMD目录对齐。
  address &= ~PMD_MASK;
  end = address + size;
  // 末尾地址是请求的末尾或者目录的末尾,无论哪个先出现。
  if (end > PMD_SIZE)
    end = PMD_SIZE;
// 遍历该页面中的每个PTE。如果提供了 pages 数组,则使用从它里面的页面来
// 构建表格。否则,分别分配每个。    
  do {
    struct page * page;
// 如果没有提供一个pages 数组,则在这里解锁内核引用页表,利用 alloc_page
// 分配一个页面,然后重新获取自旋锁。
    if (!pages) {
      spin_unlock(&init_mm.page_table_lock);
      page = alloc_page(gfp_mask);
      spin_lock(&init_mm.page_table_lock);
    } else {
// 如果不是这样,则从数组中取出一页,由于它将被插入到引用页表中,所以增加它的引用计数。   
      page = (**pages);
      (*pages)++;

      /* Add a reference to the page so we can free later */
      if (page)
        atomic_inc(&page->count);

    }
  // 如果PTE正在使用中,则意味着在某个地方vmalloc区域中的区域重叠了。    
    if (!pte_none(*pte))
      printk(KERN_ERR "alloc_area_pte: page already exists\n");
  // 如果没有物理页面可用则返回失败。     
    if (!page)
      return -ENOMEM;
  // 在PTE中设置page的相应保护位(pmt)。      
    set_pte(pte, mk_pte(page, prot));
  // address变成下一个PTE的地址。    
    address += PAGE_SIZE;
  // 移到下一个PTE。    
    pte++;
  } while (address < end);
  // 返回成功
  return 0;
}
(a)⇐ pte_none
// include/asm-i386/pgtable-2level.h
#define pte_none(x)   (!(x).pte_low)

(8)vmap

    这个函数允许由调用者提供的 pages 数组插入到 vmalloc 地址空间,这个在 2.4.22 中没有使用,我怀疑是从 2.6.x 起,linux 为了兼容某种声音子系统内核而设立的。

// mm/vmalloc.c
// pages是调用者提供的待插入的页面数组。
// count是数组中的页面数。
// flags是用于vm_struct的标志位。
// prot是设置PTE的保护位。
void * vmap(struct page **pages, int count,
      unsigned long flags, pgprot_t prot)
{
  void * addr;
  struct vm_struct *area;
  // 在数组的基础上计算要创建的区域的大小(以字节计)
  unsigned long size = count << PAGE_SHIFT;
  // 保证区域大小不会超过限制。
  if (!size || size > (max_mapnr << PAGE_SHIFT))
    return NULL;
  // 使用get_vm_area()来找到一个足够大的映射区域。如果没有找到,则返回NULL    
  area = get_vm_area(size, flags);
  if (!area) {
    return NULL;
  }
  // 获取区域的虚拟地址。
  addr = area->addr;
  // 利用__vmalloc_area_pages()向页表中插入数组。
  if (__vmalloc_area_pages(VMALLOC_VMADDR(addr), size, 0,
         prot, &pages)) {
  // 如果插入失败,则这里释放区域,并返回NULL。       
    vfree(addr);
    return NULL;
  }
  // 返回新映射区域的虚拟地址
  return addr;
}
(a)⇒ get_vm_area

传送门 get_vm_area

(b)⇒ __vmalloc_area_pages

传送门 __vmalloc_area_pages

2、释放一块非连续区域

(1)vfree

    这个函数的调用图如图 7.4 所示。这是一个负责释放非连续内存区域的高层函数。它进行简单的有效性检查,然后找到请求 addr 的 vm_struct,一旦找到,则调 vmfree_area_pages()

// mm/vmalloc.c
// 参数为 get_vm_area() 返回给 vmalloc() 或 ioremap() 的地址。
void vfree(void * addr)
{
  struct vm_struct **p, *tmp;
  // 忽略NULL地址。
  if (!addr)
    return;
  // 检查以确定地址是否页对齐,同时也是对地址是否有效的一次快速检查。   
  if ((PAGE_SIZE-1) & (unsigned long) addr) {
    printk(KERN_ERR "Trying to vfree() bad address (%p)\n", addr);
    return;
  }
  // 获取vmlist的写锁。
  write_lock(&vmlist_lock);
  // 遍历vmlist,查找addr的正确的vm_struct。
  for (p = &vmlist ; (tmp = *p) ; p = &tmp->next) {
  // 如果这是一个正确的地址.则…
    if (tmp->addr == addr) {
  // 从vmlist链表中移除这个区域。    
      *p = tmp->next;
  // 释放所有与地址范围有关的页面.      
      vmfree_area_pages(VMALLOC_VMADDR(tmp->addr), tmp->size);
  // 释放 vmlist 锁。     
      write_unlock(&vmlist_lock);
  // 释放用于vm_struct的内存并返回      
      kfree(tmp);
      return;
    }
  }
  // 没有找到vm_struct。这里释放锁,然后答应有关失败释放的信息。
  write_unlock(&vmlist_lock);
  printk(KERN_ERR "Trying to vfree() nonexistent vm area (%p)\n", addr);
}

(2)vmfree_area_pages

    这是遍历页表中与某个地址范围有关的所有页面和 PTE 的第一阶段。它负责遍历相关的 PGD 以及刷新 TLB

// mm/vmalloc.c
// 参数是起始的address和区域的size。
void vmfree_area_pages(unsigned long address, unsigned long size)
{
  // 地址空间末端是起始地址加上大小。
  pgd_t * dir;
  unsigned long end = address + size;

  // 获取地址范围的第1个PGD。
  dir = pgd_offset_k(address);
  // 刷新高速缓存CPU,这样就不会在将被删除的页面上发生高速缓存命中。在许多体
  // 系结构中(包括x86),这是一个空操作。
  flush_cache_all();
  // 调用free_area_pmd()完成遍历页表的第二阶段。
  do {
    free_area_pmd(dir, address, end - address);
  // address变成下一个PGD的开始地址。    
    address = (address + PGDIR_SIZE) & PGDIR_MASK;
  // 移到下一个PGD。    
    dir++;
  } while (address && (address < end));
  // 由于页表现在已经改变了,所以刷新TLB。
  flush_tlb_all();
}

(3)free_area_pmd

    这是页表遍历的第 2 个阶段。对这个目录中的每个 PMD,它调用 free_area_pte() 来释放页面和 PTE

// mm/vmalloc.c
// 参数是进入的PGD、起始地址和区域长度。
static inline void free_area_pmd(pgd_t * dir, unsigned long address, unsigned long size)
{
  pmd_t * pmd;
  unsigned long end;

  // 如果没有PGD,则返回。在分配失败过程中调用vfree()后这可能发生。
  if (pgd_none(*dir))
    return;
  // 如果没有表项,则PGD可能已损坏,这里将其标记为只读或者访问过或者脏的。   
  if (pgd_bad(*dir)) {
    pgd_ERROR(*dir);
    pgd_clear(dir);
    return;
  }
  // 获取地址范围的第1个PMD。
  pmd = pmd_offset(dir, address);
  // 将地址与PGD对齐。
  address &= ~PGDIR_MASK;
  // end或者是待释放空间的末端,或者是这个PGD的末端,无论哪个首先出现。
  end = address + size;
  if (end > PGDIR_SIZE)
    end = PGDIR_SIZE;
  // 对每个PMD,这里调用free_area_pte()释放PTE表项。   
  do {
    free_area_pte(pmd, address, end - address);
  // address是下一个PMD的基地址。    
    address = (address + PMD_SIZE) & PMD_MASK;
  // 移到下一个PMD。    
    pmd++;
  } while (address < end);
}

(4)free_area_pte

    这是页表遍历的最后一个阶段。对地址范围内 PMD 的每个 PTE,它将释放 PTE 和相关的页面。

// mm/vmalloc.c
// 参数是将从PTE中释放的PMD、待释放区域的起始地址和大小。
static inline void free_area_pte(pmd_t * pmd, unsigned long address, unsigned long size)
{
  pte_t * pte;
  unsigned long end;
  // 如果区域是从一个失败的vmaloc()而来,则PMD将不存在。
  if (pmd_none(*pmd))
    return;
  // 如果PMD不在主存,则它被损坏了,这里标识为只读或者脏的或者访问过的。    
  if (pmd_bad(*pmd)) {
    pmd_ERROR(*pmd);
    pmd_clear(pmd);
    return;
  }
  // pte是地址范围的第一个PTE。
  pte = pte_offset(pmd, address);
  // 将地址与PMD对齐。
  address &= ~PMD_MASK;
  end = address + size;
  if (end > PMD_SIZE)
    end = PMD_SIZE;
  // 遍历所有的PTE,进行检查,然后释放相关页面的PTE。  
  do {
    pte_t page;
  // ptep_get_and_clear()将从页表中移除一个PTE,然后返回该PTE给调用者。   
    page = ptep_get_and_clear(pte);
  // address是下一个PTE的基地址。    
    address += PAGE_SIZE;
  // 移到下一个PTE。    
    pte++;
  // 如果没有PTE,则继续。   
    if (pte_none(page))
      continue;
  // 如果页面存在,则这里进行基本的检查,然后释放该页面。     
    if (pte_present(page)) {
  // pte_page 使用全局 mem_map 来找到 PTE 的 struct page。   
      struct page *ptpage = pte_page(page);
  // 保证页面是一个有效页面,且没有被保留,然后调用__free_page()释放物理页面。      
      if (VALID_PAGE(ptpage) && (!PageReserved(ptpage)))
        __free_page(ptpage);
  // 继续下一个PTE。        
      continue;
    }
  // 如果到达这一行,则在某个时候换出了内核地址空间中的PTE,内核内存是不可换出的,
  // 所以这是一个很严重的错误。    
    printk(KERN_CRIT "Whee.. Swapped out page in kernel page table\n");
  } while (address < end);
}
(a)⇐ pgtable-2level.h
// include/asm-i386/pgtable-2level.h
/*
 * The "pgd_xxx()" functions here are trivial for a folded two-level
 * setup: the pgd is never bad, and a pmd always exists (as it's folded
 * into the pgd entry)
 */
static inline int pgd_none(pgd_t pgd)   { return 0; }
static inline int pgd_bad(pgd_t pgd)    { return 0; }
static inline int pgd_present(pgd_t pgd)  { return 1; }
#define pgd_clear(xp)       do { } while (0)

/*
 * Certain architectures need to do special things when PTEs
 * within a page table are directly modified.  Thus, the following
 * hook is made available.
 */
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
#define set_pte_atomic(pteptr, pteval) (*(pteptr) = pteval)

/*
 * (pmds are folded into pgds so this doesnt get actually called,
 * but the define is needed for a generic inline function.)
 */
#define set_pmd(pmdptr, pmdval) (*(pmdptr) = pmdval)
#define set_pgd(pgdptr, pgdval) (*(pgdptr) = pgdval)

#define pgd_page(pgd) \
((unsigned long) __va(pgd_val(pgd) & PAGE_MASK))

static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
  return (pmd_t *) dir;
}
#define ptep_get_and_clear(xp)  __pte(xchg(&(xp)->pte_low, 0))
#define pte_same(a, b)    ((a).pte_low == (b).pte_low)
#define pte_page(x)   (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
#define pte_none(x)   (!(x).pte_low)
#define __mk_pte(page_nr,pgprot) __pte(((page_nr) << PAGE_SHIFT) | pgprot_val(pgprot))


目录
相关文章
|
13天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
22天前
|
Linux 调度
深入理解Linux虚拟内存管理(七)(下)
深入理解Linux虚拟内存管理(七)
34 4
|
22天前
|
存储 Linux 索引
深入理解Linux虚拟内存管理(九)(中)
深入理解Linux虚拟内存管理(九)
19 2
|
22天前
|
Linux 索引
深入理解Linux虚拟内存管理(九)(上)
深入理解Linux虚拟内存管理(九)
23 2
|
22天前
|
Linux
深入理解Linux虚拟内存管理(七)(中)
深入理解Linux虚拟内存管理(七)
23 2
|
22天前
|
机器学习/深度学习 消息中间件 Unix
深入理解Linux虚拟内存管理(九)(下)
深入理解Linux虚拟内存管理(九)
16 1
|
22天前
|
Linux
深入理解Linux虚拟内存管理(七)(上)
深入理解Linux虚拟内存管理(七)
24 1
|
20天前
|
缓存 Linux 调度
Linux服务器如何查看CPU占用率、内存占用、带宽占用
Linux服务器如何查看CPU占用率、内存占用、带宽占用
58 0
|
22天前
|
Linux API
深入理解Linux虚拟内存管理(六)(下)
深入理解Linux虚拟内存管理(六)
13 0
|
22天前
|
Linux
深入理解Linux虚拟内存管理(六)(中)
深入理解Linux虚拟内存管理(六)
18 0