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

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

深入理解Linux虚拟内存管理(四)(中):https://developer.aliyun.com/article/1597781

2、遍历页表

(1)follow_page

    这个函数返回在 mm 页表中 addressPTE 用到的 struct page

// mm/memory.c
/*
 * Do a quick page-table lookup for a single page. 
 */
// 这个函数是 mm 中待遍历的页表。address 与 struct page 和 write 有关,它表明该页
// 是否将被写。
static struct page * follow_page(struct mm_struct *mm, unsigned long address, int write) 
{
  pgd_t *pgd;
  pmd_t *pmd;
  pte_t *ptep, pte;

  // 获取 address 处的 PGD 并保证它存在且有效。
  pgd = pgd_offset(mm, address);
  if (pgd_none(*pgd) || pgd_bad(*pgd))
    goto out;
  
  // 获取address处的PMD并保证它存在且有效。
  pmd = pmd_offset(pgd, address);
  if (pmd_none(*pmd) || pmd_bad(*pmd))
    goto out;

  // 获取address处的PTE并保证它存在且有效。
  ptep = pte_offset(pmd, address);
  if (!ptep)
    goto out;

  pte = *ptep;
  // 如果PTE当前存在,则可以返回一些东西。
  if (pte_present(pte)) {
  // 如果调用者指定了将发生写操作,则检查以保证PTE有写权限。如果是这样, 将PTE设为脏。
    if (!write ||
        (pte_write(pte) && pte_dirty(pte)))
  // 如果PTE存在并有权限,则返回由PTE映射的struct page        
      return pte_page(pte);
  }

out:
  // 返回0表明该address没有相应的struct page
  return 0;
}

3、⇔ 页表

(1)pgd_alloc

// include/asm-i386/pgalloc.h
#define pgd_alloc(mm)   get_pgd_fast()
(a)get_pgd_fast

// include/asm-i386/pgalloc.h
static inline pgd_t *get_pgd_fast(void)
{
  unsigned long *ret;

  if ((ret = pgd_quicklist) != NULL) {
    pgd_quicklist = (unsigned long *)(*ret);
    ret[0] = 0;
    pgtable_cache_size--;
  } else
    ret = (unsigned long *)get_pgd_slow();
  return (pgd_t *)ret;
}


(b)get_pgd_slow

