Linux 内核设计与实现
深入理解 Linux 内核(一)
深入理解 Linux 内核(二)
Linux 设备驱动程序(一)
Linux 设备驱动程序(二)
Linux 设备驱动程序(三)
Linux设备驱动开发详解
深入理解Linux虚拟内存管理(一)
深入理解Linux虚拟内存管理(二)
深入理解Linux虚拟内存管理(三)
深入理解Linux虚拟内存管理(四)
深入理解Linux虚拟内存管理(五)
深入理解Linux虚拟内存管理(六)
// include/linux/mm.h
// gfp_mask (Get Free Pages) 标志表明分配器如何操作。如,如果没有设置 GFP_WAIT,
// 分配器将不会阻塞,而是在内存紧张时返回 NULL,b是分配页面数的 2 的幂。
static inline struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)
{
/*
* Gets optimized away by the compiler.
*/
// 在编译时的调试检查。
if (order >= MAX_ORDER)
return NULL;
return _alloc_pages(gfp_mask, order);
}
这个函数 _alloc_pages() 有两个变种。第 1 个是设计为仅 UMA 体系结构如 x86 可用,它在 mm/page_alloc.c 中。仅指静态节点 contig_page_data。第 2 个是在 mm/numa.c 中,它是一个简单的扩展。它使用一个局部分配节点的策略,这意味着仅从靠近处理器的空闲区分配内存。对本书而言,我们仅考察 mm/page_alloc.c 中的那个。但是 NUMA 体系结构上的开发者应该阅读 mm/numa.c 中的 _alloc_pages() 和 _alloc_pages_pgdat() 。
// mm/page_alloc.c
// UMA 体系结构中的 ifndef 象 x86 中一样。NUMA 体系结构使用 mm/numa.c 中的
// _alloc_pages() 函数,它在分配时采取局部分配节点的策略。
#ifndef CONFIG_DISCONTIGMEM
// gfp_mask 标志位告知分配器如何操作。order(次)是待分配页面数的 2 的幂。
struct page *_alloc_pages(unsigned int gfp_mask, unsigned int order)
{
// node_zonelists 是由分配回退管理区组成的一个数组。它在 build_zonelists()
// 中初始化。gfp_mask 的低 16 位表明哪个管理区适合分配。应用位掩码
// gfp_mask&GFP_ZONEMASK 将给出 node_zonelists 中我们要分配的索引。
return __alloc_pages(gfp_mask, order,
contig_page_data.node_zonelists+(gfp_mask & GFP_ZONEMASK));
}
#endif
在这个阶段,我们到达了描述为 “伙伴分配器的中心地带” ,即 __alloc_pages() 函数。它负责遍历回退管理区,然后选择一个合适的进行分配。如果内存紧张,则采用一些步骤来解决这个问题。它将唤醒 kswapd,并在需要的时候,自动完成 kswapd 的工作。
// mm/page_alloc.c
/*
* This is the 'heart' of the zoned buddy allocator:
*/
struct page * __alloc_pages(unsigned int gfp_mask, unsigned int order,
zonelist_t *zonelist)
{
unsigned long min;
zone_t **zone, * classzone;
struct page * page;
int freed;
// 设置该管理区为要从中分配的合适管理区。
zone = zonelist->zones;
// 合适的管理区标记为 classzone。如果在后面达到一个页面的极值,则 classzone 标记
// 为需要平衡。
classzone = *zone;
// 不需要的有效性检查。build_zonelists()需要被严重地打散。
if (classzone == NULL)
return NULL;
// 这一块的风格似乎在这个函数中出现多次。它读作 "遍历在该回退链表中的所
// 有管理区,看是否有一个分配器可以在不超过极值的情况下满足条件" 。每个回退管理区的
// pages_low 加到一起。这里特意减少使用一个回退管理区的概率。
min = 1UL << order;
for (;;) {
// z 是当前检查的管理区。 zone 变量移到下一个回退管理区。
zone_t *z = *(zone++);
// 如果这是回退链表的最后一个管理区,则退出循环。
if (!z)
break;
// 为方便比较,将该极值分配器的页面数加 1,这一步在每个回退管理区的每个区域中
// 都进行。虽然这看起来像是一个 bug,但它实际上是想减少使用回退管理区的概率。
min += z->pages_low;
// 如果页面块分配可以不超过 pages_min 极值,就分配一块页面块。rmqueue()
// 负责从管理区中重新移动该页面块。
if (z->free_pages > min) {
page = rmqueue(z, order);
// 如果可以分配该页,则这里返回一个指向它们的指针。
if (page)
return page;
}
}
// 标记合适的管理区为需要平衡。这个标志位将在后面由 kswapd 读取。
classzone->need_balance = 1;
// 这是一个内存界限。它保证所有的 CPU 都可以看到在这行代码之前的所有变化。
// 这很重要,因为 kswapd 除了在内存分配器上运行外还可以在不同的处理器上运行。
mb();
// 如果 kswapd 处于睡眠状态,则唤醒它。
if (waitqueue_active(&kswapd_wait))
wake_up_interruptible(&kswapd_wait);
// 以第一个合适的管理区和 min 值再次开始。
zone = zonelist->zones;
min = 1UL << order;
// 遍历所有的管理区。这一次,如果不超过 page_min 的极限值则分配一页。
for (;;) {
unsigned long local_min;
zone_t *z = *(zone++);
if (!z)
break;
// local_min 表明该管理区可以拥有的空闲页数量。
local_min = z->pages_min;
// 如果该进程不能等待或者重调度 (__GFP_WAIT 清空) ,在这里允许管理区进
// 一步增加比普通极值还要大的内存压力。
if (!(gfp_mask & __GFP_WAIT))
local_min >>= 2;
min += local_min;
if (z->free_pages > min) {
page = rmqueue(z, order);
if (page)
return page;
}
}
/* here we're in the low on memory slow path */
// 在试着同步空闲页面后返回这个标号。从这一行起,就达到了内存路径的低端,在这
// 种情形下,进程很可能会睡眠。
rebalance:
// OOM 管理程序只会设置两个标志。由于进程试着完整地杀死它自身,在可能
// 的情况下,将分配页面,因为知道马上就可以释放这些页面。
if (current->flags & (PF_MEMALLOC | PF_MEMDIE)) {
zone = zonelist->zones;
for (;;) {
zone_t *z = *(zone++);
if (!z)
break;
page = rmqueue(z, order);
if (page)
return page;
}
return NULL;
}
/* Atomic allocations - we can't balance anything */
// 如果调用进程无法睡眠,这里返回 NULL,因为分配页面的惟一途径是调用睡眠。
if (!(gfp_mask & __GFP_WAIT))
return NULL;
// balance_classzone() 同步进行 kswapd 的工作。主要的区别在于,它
// 不是将内存释放到一个全局的池中,而是保证进程始终使用 curren→local_pages 链表。
page = balance_classzone(classzone, gfp_mask, order, &freed);
// 如果已经按顺序释放了一个页块,这里就返回该页块。由于高次页面可能被释
// 放,所以这里 NULL 并不意味着分配失败。
if (page)
return page;
zone = zonelist->zones;
min = 1UL << order;
// 这里与前面块相似。如果可以不超过 pages_min 极值则可以分配一页块。
for (;;) {
zone_t *z = *(zone++);
if (!z)
break;
min += z->pages_min;
if (z->free_pages > min) {
page = rmqueue(z, order);
if (page)
return page;
}
}
/* Don't let big-order allocations loop */
// 要满足一次分配一块如 2^4 那样数量的页面比较困难。如果现在还没有满足,那
// 么最好直接返回 NULL。
if (order > 3)
return NULL;
/* Yield for kswapd, and try again */
// 阻塞进程,以给 kswapd 工作的机会。
yield();
// 试着再次平衡和分配管理区。
goto rebalance;
}
传送门 6.3 释放页面
// include/linux/wait.h
static inline int waitqueue_active(wait_queue_head_t *q)
{
#if WAITQUEUE_DEBUG
if (!q)
WQ_BUG();
CHECK_MAGIC_WQHEAD(q);
#endif
return !list_empty(&q->task_list);
}
// include/linux/sched.h
// 只会唤醒那些执行可中断休眠的进程
#define wake_up_interruptible(x) __wake_up((x),TASK_INTERRUPTIBLE, 1)
// kernel/sched.c
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr)
{
if (q) {
unsigned long flags;
wq_read_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr, 0);
wq_read_unlock_irqrestore(&q->lock, flags);
}
}
传送门 balance_classzone
// kernel/sched.c
/**
* yield - yield the current processor to other threads.
*
* this is a shortcut for kernel-space yielding - it marks the
* thread runnable and calls sys_sched_yield().
*/
void yield(void)
{
set_current_state(TASK_RUNNING);
sys_sched_yield();
schedule();
}
这个函数从 __alloc_pages() 处调用。它负责找到一块足够大的用于分配的内存块。如果没有满足请求的内存块,它就试着寻找更高次的可能被分割开的两个伙伴块。实际的分割由函数 expand() 完成。
// mm/page_alloc.c
static FASTCALL(struct page * rmqueue(zone_t *zone, unsigned int order));
// 参数是要从中分配的区域和页面所需的幂次。
static struct page * rmqueue(zone_t *zone, unsigned int order)
{
// 由于 free_area 是顺序链表的一个数组,幂次可以用作数组中的下标。
free_area_t * area = zone->free_area + order;
unsigned int curr_order = order;
struct list_head *head, *curr;
unsigned long flags;
struct page *page;
// 获取一个管理区锁。
spin_lock_irqsave(&zone->lock, flags);
// 这个 while 块负责找到我们需要分配的页面次。如果空闲块处于我们感兴趣
// 的次中,这里就检查更高的块直到找到更加适合的块。
do {
// head 是该次空闲页块的链表。
head = &area->free_list;
// curr 是页面的第一块。
curr = head->next;
// 如果空闲页块处于这一次,这里就开始分配。
if (curr != head) {
unsigned int index;
// 该页设置为指向空闲块的第一个页面的指针。
page = list_entry(curr, struct page, list);
// 有效性检查保证该页属于这个管理区以及处于 zone_mem_map 中。现在还不
// 清楚是否可能发生在分配器自身将块放到错误的管理区中而不是严重 bug 的情况。
if (BAD_RANGE(zone,page))
BUG();
// 由于该块将被分配,这里就从空闲链表中移除。
list_del(curr);
// index 将 zone_mem_map 看做一个由页面组成的数组,下标就是该数组中的偏移。
index = page - zone->zone_mem_map;
// 表示伙伴对的位置位。MARX_USED() 是一个计算置位的宏。
if (curr_order != MAX_ORDER-1)
MARK_USED(index, curr_order, area);
// 更新该管理区统计。1UL << order 是要分配的页面数。
zone->free_pages -= 1UL << order;
// expand() 是负责分割高次页面块的函数。
page = expand(zone, page, index, order, curr_order, area);
// 不再需要进一步更新管理区,所以这里释放锁。
spin_unlock_irqrestore(&zone->lock, flags);
// 表明页面正在使用中。
set_page_count(page, 1);
// 进行有效性检查。
if (BAD_RANGE(zone,page))
BUG();
if (PageLRU(page))
BUG();
if (PageActive(page))
BUG();
// 由于已经成功分配页面块,所以返回它。
return page;
}
// 如果没有释放正确次的页面块,这里就将转移到高次页面块,看在那里可以找到什么。
curr_order++;
area++;
} while (curr_order < MAX_ORDER);
// 不再需要进一步更新管理区,所以这里释放锁。
spin_unlock_irqrestore(&zone->lock, flags);
// 没有所请求页面块或者更高次页面块可用,所以这里返回失败。
return NULL;
}
传送门 expand
// mm/page_alloc.c
#define MARK_USED(index, order, area) \
__change_bit((index) >> (1+(order)), (area)->map)
这个函数将页面块分割成更高次,直到所需次的页面块可用。
// mm/page_alloc.c
// zone 是从中分配的地方。
// page 是要待分割块的一个页面。
// index 是在 mem_map 中的页面索引。
// low 是需要分配的页面次。
// high 是分配时要分割的页面次。
// area 是代表高次页面块的 free_area_t。
static inline struct page * expand (zone_t *zone, struct page *page,
unsigned long index, int low, int high, free_area_t * area)
{
// size 是待分割的页面数量。
unsigned long size = 1 << high;
// 不断分割,直到找到了所需页面次的一块。
while (high > low) {
// 有效性检查保证该页面属于该管理区,且在 zone_mem_map 中。
if (BAD_RANGE(zone,page))
BUG();
// area 是现在表示低次页面块的下一个 free_area_t。
area--;
// high 是待分割页面块的下一次。
high--;
// 分割块的大小是原来大小的一半。
size >>= 1;
// 在伙伴对中,mem_map 中较低的那一个加入到低次的空闲包表中。
list_add(&(page)->list, &(area)->free_list);
// 表示伙伴对的位置位。
MARK_USED(index, high, area);
// index 是现在新创建伙伴对的第 2 个伙伴索引。
index += size;
// page 现在指向新创建伙伴对的第 2 个伙伴。
page += size;
}
// 有效性检查。
if (BAD_RANGE(zone,page))
BUG();
// 已经成功分割一块。所以返回页面。
return page;
}
这个函数是直接回收路径的一部分。可以睡眠的分配器将调用这个函数同步完成 kswapd 的工作。由于这个进程现在亲自完成工作,所以它释放的特定次的页面保留在一个 current→local_pages 链表中,链表中的页面块数量保存在 current→nr_local_pages 中。注意页面块与页面数量不同,页面块可以是任意次。
// mm/page_alloc.c
static struct page * FASTCALL(
balance_classzone(zone_t *, unsigned int, unsigned int, int *));
static struct page * balance_classzone(zone_t * classzone, unsigned int gfp_mask,
unsigned int order, int * freed)
{
struct page * page = NULL;
int __freed = 0;
// 如果调用者不允许睡眠,这里转到 out 退出该函数。如果发生睡眠,这个函数
// 将必须直接调用,或者 __alloc_pages() 需要被故意中断。
if (!(gfp_mask & __GFP_WAIT))
goto out;
// 这个函数可能不会被突然使用。另外,发生这种条件必须引入故意的损坏。
if (in_interrupt())
BUG();
// 记录在 current->allocation_order 的分配大小。虽然它可能用于将特定次的页面加
// 入到 local_pages 链表中,但实际上这没有用。链表中页面的次存放在 page→index 中。
current->allocation_order = order;
// 设置释放函数的标志以将页面加入 local_list。
current->flags |= PF_MEMALLOC | PF_FREE_PAGES;
// 利用 try_to_free_pages_zone() 直接从特定管理区中释放页面。这也
// 是 kswapd 与直接回收路径交互的地方。
__freed = try_to_free_pages_zone(classzone, gfp_mask);
// 再一次清除标志位,这样释放函数不会继续将页面加入到 local_pages 链表中。
current->flags &= ~(PF_MEMALLOC | PF_FREE_PAGES);
// 假设页面在 local_pages 链表中,这个函数将遍历链表查找属于特定管理区和次的页面块。
// 如果页面存放在局部链表中则仅进入这一块。
if (current->nr_local_pages) {
struct list_head * entry, * local_pages;
struct page * tmp;
int nr_pages;
// 从链表头开始。
local_pages = ¤t->local_pages;
// 如果利用 try_to_free_pages_zone() 释放了页面,则 ......
if (likely(__freed)) {
/* pick from the last inserted so we're lifo */
// 插入的最后一页选择为第 1 页,因为这可能是一次高速缓存命中,而且一般都使用最
// 近引用过的页面。
entry = local_pages->next;
// 遍历链表中所有页面,直到我们找到合适的管理区和次。
do {
// 从链表项中获取页面。
tmp = list_entry(entry, struct page, list);
// 页面块的次存放在 page→index 中,所以这里检查该次是否与请求的次相符以及它是
// 否属于正确的管理区。虽然链表中的页面还不太可能来自于其他管理区,但是如果调用
// swap_out() 将页面直接从进程页表中释放,这种情况还是有可能发生的。
if (tmp->index == order && memclass(page_zone(tmp), classzone)) {
// 这是一个处于正确次和管理区的页面,所以从链表中移除它。
list_del(entry);
// 将链表的页面块数量减 1。
current->nr_local_pages--;
// 设置页面计数为 1,因为它将被释放。
set_page_count(tmp, 1);
// 设置 page 因为它可能返回。需要 tmp 在下一块中释放局部链表中的剩余页。
page = tmp;
// 进行在 __free_pages_ok() 中相同的检查以确保释放该页的安全。
if (page->buffers)
BUG();
if (page->mapping)
BUG();
if (!VALID_PAGE(page))
BUG();
if (PageLocked(page))
BUG();
if (PageLRU(page))
BUG();
if (PageActive(page))
BUG();
if (PageDirty(page))
BUG();
break;
}
// 如果当前页面不在特定的次和管理区中则移到链表中的下一页。
} while ((entry = entry->next) != local_pages);
}
// 这一块释放链表中的剩余页面。
// 获取那些待释放的页面块数量。
nr_pages = current->nr_local_pages;
/* free in reverse order so that the global order will be lifo */
// 循环直到 local_pages 链表变为空。
while ((entry = local_pages->prev) != local_pages) {
// 从链表中移除该页面块。
list_del(entry);
// 获取 struct page 对应的表项。
tmp = list_entry(entry, struct page, list);
// 利用 __free_pages_ok() 释放页面。
__free_pages_ok(tmp, tmp->index);
// 如果页面块的引用计数达到 0,而页面还在链表中,则意味着计数在某个地方被
// 严重地打乱,或者某个人手动将页面加入到 local_pages 链表中,所以这里调用 BUG()。
if (!nr_pages--)
BUG();
}
// 设置页面块数量为 0,因为它们都将被释放。
current->nr_local_pages = 0;
}
out:
// 更新 freed 参数告知调用者总共释放了多少页面。
*freed = __freed;
// 返回请求次和管理区的页面块。如果释放失败,这里将返回 NULL。
return page;
}
传送门 try_to_free_pages_zone
// include/linux/mmzone.h
#define memclass(pgzone, classzone) (((pgzone)->zone_pgdat == (classzone)->zone_pgdat) \
&& ((pgzone) <= (classzone)))
传送门 __free_pages_ok
// include/linux/mm.h
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
// include/linux/mm.h
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask),0)
// mm/page_alloc.c
/*
* Common helper functions.
*/
unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
{
struct page * page;
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
// 返回页面虚拟地址
return (unsigned long) page_address(page);
}
传送门 alloc_pages
// include/linux/mm.h
/*
* Permanent address of a page. Obviously must never be
* called on a highmem page.
*/
#if defined(CONFIG_HIGHMEM) || defined(WANT_PAGE_VIRTUAL)
#define page_address(page) ((page)->virtual)
#else /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */
#define page_address(page) \
__va( (((page) - page_zone(page)->zone_mem_map) << PAGE_SHIFT) \
+ page_zone(page)->zone_start_paddr)
#endif /* CONFIG_HIGHMEM || WANT_PAGE_VIRTUAL */
这是设备驱动器主要关心的函数。它从 DMA 设备中返回适合使用 ZONE_DMA 中的内存。
// include/linux/mm.h
#define __get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA,(order))
// gfp_mask 与 GFP_DMA 进行或操作, 告知分配器从 ZONE_DMA 中分配。
这个函数分配一页,然后将其内容清零。
// mm/page_alloc.c
// gfp_mask 是影响分配器行为的标志位。
unsigned long get_zeroed_page(unsigned int gfp_mask)
{
struct page * page;
// alloc_pages() 完成分配器页面块的工作
page = alloc_pages(gfp_mask, 0);
if (page) {
// page_address() 返回页面的虚拟地址。
void *address = page_address(page);
// clear_page()将页面内容清零。
clear_page(address);
// 返回清零后的页面。
return (unsigned long) address;
}
return 0;
}
传送门 alloc_pages
传送门 page_address
// include/asm-i386/page.h
#ifdef CONFIG_X86_USE_3DNOW
#include
#define clear_page(page) mmx_clear_page((void *)(page))
#define copy_page(to,from) mmx_copy_page(to,from)
#else
/*
* On older X86 processors its not a win to use MMX here it seems.
* Maybe the K6-III ?
*/
#define clear_page(page) memset((void *)(page), 0, PAGE_SIZE)
#define copy_page(to,from) memcpy((void *)(to), (void *)(from), PAGE_SIZE)
#endif
这个函数的调用图如图 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);
}
// 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)
这个函数将完成实际的释放页面工作,并在可能的情况下合并伙伴。
// 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++;
}
每个管理区由一个 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];
}
传送门 6.3 释放页面
这个函数负责初始化所有的区域,并在节点中分配它们的局部 Imem_map(类型 struct page *)。并初始化 pg_data_t 中字段 node_mem_map,node_size,node_start_paddr,node_start_mapnr,nr_zones,node_zones 以及全局 zone_table。
传送门 free_area_init_core
一文看懂物理内存分配算法(伙伴系统)
伙伴算法原理简介
这些函数与页面分配的辅助函数非常相似,因为它们也不完成 “实际” 的工作,它们依赖于 __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);
}
// include/asm/page.h
#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))
传送门 __free_pages
// include/linux/mm.h
// 这个宏仅调用函数 __free_pages() ,参数为 0 幂次和一个页面。
#define __free_page(page) __free_pages((page), 0)
传送门 __free_pages
// include/linux/mm.h
// 这个小宏仅调用 free_pages() 。这个宏与 __free_page() 的主要区别在于这个函数以一个
// 虚拟地址为参数,而 __free_page() 以一个 struct page 为参数
#define free_page(addr) free_pages((addr),0)
传送门 free_pages
这个函数的调用图如图 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);
}
// include/asm-x86_64/pgtable.h
#define PAGE_KERNEL __PTE_SUPP(__PAGE_KERNEL|_PAGE_GLOBAL)
传送门 第7章 非连续内存分配
这个函数有 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;
}
// 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() */
传送门 get_vm_area
传送门 __vmalloc_area_pages
为了给 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;
}
传送门 kmalloc ?
// 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
这个函数只是对 __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);
}
这是标准的页面遍历函数的开始部分。这个顶层函数将遍历在一定地址范围内的所有 PGD。对每个 PGD,它将调用 pmd_alloc() 来分配一个 PMD 目录,然后调用 alloc_area_pmd() 分配目录。
// 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 目录。
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;
}
// 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))
// 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.
*/
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);
}
// 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);
}
这是在地址范围内为分配 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;
}
⇐ ⇒ ⇔ ⇆ ⇒ ⟺
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑿⒀⒁⒂⒃⒄⒅⒆⒇
➊➋➌➍➎➏➐➑➒➓⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵
ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏ
123