深入理解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, ¤t->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)⇐ 概览
(c)⇐ free_area_init_core
这个函数负责初始化所有的区域,并在节点中分配它们的局部 Imem_map(类型 struct page *)。并初始化 pg_data_t 中字段 node_mem_map,node_size,node_start_paddr,node_start_mapnr,nr_zones,node_zones 以及全局 zone_table。
(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))
(2)__free_page
// include/linux/mm.h // 这个宏仅调用函数 __free_pages() ,参数为 0 幂次和一个页面。 #define __free_page(page) __free_pages((page), 0)
(3)free_page
// include/linux/mm.h // 这个小宏仅调用 free_pages() 。这个宏与 __free_page() 的主要区别在于这个函数以一个 // 虚拟地址为参数,而 __free_page() 以一个 struct page 为参数 #define free_page(addr) free_pages((addr),0)
二、不连续内存分配
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)⇐ 非连续内存分配
(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
(c)⇒ __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
(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() 分配目录。
即按照虚拟地址的范围,建立相应的 pmd,pte,以及 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
(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
(b)⇒ __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))