// include/asm-i386/pgalloc.h
static inline pgd_t *get_pgd_slow(void)
{
  pgd_t *pgd = (pgd_t *)__get_free_page(GFP_KERNEL);

  if (pgd) {
    memset(pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
    memcpy(pgd + USER_PTRS_PER_PGD,
      swapper_pg_dir + USER_PTRS_PER_PGD,
      (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
  }
  return pgd;
}

传送门 __get_free_page

(2)pgd_free

// include/asm-i386/pgalloc.h
#define pgd_free(pgd)   free_pgd_slow(pgd)
(a)free_pgd_slow

// include/asm-i386/pgalloc.h
static inline void free_pgd_slow(pgd_t *pgd)
{
#if defined(CONFIG_X86_PAE)
  int i;

  for (i = 0; i < USER_PTRS_PER_PGD; i++)
    free_page((unsigned long)__va(pgd_val(pgd[i])-1));
  kmem_cache_free(pae_pgd_cachep, pgd);
#else
  free_page((unsigned long)pgd);
#endif
}

(3)pmd_alloc

// include/linux/mm.h
/*
 * On a two-level page table, this ends up being trivial. Thus the
 * inlining and the symmetry break with pte_alloc() that does all
 * of this out-of-line.
 */
 // x86 两级页表,pmd 即为 pgd ?
static inline pmd_t *pmd_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
  if (pgd_none(*pgd))
    return __pmd_alloc(mm, pgd, address);
  return pmd_offset(pgd, address);
}
(a) ⇐ pgtable-2level.h
// include/asm-i386/pgtable-2level.h
static inline int pgd_none(pgd_t pgd)   { return 0; }

static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
  return (pmd_t *) dir;
}

(4)__pmd_alloc

// mm/memory.c
/*
 * Allocate page middle directory.
 *
 * We've already handled the fast-path in-line, and we own the
 * page table lock.
 *
 * On a two-level page table, this ends up actually being entirely
 * optimized away.
 */
pmd_t *__pmd_alloc(struct mm_struct *mm, pgd_t *pgd, unsigned long address)
{
  pmd_t *new;

  /* "fast" allocation can happen without dropping the lock.. */
  new = pmd_alloc_one_fast(mm, address);
  if (!new) {
    spin_unlock(&mm->page_table_lock);
    new = pmd_alloc_one(mm, address);
    spin_lock(&mm->page_table_lock);
    if (!new)
      return NULL;

    /*
     * Because we dropped the lock, we should re-check the
     * entry, as somebody else could have populated it..
     */
    if (!pgd_none(*pgd)) {
      pmd_free(new);
      goto out;
    }
  }
  pgd_populate(mm, pgd, new);
out:
  return pmd_offset(pgd, address);
}
(a) ⇐ pgalloc.h
// include/asm-i386/pgalloc.h
/*
 * allocating and freeing a pmd is trivial: the 1-entry pmd is
 * inside the pgd, so has no extra memory associated with it.
 * (In the PAE case we free the pmds as part of the pgd.)
 */

#define pmd_alloc_one_fast(mm, addr)  ({ BUG(); ((pmd_t *)1); })
#define pmd_alloc_one(mm, addr)   ({ BUG(); ((pmd_t *)2); })
#define pmd_free_slow(x)    do { } while (0)
#define pmd_free_fast(x)    do { } while (0)
#define pmd_free(x)     do { } while (0)
#define pgd_populate(mm, pmd, pte)  BUG()
(b) ⇒ pmd_alloc_one
// include/asm-x86_64/pgalloc.h
static inline pmd_t *pmd_alloc_one (struct mm_struct *mm, unsigned long addr)
{
  return (pmd_t *)get_zeroed_page(GFP_KERNEL); 
}
(c) ⇒ get_zeroed_page
// mm/page_alloc.c
unsigned long get_zeroed_page(unsigned int gfp_mask)
{
  struct page * page;

  page = alloc_pages(gfp_mask, 0);
  if (page) {
    void *address = page_address(page);
    clear_page(address);
    return (unsigned long) address;
  }
  return 0;
}

(5)pte_alloc

// mm/memory.c
/*
 * Allocate the page table directory.
 *
 * We've already handled the fast-path in-line, and we own the
 * page table lock.
 */
pte_t *pte_alloc(struct mm_struct *mm, pmd_t *pmd, unsigned long address)
{
  if (pmd_none(*pmd)) {
    pte_t *new;

    /* "fast" allocation can happen without dropping the lock.. */
    new = pte_alloc_one_fast(mm, address);
    if (!new) {
      spin_unlock(&mm->page_table_lock);
      new = pte_alloc_one(mm, address);
      spin_lock(&mm->page_table_lock);
      if (!new)
        return NULL;

      /*
       * Because we dropped the lock, we should re-check the
       * entry, as somebody else could have populated it..
       */
      if (!pmd_none(*pmd)) {
        pte_free(new);
        goto out;
      }
    }
    pmd_populate(mm, pmd, new);
  }
out:
  return pte_offset(pmd, address);
}
(a) ⇒ pte_alloc_one
// include/asm-i386/pgalloc.h
static inline pte_t *pte_alloc_one(struct mm_struct *mm, unsigned long address)
{
  pte_t *pte;

  pte = (pte_t *) __get_free_page(GFP_KERNEL);
  if (pte)
    clear_page(pte);
  return pte;
}

三、描述物理内存

1、初始化管理区

(1)setup_memory

    这个函数的调用图如图 2.3 所示。它为引导内存分配器初始化自身进行所需信息的获取。它可以分成几个不同的任务。

  • 找到低端内存的 PFN 的起点和终点(min_low_pfnmax_low_pfn),找到高端内存的 PFN 的起点和终点(highstart_pfnhighend_pfn),以及找到系统中最后一页的 PFN
  • 初始化 bootmem_date 结构以及声明可能被引导内存分配器用到的页面。
  • 标记所有系统可用的页面为空闲,然后为那些表示页面的位图保留页面。
  • SMP 配置或 initrd 镜像存在时,为它们保留页面。
// arch/i386/kernel/setup.c
static unsigned long __init setup_memory(void) {
  unsigned long bootmap_size, start_pfn, max_low_pfn;
  // 将物理地址向上取整到下一页面,返回页帧号。由于_end 是已载入内核
  // 镜像的底端地址,所以 start_pfn 现在是可能被用到的第一块物理页面帧的偏移。
  start_pfn = PFN_UP(__pa(&_end));
  // 遍历 e820 图,查找最高的可用 PFN。
  find_max_pfn();
  // 在 ZONE_NORMAL 中找到可寻址的最高页面帧。
  max_low_pfn = find_max_low_pfn();
  
#ifdef CONFIG_HIGHMEM
  // 如果高端内存可用,则从高端内存区域的 0 位置开始。如果内存在 max_low_pfn 后,
  // 则把高端内存区的起始位置(highstart_pfn)定在那里,而其结束位置定在 max_pfn,
  // 然后打印可用高端内存的提示消息。
  highstart_pfn = highend_pfn = max_pfn;
  if (max_pfn > max_low_pfn) {
    highstart_pfn = max_low_pfn;
  }
  printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
    pages_to_mb(highend_pfn - highstart_pfn));
#endif

  // 为 config_page_data 节点初始化 bootmem_data 结构。
  // 它设置节点的物理内存起始点(页帧号 start_pfn)和终点(页帧号 max_low_pfn ),
  // 分配一张位图来表示这些页面,并将所有的页面设置为初始时保留。
  bootmap_size = init_bootmem(start_pfn, max_low_pfn);
  // 读入 e820 图,然后为运行时系统中的所有可用页面
  // 调用 free_bootmem() 这将标记页面在初始化时为空闲(即可分配页面)。
  register_bootmem_low_pages(max_low_pfn);
  
  // 保留页面,即相应的位图的位设置为 1
  reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
       bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));

  // 保留 0 号页面,因为 0 号页面是 BIOS 用到的一个特殊页面。      
  reserve_bootmem(0, PAGE_SIZE);
