深入理解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))


目录
相关文章
|
3月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
193 6
|
12天前
|
缓存 Java Linux
如何解决 Linux 系统中内存使用量耗尽的问题?
如何解决 Linux 系统中内存使用量耗尽的问题?
|
12天前
|
缓存 Linux
如何检查 Linux 内存使用量是否耗尽?
何检查 Linux 内存使用量是否耗尽?
|
22天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
4天前
|
存储 算法 安全
深入理解Linux内核的内存管理机制
本文旨在深入探讨Linux操作系统内核的内存管理机制,包括其设计理念、实现方式以及优化策略。通过详细分析Linux内核如何处理物理内存和虚拟内存,揭示了其在高效利用系统资源方面的卓越性能。文章还讨论了内存管理中的关键概念如分页、交换空间和内存映射等,并解释了这些机制如何协同工作以提供稳定可靠的内存服务。此外,本文也探讨了最新的Linux版本中引入的一些内存管理改进,以及它们对系统性能的影响。
|
27天前
|
存储 缓存 监控
|
2月前
|
存储 缓存 监控
Linux中内存和性能问题
【10月更文挑战第5天】
40 4
|
2月前
|
算法 Linux
Linux中内存问题
【10月更文挑战第6天】
54 2
|
25天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
30 0
|
2月前
|
存储 缓存 固态存储