#ifdef CONFIG_SMP
  // 保留额外的页面为跳板代码用。跳板代码处理用户空间如何进入内核空间。
  reserve_bootmem(PAGE_SIZE, PAGE_SIZE);
#endif
#ifdef CONFIG_ACPI_SLEEP
  // 如果加入了睡眠机制,就需要为它保留内存。这仅为那些有挂起功能的手提
  // 电脑所用到。它已经超过本书的范围。
  acpi_reserve_bootmem();
#endif
  // ...  
  return max_low_pfn;      
}
(a)⇒ init_bootmem

传送门 init_bootmem

(b)⇒ register_bootmem_low_pages
// arch/i386/kernel/setup.c
// 读入 e820 图,然后为运行时系统中的所有可用页面
// 调用 free_bootmem() 这将标记页面在初始化时为空闲(即可分配页面)。
static void __init register_bootmem_low_pages(unsigned long max_low_pfn)
{
  int i;

  for (i = 0; i < e820.nr_map; i++) {
    unsigned long curr_pfn, last_pfn, size;
    // ...  
    size = last_pfn - curr_pfn;
    free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size));
  }
}

传送门 free_bootmem

(c)⇒ reserve_bootmem

传送门 reserve_bootmem

(2)zone_sizes_init

  这是一个用于初始化各管理区的高层函数。PFN 中管理区大小在 setup_memory() 过程中发现。这个函数填充一个管理区大小的数组,并把它传给 free_area_init()

// arch/i386/mm/init.c
static void __init zone_sizes_init(void)
{
  // 初始化大小为 0。
  unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};
  unsigned int max_dma, high, low;
  
  // 计算最大可能 DMA 寻址的 PFN。
  // #define MAX_DMA_ADDRESS      (PAGE_OFFSET+0x1000000) 16M
  max_dma = virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
  // max_low_pfn 是 ZONE_NORMAL 可用的最高 PFN。
  low = max_low_pfn;
  // highest_pfn 是 ZONE_HIGHMEM 可用的最高 PFN。
  high = highend_pfn;

  // 如果在 ZONE_NORMAL 中的最高 PFN 低于 MAX_DMA_ADDRESS,则仅
  // 把 ZONE_DMA 的大小赋值给它,其他管理区仍然为 0。
  if (low < max_dma)
    zones_size[ZONE_DMA] = low;
  else {
  // 设置 ZONE_DMA 中的页面数。
    zones_size[ZONE_DMA] = max_dma;
  // ZONE_NORMAL 的大小等于 max_low_pfn 减去 ZONE_DMA 中的页面数。   
    zones_size[ZONE_NORMAL] = low - max_dma;
#ifdef CONFIG_HIGHMEM
  // ZONE_HIGHMEM 的大小是可能的最高 PFN 减去 ZONE_NORMAL 中可能的最
  // 高 PFN(max_low_pfn)。
    zones_size[ZONE_HIGHMEM] = high - low;
#endif
  }
  free_area_init(zones_size);
}

(3)free_area_init

    这是一个与体系结构无关的函数,用于设置 UMA 体系结构。它仅是调用核心函数,传入静态 contig_page_data 作为节点。不同的是,NUMA 体系结构将使用 free_area_node()

// mm/page_alloc.c
void __init free_area_init(unsigned long *zones_size)
{
  free_area_init_core(0, &contig_page_data, &mem_map, zones_size, 0, 0, 0);
}

0 是该节点的节点标识(NID),这里为 0。

contig_page_data 是静态全局 pg_data_t。

mem_map 用于跟踪 struct page 的全局 mem_map。 函数 free_area_init_core() 将为这个数组分配内存。

zones_sizes 是由 zone_sizes_init() 填充的管理区大小的数组。

0,这个 0 是物理地址的起始点。

0,第 2 个 0 是内存空洞大小的数组,但不用于 UMA 体系结构。

0,最后一个 0 是一个指向该节点局部 mem_map 的一个指针,用于 NUMA 体系结构

传送门 free_area_init_core


(4)free_area_init_node

   这个函数有 2 个版本。第 1 个版本除了使用了一个不同的物理地址起点外,几乎与 free_area_init() 一样。这个函数还用于仅有一个节点的体系结构(所以它们使用 contig_page_data),但是它们的物理地址不是 0。


   在页表初始化后调用的版本用于初始化系统中的每个 pgdat。 如果希望在特定的体系结构中调优它们的位置,调用者可以有选择地分配它们自己的 mem_map 部分并将它们作为参数传递给这个函数。如果选择不,则 mem_map 部分就会在后面由 free_area_init_core() 来分配。

// mm/numa.c
/*
 * Nodes can be initialized parallely, in no particular order.
 */
// nid是传入的pgdat的NID。
// pgdat是将被初始化的节点。
// pmap是指向将要用到的节点的mem_map部分的指针,它经常传入NULL,并在以后分配空间。
// zones_size是该节点中容量为管理区大小的一个数组。
// zone_start_paddr是节点的物理地址起点。
// zholes_size是容量为每个管理区中空洞大小的一个数组。 
void __init free_area_init_node(int nid, pg_data_t *pgdat, struct page *pmap,
  unsigned long *zones_size, unsigned long zone_start_paddr, 
  unsigned long *zholes_size)
{
  int i, size = 0;
  struct page *discard;

  // 如果设置全局的mem_map,就将其设置在线性地址空间中内核部分的起点。请
  // 记住,在NUMA中,mem_map是一个分块的虚拟数组,数组由每个节点中的局部映射填充。
  if (mem_map == (mem_map_t *)NULL)
    mem_map = (mem_map_t *)PAGE_OFFSET;

  // 调用free_area_init_core()。注意,discard作为第3个参数传入,这是因为NUMA 中
  // 并不需要设置全局的mem_map。
  free_area_init_core(nid, pgdat, &discard, zones_size, zone_start_paddr,
          zholes_size, pmap);
  // 记录 pgdat 的 NID。          
  pgdat->node_id = nid;

  /*
   * Get space for the valid bitmap.
   */
  // 计算NID的总大小。  
  for (i = 0; i < MAX_NR_ZONES; i++)
    size += zones_size[i];
  // 重新计算位数,以满足这样大小的每个字节都有一位。   
  size = LONG_ALIGN((size + 7) >> 3);
  // 分配一张位图,表示节点中存在的有效管理区。实际上,这条语句仅用于Sparc体系结构,
  // 所以对其他体系结构而言是浪费内存。
  pgdat->valid_addr_bitmap = (unsigned long *)alloc_bootmem_node(pgdat, size);
  // 开始时,所有的区域都无效。有效区域由后面所述Sparc中的mem_init()函数标记。
  // 其他体系结构仅是忽略掉这张位图。
  memset(pgdat->valid_addr_bitmap, 0, size);
}

(5)free_area_init_core

    这个函数负责初始化所有的区域,并在节点中分配它们的局部 Imem_map。在 UMA 体系结构中,调用这个函数将初始化全局 mem_map 数组。在 NUMA 体系结构中,这个数组被看作是一个稀疏分布的虚拟数组。

// mm/page_alloc.c
/*
 * Set up the zone data structures:
 *   - mark all pages reserved
 *   - mark all memory queues empty
 *   - clear the memory bitmaps
 */
void __init free_area_init_core(int nid, pg_data_t *pgdat, struct page **gmap,
  unsigned long *zones_size, unsigned long zone_start_paddr, 
  unsigned long *zholes_size, struct page *lmem_map)
{
  unsigned long i, j;
  unsigned long map_size;
  unsigned long totalpages, offset, realtotalpages;
  
  // 该块主要负责计算每个区域的大小。
  // 这个区域必须邻近由伙伴分配器分配的最大大小的块,从而进行位级操作。
  // MAX_ORDER 为 10
  const unsigned long zone_required_alignment = 1UL << (MAX_ORDER-1);
  // 如果物理地址不是按页面排列的,就是一个 BUG()。
  if (zone_start_paddr & ~PAGE_MASK)
    BUG();
  // 为这个节点初始化 totalpages 为 0。
  totalpages = 0;
  // 通过遍历 zone_sizes 来计算节点的总大小。
  for (i = 0; i < MAX_NR_ZONES; i++) {
    unsigned long size = zones_size[i];
    totalpages += size;
  }
  // 通过减去 zholes_size 的空洞大小来计算实际的内存量。
  realtotalpages = totalpages;
  if (zholes_size)
    for (i = 0; i < MAX_NR_ZONES; i++)
      realtotalpages -= zholes_size[i];
  // 打印提示信息告知用户这个节点可用的内存量。    
  printk("On node %d totalpages: %lu\n", nid, realtotalpages);

  /*
   * Some architectures (with lots of mem and discontinous memory
   * maps) have to search for a good mem_map area:
   * For discontigmem, the conceptual mem map array starts from 
   * PAGE_OFFSET, we need to align the actual array onto a mem map 
   * boundary, so that MAP_NR works.
   */
  // 这一块在需要时分配局部 lmem_map,设置 gmap 位。在 UMA 体系结构中,gmap 实际
  // 上就是 mem_map,所以这就是为它分配的内存。
  // 
  // 计算数组所需的内存量。它等于页面总数量乘以 struct page 的大小。
  map_size = (totalpages + 1)*sizeof(struct page);
  // 如果映射图还没有分配,就在这里分配。
  if (lmem_map == (struct page *)0) {
  // 从引导内存分配器中分配一块内存。
    lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size);
  // MAP_ALIGN() 将在一个 struct page 大小范围内排列数组,从而计算在 mem_map
  // 中基于物理地址 MAP_NR() 宏的内部偏移。   
    lmem_map = (struct page *)(PAGE_OFFSET + 
      MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
  }
  // 设置 gmap 和 pgdat-node_mem_map 变量以分配 lmem_map。在 UMA 体系结构
  // 中,仅设置 mem_map。
  *gmap = pgdat->node_mem_map = lmem_map;
  // 记录节点大小。
  pgdat->node_size = totalpages;
  // 记录起始物理地址。
  pgdat->node_start_paddr = zone_start_paddr;
  // 记录节点所占 mem_map 中的偏移。
  pgdat->node_start_mapnr = (lmem_map - mem_map);
  // 初始化管理区计数为 0,这将在函数的后面设置。
  pgdat->nr_zones = 0;

  // offset 现在是 lmem_map 开始的局部部分中 mem_map 的偏移。 
  offset = lmem_map - mem_map;  

  // 从这块管理区开始循环,初始化节点中的每个 zone_t。该初始化过程开始时设置已存在
  // 基本字段的值。
  // 
  // 追历节点中所有管理区。
  for (j = 0; j < MAX_NR_ZONES; j++) {
    zone_t *zone = pgdat->node_zones + j;
    unsigned long mask;
    unsigned long size, realsize;

  // 记录在 zone_table 中指向该管理区的指针,见 2.6 节。
    zone_table[nid * MAX_NR_ZONES + j] = zone;
  // 计算管理区的实际大小。它基于 zones_sizes 的总大小减去 zholes_size 的空洞大小。   
    realsize = size = zones_size[j];
    if (zholes_size)
      realsize -= zholes_size[j];
  // 打印提示信息告知在这个管理区中的页面数。
    printk("zone(%lu): %lu pages.\n", j, size);
  // 记录管理区的大小。    
    zone->size = size;
  // zone_names 是管理区的大小,这里是为了打印的需要。   
    zone->name = zone_names[j];
  // 初始化管理区的其他字段,如它的父 pgdat。    
    zone->lock = SPIN_LOCK_UNLOCKED;
    zone->zone_pgdat = pgdat;
    zone->free_pages = 0;
    zone->need_balance = 0;
  // 如果管理区没有内存,就转到下一个管理区,因为不需要其他操作了。    
    if (!size)
      continue;

    /*
     * The per-page waitqueue mechanism uses hashed waitqueues
     * per zone.
     */
// 这一块初始化管理区的等待队列。等待该管理区中页面的进程将会使用这个哈希表选择
// 一个队列等待。这意味着在页面解锁时并不需要唤醒所有的等待进程,仅需要唤醒其中的一
// 个子集。
// wait_table_size() 计算所用哈希表的大小。它基于管理区的页面数和队列数与页面
// 数之间的特定比率完成计算。该哈希表不会大于 4 KB。
    zone->wait_table_size = wait_table_size(size);
// 计算哈希算法的因子。
    zone->wait_table_shift =
      BITS_PER_LONG - wait_table_bits(zone->wait_table_size);
// 分配可以容纳 zone→wait_table_size 项的 wait_queue_head_t 表。
    zone->wait_table = (wait_queue_head_t *)
      alloc_bootmem_node(pgdat, zone->wait_table_size
            * sizeof(wait_queue_head_t));
// 初始化所有的等待队列。
    for(i = 0; i < zone->wait_table_size; ++i)
      init_waitqueue_head(zone->wait_table + i);

// 这一块计算管理区极值并记录管理区地址。这个极值记为管理区大小的比率。
// 
// 首先,若激活了一个新的管理区,更新节点中的管理区数量。
    pgdat->nr_zones = j+1;
// 计算掩码(将用于 page_min 极值)为管理区的大小除以管理区的平衡因子。所有管
// 理区的平衡因子在 mm/page_alloc.c 的首部声明为 128。
    mask = (realsize / zone_balance_ratio[j]);
// 所有管 理 区的 zone_balance_min 比率都为 20,这意味着 page_min 将不低于 20 。
    if (mask < zone_balance_min[j])
      mask = zone_balance_min[j];
    else if (mask > zone_balance_max[j])
// 类似地,所有的 zone_balance_max 都为 255,所以 pages_min 将不会超过 255。
      mask = zone_balance_max[j];
//  pages_min 设为 mask。
    zone->pages_min = mask;
// pages_low 是 pages_min 页面数量的 2 倍。
    zone->pages_low = mask*2;
// pages_high 是 pages_min 页面数量的 3 倍。
    zone->pages_high = mask*3;
// 记录管理区的第 1 个 struct page 在 mem_map 中的地址。
    zone->zone_mem_map = mem_map + offset;
// 记录 mem_map 中管理区起点的索引。
    zone->zone_start_mapnr = offset;
// 记录起始的物理地址。
    zone->zone_start_paddr = zone_start_paddr;
// 利用伙伴分配器保证管理区已经正确的排列可用。否则,伙伴分配器用到的位
// 级操作就会失败。
    if ((zone_start_paddr >> PAGE_SHIFT) & (zone_required_alignment-1))
      printk("BUG: wrong zone alignment, it will crash\n");

    /*
     * Initially all pages are reserved - free ones are freed
     * up by free_all_bootmem() once the early boot process is
     * done. Non-atomic initialization, single-pass.
     */
// 初始时,管理区中所有的页面都标记为保留,因为没有办法知道引导内存分配
// 器使用的是哪些页面。当引导内存分配器在 free_all_bootmem() 中收回时,
// 未使用页面中的 PG_reserved 会被清除。
    for (i = 0; i < size; i++) {
// 获取页面的偏移。
      struct page *page = mem_map + offset + i;
// 页面所在的管理区由页面标志编码,见 2.6 节。
      set_page_zone(page, nid * MAX_NR_ZONES + j);
// 设置计数为 0,因为管理区未被使用。
      set_page_count(page, 0);
// 设置保留标志,然后,若页面不再被使用,引导内存分配器将清除该位。
      SetPageReserved(page);
// 初始化页面链表头。
      INIT_LIST_HEAD(&page->list);
// 如果页面可用且页面在低端内存,设置 page virtual 字段。
      if (j != ZONE_HIGHMEM)
        set_page_address(page, __va(zone_start_paddr));
// 将 zone_start_paddr 增加一个页面大小,该参数将用于记录下一个管理区的起点。
      zone_start_paddr += PAGE_SIZE;
    }

// 这一块初始化管理区的空闲链表,分配伙伴分配器在记录页面伙伴状态时的位图。
    offset += size;
// 从 0 循环到 MAX_ORDER -1。
    for (i = 0; ; i++) {
      unsigned long bitmap_size;
// 初始化当前次序为 i的 free_list 链表。
      INIT_LIST_HEAD(&zone->free_area[i].free_list);
// 如果处于最后,设置空闲区域映射为 NULL,表示空闲链表的末端。
      if (i == MAX_ORDER-1) {
        zone->free_area[i].map = NULL;
        break;
      }

      /*
       * Page buddy system uses "index >> (i+1)",
       * where "index" is at most "size-1".
       *
       * The extra "+3" is to round down to byte
       * size (8 bits per byte assumption). Thus
       * we get "(size-1) >> (i+4)" as the last byte
       * we can access.
       *
       * The "+1" is because we want to round the
       * byte allocation up rather than down. So
       * we should have had a "+7" before we shifted
       * down by three. Also, we have to add one as
       * we actually _use_ the last bit (it's [0,n]
       * inclusive, not [0,n[).
       *
       * So we actually had +7+1 before we shift
       * down by 3. But (n+8) >> 3 == (n >> 3) + 1
       * (modulo overflows, which we do not have).
       *
       * Finally, we LONG_ALIGN because all bitmap
       * operations are on longs.
       */
// 下面两句:
// bitmap_size = (size-1) >> (i+4);
// bitmap_size = LONG_ALIGN(bitmap_size+1);
// 相当于如下代码:
// bitmap_size = size >> i;      // 内存块个数
// bitmap_size = (bitmap_size + 7) >> 3; // 因为一个字节有8个位, 所以要除以8
// bitmap_size = LONG_ALIGN(bitmap_size); 
//
// 计算 bitmap_size 为容纳整个位图所需的字节数。位图中每一位表示一个有 2^i 数量
// 页面的伙伴对。
      bitmap_size = (size-1) >> (i+4);
// 利用 LONG_ALIGN() 转化容量值为一个长整型,因为所有的位操作都在长整型上进行。
      bitmap_size = LONG_ALIGN(bitmap_size+1);
// 分配映射用到的内存。
      zone->free_area[i].map = 
        (unsigned long *) alloc_bootmem_node(pgdat, bitmap_size);
    }
// 循环到下一个管理区。
  }
// 利用 build_zonelists()来构造节点的管理区回退链表。
  build_zonelists(pgdat);
}
(a)⇒ alloc_bootmem_node

传送门 alloc_bootmem_node

(b)⇒ 初始化 mem_map

    核心函数 free_area_init_core() 为已经初始化过的节点分配局部 lmem_map。而该数组的内存通过引导内存分配器中的 alloc_bootmem_node() 分配得到。在 UMA 结构中,新分配的内存变成了全局的 mem_map

传送门 初始化 mem_map 描述

(c)⇐ zone_t

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

(d)⇒ wait_table_size
/*
 * Helper functions to size the waitqueue hash table.
 * Essentially these want to choose hash table sizes sufficiently
 * large so that collisions trying to wait on pages are rare.
 * But in fact, the number of active page waitqueues on typical
 * systems is ridiculously low, less than 200. So this is even
 * conservative, even though it seems large.
 *
 * The constant PAGES_PER_WAITQUEUE specifies the ratio of pages to
 * waitqueues, i.e. the size of the waitq table given the number of pages.
 */
#define PAGES_PER_WAITQUEUE 256

// 这一块初始化管理区的等待队列。等待该管理区中页面的进程将会使用这个哈希表选择
// 一个队列等待。这意味着在页面解锁时并不需要唤醒所有的等待进程,仅需要唤醒其中的一
// 个子集。
// wait_table_size() 计算所用哈希表的大小。它基于管理区的页面数和队列数与页面
// 数之间的特定比率完成计算。该哈希表不会大于 4 KB。
// 每 PAGES_PER_WAITQUEUE(256)个使用同一个值
static inline unsigned long wait_table_size(unsigned long pages)
{
  unsigned long size = 1;

  pages /= PAGES_PER_WAITQUEUE;

  while (size < pages)
    size <<= 1;

  /*
   * Once we have dozens or even hundreds of threads sleeping
   * on IO we've got bigger problems than wait queue collision.
   * Limit the size of the wait table to a reasonable size.
   */
  size = min(size, 4096UL);

  return size;
}
(e)⇒ wait_table_bits
// mm/page_alloc.c
/*
 * This is an integer logarithm so that shifts can be used later
 * to extract the more random high bits from the multiplicative
 * hash function before the remainder is taken.
 */
static inline unsigned long wait_table_bits(unsigned long size)
{
  return ffz(~size);
}
(e)⇒ wait.h
// include/linux/wait.h
struct __wait_queue_head {
  wq_lock_t lock;
  struct list_head task_list;
#if WAITQUEUE_DEBUG
  long __magic;
  long __creator;
#endif
};
typedef struct __wait_queue_head wait_queue_head_t;

static inline void init_waitqueue_head(wait_queue_head_t *q)
{
#if WAITQUEUE_DEBUG
  if (!q)
    WQ_BUG();
#endif
  q->lock = WAITQUEUE_RW_LOCK_UNLOCKED;
  INIT_LIST_HEAD(&q->task_list);
#if WAITQUEUE_DEBUG
  q->__magic = (long)&q->__magic;
  q->__creator = (long)current_text_addr();
#endif
}
(f)⇒ mm.h
// include/linux/mm.h
/*
 * The zone field is never updated after free_area_init_core()
 * sets it, so none of the operations on it need to be atomic.
 */
#define NODE_SHIFT 4
#define ZONE_SHIFT (BITS_PER_LONG - 8)

struct zone_struct;
extern struct zone_struct *zone_table[];

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

static inline void set_page_zone(struct page *page, unsigned long zone_num)
{
  page->flags &= ~(~0UL << ZONE_SHIFT);
  page->flags |= zone_num << ZONE_SHIFT;
}

#define set_page_count(p,v)   atomic_set(&(p)->count, v)

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

/*
 * In order to avoid #ifdefs within C code itself, we define
 * set_page_address to a noop for non-highmem machines, where
 * the field isn't useful.
 * The same is true for page_address() in arch-dependent code.
 */
#if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)

#define set_page_address(page, address)     \
  do {            \
    (page)->virtual = (address);    \
  } while(0)

#else /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */
#define set_page_address(page, address)  do { } while(0)
#endif /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */
(g)⇐ 概览

传送门 6.3 释放页面

(h)⇐ 伙伴算法

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

伙伴算法原理简介


(6)build_zonelists

    这个函数在所请求的节点中为每个管理区构造回退管理区。这是为了在不能满足一个分配时可以考察下一个管理区而设立的。在考察结束时,分配将从 ZONE_HIGHMEM 回退到 ZONE_NORMAL,在分配从 ZONE_NORMAL 回退到 ZONE_DMA 后就不再回退。

/*
 * Builds allocation fallback zone lists.
 */
static inline void build_zonelists(pg_data_t *pgdat)
{
  int i, j, k;

// 遍历最大可能数量的管理区。GFP_ZONEMASK(15)
  for (i = 0; i <= GFP_ZONEMASK; i++) {
    zonelist_t *zonelist;
    zone_t *zone;

// 获取管理区的 zonelist 并归 0。
    zonelist = pgdat->node_zonelists + i;
    memset(zonelist, 0, sizeof(*zonelist));

// 与 ZONE_DMA 对应,j 从 0 开始。
    j = 0;
// 设置 k 为当前检查过的管理区类型。
    k = ZONE_NORMAL;
    if (i & __GFP_HIGHMEM)
      k = ZONE_HIGHMEM;
    if (i & __GFP_DMA)
      k = ZONE_DMA;

    switch (k) {
      default:
        BUG();
      /*
       * fallthrough:
       */
      case ZONE_HIGHMEM:
// 获取 ZONE_HIGHMEM。
        zone = pgdat->node_zones + ZONE_HIGHMEM;
// 如果管理区有内存,ZONE_HIGHMEM 就是从高端内存分配的相应管理区。
// 如果 ZONE_HIGHMEM 没有内存,ZONE_NORMAL 将在下一次变成相应的管理区,这是
// 因为 j 并没有在空管理区时加 1。
        if (zone->size) {
#ifndef CONFIG_HIGHMEM
          BUG();
#endif
          zonelist->zones[j++] = zone;
        }
// 设置下一个相应的管理区中分配的为 ZONE_NORMAL,同样,如果管理区没
// 有内存就不使用它。
      case ZONE_NORMAL:
        zone = pgdat->node_zones + ZONE_NORMAL;
        if (zone->size)
          zonelist->zones[j++] = zone;
      case ZONE_DMA:
// 设置最后的回退管理区为 ZONE_DMA。这个检查也对拥有内存的 ZONE_DMA 
// 进行。在 NUMA 体系结构中,不是所有的节点都有 ZONE_DMA。
        zone = pgdat->node_zones + ZONE_DMA;
        if (zone->size)
          zonelist->zones[j++] = zone;
    }
    zonelist->zones[j++] = NULL;
  } 
}
(a)⇐ mmzone.h
// include/linux/mmzone.h
#define ZONE_DMA    0
#define ZONE_NORMAL   1
#define ZONE_HIGHMEM    2
#define MAX_NR_ZONES    3

/*
 * One allocation request operates on a zonelist. A zonelist
 * is a list of zones, the first one is the 'goal' of the
 * allocation, the other zones are fallback zones, in decreasing
 * priority.
 *
 * Right now a zonelist takes up less than a cacheline. We never
 * modify it apart from boot-up, and only a few indices are used,
 * so despite the zonelist table being relatively big, the cache
 * footprint of this construct is very small.
 */
typedef struct zonelist_struct {
  zone_t * zones [MAX_NR_ZONES+1]; // NULL delimited
} zonelist_t;

#define GFP_ZONEMASK  0x0f

build_zonelists 函数执行后,pgdat->node_zonelists15 个大小。其大致如下:


   15 个 zonelist_t, k 的值依次为 ZONE_NORMAL、ZONE_DMA、ZONE_HIGHMEM、ZONE_DMA,每 4 个循环一次。而每个 zonelist_t 中有 4 个 zone_t,其值为:


当为 ZONE_NORMAL 时,4 个 zone_t 依次为指向 NORMAL、DMA、NULL、NULL

当为 ZONE_DMA 时,4 个 zone_t 依次为指向 DMA、NULL、NULL、NULL

当为 ZONE_HIGHMEM 时,4 个 zone_t 依次为指向 HIGHMEM、NORMAL、DMA、NULL

123

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