Linux中页面分配alloc_pages相关函数
返回清零的页面get_zeroed_page
fastcall unsigned long get_zeroed_page(unsigned int gfp_mask)
{struct page * page;/** get_zeroed_page() returns a 32-bit address, which cannot represent* a highmem page*/BUG_ON(gfp_mask & __GFP_HIGHMEM);page = alloc_pages(gfp_mask, 0);if (page) {void *address = page_address(page);clear_page(address);return (unsigned long) address;}return 0;
}
1. 函数定义
fastcall unsigned long get_zeroed_page(unsigned int gfp_mask)
fastcall
: 一个编译器宏,表示这个函数使用寄存器来传递参数,而不是通过堆栈,这样可以提高函数调用速度unsigned long
: 返回值类型,返回分配页的虚拟地址get_zeroed_page
: 函数名,明确表示获取一个内容全为零的页面unsigned int gfp_mask
: 参数,GFP(Get Free Pages)标志,控制分配行为
2. 局部变量声明
struct page * page;
struct page * page
: 声明一个指向page
结构体的指针,用于保存alloc_pages
返回的页面描述符
3. HIGHMEM 检查断言
/** get_zeroed_page() returns a 32-bit address, which cannot represent* a highmem page*/BUG_ON(gfp_mask & __GFP_HIGHMEM);
- 这个函数返回32位地址,无法表示高端内存页面
BUG_ON(gfp_mask & __GFP_HIGHMEM)
:__GFP_HIGHMEM
标志表示允许从高端内存区域分配页面。BUG_ON(condition)
是一个内核宏,如果条件为真,会触发内核错误导致系统崩溃- 作用: 确保调用者没有请求高端内存,因为此函数无法处理高端内存页面
4. 分配单个页面
page = alloc_pages(gfp_mask, 0);
alloc_pages(gfp_mask, 0)
: 核心分配函数调用gfp_mask
: GFP分配标志,如GFP_KERNEL
,GFP_ATOMIC
等0
: 阶数参数,2^0 = 1
,表示分配单个页面- 返回值: 成功返回
struct page *
,失败返回NULL
5. 页面处理逻辑
if (page) {void *address = page_address(page);clear_page(address);return (unsigned long) address;}
if (page)
: 检查页面分配是否成功void *address = page_address(page)
:page_address(page)
: 将struct page *
转换为内核虚拟地址- 这个函数只对低端内存有效,因为低端内存有直接的线性映射
clear_page(address)
:- 内核函数,将指定页面内容全部清零
return (unsigned long) address
:- 将虚拟地址转换为
unsigned long
类型并返回 - 这就是调用者得到的清零后的页面地址
- 将虚拟地址转换为
6. 分配失败处理
return 0;
- 执行条件: 当
alloc_pages
返回NULL
(分配失败)时执行 - 返回值: 返回
0
,表示分配失败
内核页面分配器alloc_pages
#define alloc_pages(gfp_mask, order) \alloc_pages_node(numa_node_id(), gfp_mask, order)
static inline struct page *alloc_pages_node(int nid, unsigned int gfp_mask,unsigned int order)
{if (unlikely(order >= MAX_ORDER))return NULL;return __alloc_pages(gfp_mask, order,NODE_DATA(nid)->node_zonelists + (gfp_mask & GFP_ZONEMASK));
}
struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,struct zonelist *zonelist)
{const int wait = gfp_mask & __GFP_WAIT;unsigned long min;struct zone **zones, *z;struct page *page;struct reclaim_state reclaim_state;struct task_struct *p = current;int i;int alloc_type;int do_retry;int can_try_harder;might_sleep_if(wait);/** The caller may dip into page reserves a bit more if the caller* cannot run direct reclaim, or is the caller has realtime scheduling* policy*/can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;zones = zonelist->zones; /* the list of zones suitable for gfp_mask */if (unlikely(zones[0] == NULL)) {/* Should this ever happen?? */return NULL;}alloc_type = zone_idx(zones[0]);/* Go through the zonelist once, looking for a zone with enough free */for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_low + (1<<order) + z->protection[alloc_type];if (z->free_pages < min)continue;page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}for (i = 0; (z = zones[i]) != NULL; i++)wakeup_kswapd(z);/** Go through the zonelist again. Let __GFP_HIGH and allocations* coming from realtime tasks to go deeper into reserves*/for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_min;if (gfp_mask & __GFP_HIGH)min /= 2;if (can_try_harder)min -= min / 4;min += (1<<order) + z->protection[alloc_type];if (z->free_pages < min)continue;page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}/* This allocation should allow future memory freeing. */if ((p->flags & (PF_MEMALLOC | PF_MEMDIE)) && !in_interrupt()) {/* go through the zonelist yet again, ignoring mins */for (i = 0; (z = zones[i]) != NULL; i++) {page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}goto nopage;}/* Atomic allocations - we can't balance anything */if (!wait)goto nopage;rebalance:/* We now go into synchronous reclaim */p->flags |= PF_MEMALLOC;reclaim_state.reclaimed_slab = 0;p->reclaim_state = &reclaim_state;try_to_free_pages(zones, gfp_mask, order);p->reclaim_state = NULL;p->flags &= ~PF_MEMALLOC;/* go through the zonelist yet one more time */for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_min;if (gfp_mask & __GFP_HIGH)min /= 2;if (can_try_harder)min -= min / 4;min += (1<<order) + z->protection[alloc_type];if (z->free_pages < min)continue;page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}/** Don't let big-order allocations loop unless the caller explicitly* requests that. Wait for some write requests to complete then retry.** In this implementation, __GFP_REPEAT means __GFP_NOFAIL for order* <= 3, but that may not be true in other implementations.*/do_retry = 0;if (!(gfp_mask & __GFP_NORETRY)) {if ((order <= 3) || (gfp_mask & __GFP_REPEAT))do_retry = 1;if (gfp_mask & __GFP_NOFAIL)do_retry = 1;}if (do_retry) {blk_congestion_wait(WRITE, HZ/50);goto rebalance;}nopage:if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {printk(KERN_WARNING "%s: page allocation failure."" order:%d, mode:0x%x\n",p->comm, order, gfp_mask);dump_stack();}return NULL;
got_pg:zone_statistics(zonelist, z);kernel_map_pages(page, 1 << order, 1);return page;
}
1. 函数定义和宏展开
#define alloc_pages(gfp_mask, order) \alloc_pages_node(numa_node_id(), gfp_mask, order)
- 作用:
alloc_pages
宏展开为alloc_pages_node
,自动传入当前NUMA节点ID。
static inline struct page *alloc_pages_node(int nid, unsigned int gfp_mask,unsigned int order)
{if (unlikely(order >= MAX_ORDER))return NULL;return __alloc_pages(gfp_mask, order,NODE_DATA(nid)->node_zonelists + (gfp_mask & GFP_ZONEMASK));
}
-
参数:
nid
: NUMA节点IDgfp_mask
: GFP分配标志order
: 分配阶数(2^order个页面)
-
检查:
order >= MAX_ORDER
时直接返回NULL(阶数过大) -
计算
zonelist
: 根据节点数据和GFP掩码中的区域掩码选择合适的内存区域列表
NODE_DATA(nid)
- 作用: 获取指定NUMA节点的描述符
->node_zonelists
- 作用: 访问节点的
zonelist
数组 - 说明: 每个NUMA节点有多个
zonelist
,用于不同的分配策略
GFP_ZONEMASK
- 作用: 从
gfp_mask
中提取zone类型选择位
gfp_mask & GFP_ZONEMASK
- 作用: 提取GFP掩码中的zone选择标志
示例1: 普通内核分配
gfp_mask = GFP_KERNEL; // 通常不包含特殊zone标志 gfp_mask & GFP_ZONEMASK = 0; NODE_DATA(nid)->node_zonelists + 0 // 使用默认zonelist[0]
struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,struct zonelist *zonelist)
- 函数定义: 实际的页面分配核心函数
- 参数:
gfp_mask
: 分配标志order
: 分配阶数zonelist
: 适合的内存区域列表
2. 变量声明
const int wait = gfp_mask & __GFP_WAIT;unsigned long min;struct zone **zones, *z;struct page *page;struct reclaim_state reclaim_state;struct task_struct *p = current;int i;int alloc_type;int do_retry;int can_try_harder;
wait
: 判断是否允许等待和回收min
: 计算每个zone需要的最小空闲页面zones
,z
: 内存区域数组和当前区域指针page
: 返回的页面指针reclaim_state
: 页面回收状态p = current
: 当前进程指针- 其他: 循环计数器、分配类型、重试标志等
3. 初始检查和设置
might_sleep_if(wait);
- 作用: 如果允许等待,那么这里可能睡眠,在原子上下文中将报错
can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;
- 计算: 实时任务且不在中断上下文,或者不允许等待时,可以更努力尝试分配
zones = zonelist->zones; /* the list of zones suitable for gfp_mask */
- 获取: 从
zonelist
中提取实际的zone数组
if (unlikely(zones[0] == NULL)) {/* Should this ever happen?? */return NULL;}
- 检查: 如果zone列表为空(不应该发生),直接返回NULL
alloc_type = zone_idx(zones[0]);
-
记录: 第一个zone的索引作为分配类型
-
#define zone_idx(zone) ((zone) - (zone)->zone_pgdat->node_zones)
(zone)->zone_pgdat
- 作用: 访问该zone所属的NUMA节点描述符
- 关系: 每个zone都知道它属于哪个NUMA节点
(zone)->zone_pgdat->node_zones
- 作用: 访问该节点中zones数组的起始地址
- 类型:
struct zone node_zones[MAX_NR_ZONES]
(zone数组) - 说明: 这是节点中所有内存区域的数组
(zone) - (zone)->zone_pgdat->node_zones
- 操作: 指针减法
- 含义: 计算目标zone在节点zones数组中的偏移量(即索引)
4. 第一次分配尝试 - 宽松条件
/* Go through the zonelist once, looking for a zone with enough free */for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_low + (1<<order) + z->protection[alloc_type];
-
循环: 遍历所有合适的zone
-
min计算:
pages_low
(低水位线) + 所需页面数 + 保护值z->pages_low
- 低水位线- 含义: 当空闲页面低于这个值时,系统开始压力回收
(1<<order)
- 请求的页面数量- 作用: 计算本次分配请求需要多少连续页面
<<
操作: 左移运算,相当于2的order次方
z->protection[alloc_type]
- 保护机制protection
数组: 每个zone对其他zone的保护值alloc_type
: 之前计算的zone_idx(zones[0])
- 作用: 防止高优先级zone耗尽低优先级zone的内存
if (z->free_pages < min)continue;
- 检查: 如果空闲页面不足,跳过这个zone
page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}
- 尝试分配: 从zone的per-CPU缓存中分配页面
- 成功: 跳转到
got_pg
标签返回页面
5. 唤醒kswapd
和第二次尝试 - 中等条件
for (i = 0; (z = zones[i]) != NULL; i++)wakeup_kswapd(z);
- 唤醒回收: 唤醒每个zone的
kswapd
内核线程开始异步页面回收
for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_min;if (gfp_mask & __GFP_HIGH)min /= 2;if (can_try_harder)min -= min / 4;min += (1<<order) + z->protection[alloc_type];
- 新的min计算:
- 基础:
pages_min
(最小水位线) __GFP_HIGH
: 降低要求一半can_try_harder
: 再降低25%- 加上所需页面和保护值
- 基础:
if (z->free_pages < min)continue;page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}
- 再次尝试分配: 使用更宽松的条件
6. 内存分配者特权路径
if ((p->flags & (PF_MEMALLOC | PF_MEMDIE)) && !in_interrupt()) {
- 条件: 当前进程是内存分配者或正在死亡,且不在中断上下文
for (i = 0; (z = zones[i]) != NULL; i++) {page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}goto nopage;}
- 特权分配: 忽略所有水位线限制,直接尝试分配
PF_MEMALLOC
: 确保内存回收等关键操作永远有内存可用PF_MEMDIE
: 确保被杀死进程能够优雅退出- 失败: 跳转到
nopage
7. 原子分配处理
if (!wait)goto nopage;
- 原子分配: 不允许等待时直接失败
8. 同步回收路径
rebalance:p->flags |= PF_MEMALLOC;reclaim_state.reclaimed_slab = 0;p->reclaim_state = &reclaim_state;
- 设置标志: 标记为内存分配者
- 初始化: 设置回收状态
try_to_free_pages(zones, gfp_mask, order);
- 核心回收: 同步尝试释放页面
p->reclaim_state = NULL;p->flags &= ~PF_MEMALLOC;
- 清理: 恢复进程状态
for (i = 0; (z = zones[i]) != NULL; i++) {min = z->pages_min;if (gfp_mask & __GFP_HIGH)min /= 2;if (can_try_harder)min -= min / 4;min += (1<<order) + z->protection[alloc_type];if (z->free_pages < min)continue;page = buffered_rmqueue(z, order, gfp_mask);if (page)goto got_pg;}
- 第三次尝试: 回收后再次尝试分配
9. 重试逻辑
do_retry = 0;if (!(gfp_mask & __GFP_NORETRY)) {if ((order <= 3) || (gfp_mask & __GFP_REPEAT))do_retry = 1;if (gfp_mask & __GFP_NOFAIL)do_retry = 1;}
- 重试条件:
- 没有设置
__GFP_NORETRY
- 小阶数(≤8页)或设置了
__GFP_REPEAT
- 设置了
__GFP_NOFAIL
(必须成功)
- 没有设置
if (do_retry) {blk_congestion_wait(WRITE, HZ/50);goto rebalance;}
- 实际重试: 等待IO拥塞缓解,然后回到回收路径
10. 失败处理
nopage:if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {printk(KERN_WARNING "%s: page allocation failure."" order:%d, mode:0x%x\n",p->comm, order, gfp_mask);dump_stack();}return NULL;
- 警告: 除非设置了
__GFP_NOWARN
,否则打印分配失败信息 - 返回: 返回NULL表示失败
11. 成功处理
got_pg:zone_statistics(zonelist, z);kernel_map_pages(page, 1 << order, 1);return page;
- 统计: 更新zone统计信息
- 映射: 内核映射页面
- 返回: 返回分配的页面
处理块设备IO拥塞的等待函blk_congestion_wait
long blk_congestion_wait(int rw, long timeout)
{long ret;DEFINE_WAIT(wait);wait_queue_head_t *wqh = &congestion_wqh[rw];prepare_to_wait(wqh, &wait, TASK_UNINTERRUPTIBLE);ret = io_schedule_timeout(timeout);finish_wait(wqh, &wait);return ret;
}
1. 函数定义
long blk_congestion_wait(int rw, long timeout)
- 返回值:
long
- 剩余的等待时间 - 参数:
rw
: 读写方向,READ
或WRITE
timeout
: 最大等待时间(单位:jiffies)
2. 逐行代码解析
2.1. 变量声明
long ret;DEFINE_WAIT(wait);wait_queue_head_t *wqh = &congestion_wqh[rw];
long ret
:
- 存储函数返回值,表示剩余的等待时间
DEFINE_WAIT(wait)
:
- 宏,定义并初始化一个等待队列条目
wait_queue_head_t *wqh = &congestion_wqh[rw]
:
congestion_wqh
: 全局的拥塞等待队列数组rw
作为索引:[READ]
和[WRITE]
分别有独立的等待队列- 这样读写操作可以在不同的队列中等待,互不干扰
2.2. 准备等待
prepare_to_wait(wqh, &wait, TASK_UNINTERRUPTIBLE);
作用: 将当前进程加入到拥塞等待队列,并设置进程状态。
参数:
wqh
: 等待队列头(读或写队列)&wait
: 等待队列条目TASK_UNINTERRUPTIBLE
: 进程状态 - 不可中断睡眠
不可中断睡眠的特点:
- 进程不会响应信号(如Ctrl+C)
- 只能被特定的唤醒操作唤醒
- 适用于必须完成的关键操作
2.3. 调度超时
ret = io_schedule_timeout(timeout);
io_schedule_timeout(timeout)
:
- 让出CPU,进入睡眠状态
- 最多睡眠
timeout
个jiffies - 返回剩余的等待时间
工作流程:
- 将当前进程状态设置为睡眠
- 调用调度器选择其他进程运行
- 当被唤醒或超时时,重新被调度执行
- 返回剩余的时间:
剩余时间 = timeout - 已等待时间
返回值含义:
> 0
: 超时前被唤醒,返回剩余时间= 0
: 正常超时< 0
: 错误情况
2.4. 完成等待
finish_wait(wqh, &wait);
作用: 清理等待状态,将进程从等待队列中移除。
具体操作:
- 将进程状态设置回
TASK_RUNNING
(可运行) - 从等待队列
wqh
中移除wait
条目 - 如果进程已经被自动移除(被唤醒时),则不做重复操作
2.5. 返回结果
return ret;
返回剩余的等待时间给调用者。
3. 拥塞管理机制
拥塞触发条件
当块设备的请求队列过于繁忙时:
- 太多未完成的IO请求
- 设备处理能力达到上限
- 需要等待一些IO完成才能继续
4. 设计意义
- 避免忙等待: 让出CPU而不是循环检查,提高系统效率
- 响应拥塞: 感知块设备状态,在拥塞时适当等待
- 协同工作: 内存分配与IO子系统协同,共同应对系统压力
- 避免活锁: 在内存紧张时通过短暂等待打破竞争状态
页面分配buffered_rmqueue
static struct page *
buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
{unsigned long flags;struct page *page = NULL;int cold = !!(gfp_flags & __GFP_COLD);if (order == 0) {struct per_cpu_pages *pcp;pcp = &zone->pageset[get_cpu()].pcp[cold];local_irq_save(flags);if (pcp->count <= pcp->low)pcp->count += rmqueue_bulk(zone, 0,pcp->batch, &pcp->list);if (pcp->count) {page = list_entry(pcp->list.next, struct page, lru);list_del(&page->lru);pcp->count--;}local_irq_restore(flags);put_cpu();}if (page == NULL) {spin_lock_irqsave(&zone->lock, flags);page = __rmqueue(zone, order);spin_unlock_irqrestore(&zone->lock, flags);}if (page != NULL) {BUG_ON(bad_range(zone, page));mod_page_state_zone(zone, pgalloc, 1 << order);prep_new_page(page, order);if (order && (gfp_flags & __GFP_COMP))prep_compound_page(page, order);}return page;
}
1. 函数定义和变量声明
static struct page *
buffered_rmqueue(struct zone *zone, int order, int gfp_flags)
{unsigned long flags;struct page *page = NULL;int cold = !!(gfp_flags & __GFP_COLD);
static struct page *
: 静态函数,返回页面指针zone
: 要从哪个内存区域分配order
: 分配阶数(2^order个连续页面)gfp_flags
: GFP分配标志flags
: 保存中断状态,用于本地中断控制page = NULL
: 初始化返回的页面指针为NULLcold = !!(gfp_flags & __GFP_COLD)
:gfp_flags & __GFP_COLD
: 检查是否请求冷页面!!
: 双重逻辑非,让结果转换成0或者1- 冷页面: 不太可能被很快访问的页面,放在Per-CPU列表的尾部
- 热页面: 可能被很快访问的页面,放在Per-CPU列表的头部
2. 单页分配路径(order == 0)
if (order == 0) {struct per_cpu_pages *pcp;pcp = &zone->pageset[get_cpu()].pcp[cold];
if (order == 0)
: 单页分配的特殊优化路径struct per_cpu_pages *pcp
: Per-CPU页面缓存结构指针get_cpu()
: 获取当前CPU ID并禁用内核抢占zone->pageset[get_cpu()].pcp[cold]
:- 获取当前CPU在指定zone的pageset
pcp[cold]
: 根据冷热页面选择对应的Per-CPU列表pcp[0]
: 热页面列表,pcp[1]
: 冷页面列表
local_irq_save(flags);
- 保存本地中断状态并禁用中断,防止并发访问Per-CPU缓存
if (pcp->count <= pcp->low)pcp->count += rmqueue_bulk(zone, 0,pcp->batch, &pcp->list);
pcp->count
: 当前Per-CPU缓存中的页面数量pcp->low
: 低水位线,当页面数量低于此值时需要补充pcp->batch
: 每次批量补充的页面数量rmqueue_bulk(zone, 0, pcp->batch, &pcp->list)
:- 从zone的伙伴系统中批量分配单个页面
- 参数:zone, order=0, 数量=batch, 添加到
pcp->list
- 返回实际分配的页面数量
- 作用: 如果缓存不足,从伙伴系统批量补充页面
if (pcp->count) {page = list_entry(pcp->list.next, struct page, lru);list_del(&page->lru);pcp->count--;}
if (pcp->count)
: 如果缓存中有页面可用list_entry(pcp->list.next, struct page, lru)
:- 从链表头部获取第一个页面
list_entry
宏:通过成员指针获取包含它的结构体指针pcp->list.next
: 链表中的第一个页面lru
: 页面结构体中用于链接的list_head成员
list_del(&page->lru)
: 从链表中删除这个页面pcp->count--
: 减少缓存中的页面计数
local_irq_restore(flags);put_cpu();}
local_irq_restore(flags)
: 恢复中断状态put_cpu()
: 启用内核抢占
3. 多页分配或单页分配失败路径
if (page == NULL) {spin_lock_irqsave(&zone->lock, flags);page = __rmqueue(zone, order);spin_unlock_irqrestore(&zone->lock, flags);}
if (page == NULL)
: 如果Per-CPU缓存分配失败(或order>0)spin_lock_irqsave(&zone->lock, flags)
: 获取zone锁并保存中断状态page = __rmqueue(zone, order)
: 核心函数,从伙伴系统分配连续页面spin_unlock_irqrestore(&zone->lock, flags)
: 释放zone锁并恢复中断
4. 页面后期处理
if (page != NULL) {BUG_ON(bad_range(zone, page));
if (page != NULL)
: 如果成功分配到页面BUG_ON(bad_range(zone, page))
:bad_range(zone, page)
: 检查页面是否在zone的有效范围内BUG_ON()
: 如果检查失败,触发内核错误(严重bug)
mod_page_state_zone(zone, pgalloc, 1 << order);
- 更新页面分配统计信息:
zone
: 目标内存区域pgalloc
: 页面分配计数器1 << order
: 分配的页面数量
prep_new_page(page, order);
- 准备新页面:
- 设置页面标志位
- 初始化页面结构
- 清除页面内容(如果需要)
if (order && (gfp_flags & __GFP_COMP))prep_compound_page(page, order);}
if (order && (gfp_flags & __GFP_COMP))
:- 如果分配多个页面且请求复合页面
__GFP_COMP
: 允许复合页面(用于大页等)
prep_compound_page(page, order)
: 设置复合页面结构
return page;
}
- 返回分配的页面指针(成功)或NULL(失败)
从内存区域批量分配页面块到指定链表rmqueue_bulk
static int rmqueue_bulk(struct zone *zone, unsigned int order,unsigned long count, struct list_head *list)
{unsigned long flags;int i;int allocated = 0;struct page *page;spin_lock_irqsave(&zone->lock, flags);for (i = 0; i < count; ++i) {page = __rmqueue(zone, order);if (page == NULL)break;allocated++;list_add_tail(&page->lru, list);}spin_unlock_irqrestore(&zone->lock, flags);return allocated;
}
1. 函数定义和变量声明
static int rmqueue_bulk(struct zone *zone, unsigned int order,unsigned long count, struct list_head *list)
static int
: 静态函数,只在当前文件内可见,返回实际分配的页面块数量zone
: 目标内存区域指针,要从这个zone分配页面order
: 分配阶数,每个页面块包含 2^order 个连续物理页面count
: 请求分配的页面块数量list
: 目标链表头,分配的页面块将添加到这个链表中
unsigned long flags;
flags
: 用于保存中断状态的无符号长整型变量,在操作自旋锁时使用
int i;
i
: 循环计数器,用于控制分配循环的次数
int allocated = 0;
allocated
: 实际成功分配的页面块计数器,初始化为0
struct page *page;
page
: 指向分配到的页面块的指针,struct page
是内核中表示物理页面的数据结构
2. 加锁和循环分配
spin_lock_irqsave(&zone->lock, flags);
spin_lock_irqsave(&zone->lock, flags)
:- 获取zone结构的自旋锁,并保存当前中断状态到flags变量中
- 这会禁用本地CPU的中断,防止中断处理程序并发访问zone的空闲列表
- 保护zone的空闲页面数据结构不被多个CPU同时修改
for (i = 0; i < count; ++i) {
- 循环开始: 尝试分配
count
个页面块i = 0
: 循环计数器从0开始i < count
: 循环条件,最多分配count次++i
: 每次循环后计数器加1
page = __rmqueue(zone, order);
- 核心分配调用:
__rmqueue(zone, order)
: 从zone的伙伴系统中分配一个2^order大小的连续页面块- 这是实际执行伙伴算法进行内存分配的函数
- 返回指向第一个页面的
struct page
指针,如果分配失败则返回NULL
if (page == NULL)break;
- 分配失败处理:
- 检查
__rmqueue
是否返回NULL(表示分配失败) - 如果分配失败,立即执行
break
语句跳出循环 - 此时
allocated
变量记录的是已经成功分配的页面块数量
- 检查
allocated++;
- 成功分配计数:
- 只有当页面分配成功时才会执行到这里
allocated++
: 将成功分配的页面块计数器加1
list_add_tail(&page->lru, list);
-
页面添加到链表:
&page->lru
: 获取页面结构中链表节点的地址lru
是struct page
中用于链接的list_head
成员
list_add_tail(&page->lru, list)
: 将页面添加到链表的尾部
-
循环结束: 循环体结束,可能因以下原因退出:
- 正常完成:成功分配了
count
个页面块(i == count
) - 提前退出:分配失败跳出(
i < count
)
- 正常完成:成功分配了
3. 清理和返回
spin_unlock_irqrestore(&zone->lock, flags);
- 释放锁和恢复中断:
spin_unlock_irqrestore(&zone->lock, flags)
:- 释放zone的自旋锁,允许其他CPU访问zone
- 恢复中断状态到加锁前的设置(重新启用中断如果之前是开启的)
flags
参数确保正确恢复之前的中断状态
return allocated;
- 返回结果:
- 返回实际成功分配的页面块数量
- 如果全部成功,
allocated == count
- 如果部分成功,
0 < allocated < count
- 如果完全失败,
allocated == 0
分配连续物理页面块__rmqueue
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{struct free_area * area;unsigned int current_order;struct page *page;unsigned int index;for (current_order = order; current_order < MAX_ORDER; ++current_order) {area = zone->free_area + current_order;if (list_empty(&area->free_list))continue;page = list_entry(area->free_list.next, struct page, lru);list_del(&page->lru);index = page - zone->zone_mem_map;if (current_order != MAX_ORDER-1)MARK_USED(index, current_order, area);zone->free_pages -= 1UL << order;return expand(zone, page, index, order, current_order, area);}return NULL;
}
1. 函数功能
从指定内存区域的伙伴系统中分配指定阶数的连续物理页面块。这是伙伴分配器的核心实现,通过从高阶到低阶的搜索和拆分机制来满足分配请求
2. 逐行代码解析
static struct page *__rmqueue(struct zone *zone, unsigned int order)
{
static struct page *
: 静态函数,返回分配的页面块指针zone
: 目标内存区域order
: 请求的分配阶数(需要 2^order 个连续页面)
struct free_area * area;
area
: 指向free_area结构的指针,用于访问不同阶数的空闲链表
unsigned int current_order;
current_order
: 当前搜索的阶数,从请求的order开始向上搜索
struct page *page;
page
: 指向分配到的页面块的指针
unsigned int index;
index
: 页面在zone内存映射数组中的索引
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
- 循环开始: 从请求的order开始,向高阶搜索直到MAX_ORDER
current_order = order
: 初始化当前阶数为请求的阶数current_order < MAX_ORDER
: 循环条件,不超过最大阶数++current_order
: 每次循环阶数加1,向更高阶搜索
area = zone->free_area + current_order;
- 获取当前阶数的free_area:
zone->free_area
: 指向zone的free_area数组起始位置zone->free_area + current_order
: 计算第current_order阶的free_area地址- free_area数组按阶数索引,每个元素管理该阶数的空闲页面块
if (list_empty(&area->free_list))continue;
- 检查空闲链表是否为空:
list_empty(&area->free_list)
: 检查该阶数的空闲链表是否为空- 如果为空,执行
continue
跳过当前阶数,继续搜索更高阶数 - 这实现了"最佳适配"策略:使用能满足要求的最小可用块
page = list_entry(area->free_list.next, struct page, lru);
- 从链表中获取页面:
area->free_list.next
: 空闲链表的第一个节点list_entry(area->free_list.next, struct page, lru)
:- 通过链表节点指针获取包含它的struct page结构体指针
lru
是struct page中用于链接的list_head成员
list_del(&page->lru);
- 从空闲链表中删除页面:
list_del(&page->lru)
: 将页面从其所在的空闲链表中移除- 现在这个页面块不再属于空闲状态
index = page - zone->zone_mem_map;
- 计算页面索引:
page - zone->zone_mem_map
: 指针减法,计算页面在zone内存映射中的索引zone->zone_mem_map
: 指向zone中第一个页面的指针- 结果是页面在zone中的偏移量(页面号)
if (current_order != MAX_ORDER-1)MARK_USED(index, current_order, area);
- 标记页面块为已使用:
current_order != MAX_ORDER-1
: 如果不是最高阶,需要标记位图MARK_USED(index, current_order, area)
:- 宏,在位图中标记该页面块为已使用
- 参数:页面索引、当前阶数、free_area指针
- 用于伙伴系统的合并操作时判断相邻块的状态
zone->free_pages -= 1UL << order;
- 更新zone的空闲页面计数:
1UL << order
: 计算实际分配的页面数量(2^order)zone->free_pages -= ...
: 从zone的总空闲页面中减去分配的数量- 注意:这里减去的是请求的order对应的页面数,不是当前order
return expand(zone, page, index, order, current_order, area);
-
调用expand处理块拆分:
expand(zone, page, index, order, current_order, area)
:- 如果当前阶数大于请求阶数,需要拆分大的页面块
- 返回请求阶数的页面块,将剩余部分放回空闲链表
-
循环结束: 如果所有阶数都搜索完毕仍未找到可用块,继续执行
return NULL;
- 分配失败: 返回NULL指针,表示无法满足分配请
3. 关键设计要点
-
最佳适配策略:
- 从请求阶数开始向上搜索,使用能满足要求的最小可用块
- 减少内存碎片,提高内存利用率
-
伙伴系统核心:
- 通过高阶块的拆分来满足低阶请求
- 通过低阶块的合并来形成高阶块
-
位图管理:
MARK_USED
设置位图标志,记录页面块状态- 用于后续的伙伴合并判断
拆分高阶连续页面块expand
static inline struct page *
expand(struct zone *zone, struct page *page,unsigned long index, int low, int high, struct free_area *area)
{unsigned long size = 1 << high;while (high > low) {area--;high--;size >>= 1;BUG_ON(bad_range(zone, &page[size]));list_add(&page[size].lru, &area->free_list);MARK_USED(index + size, high, area);}return page;
}
1. 函数功能
将高阶的连续页面块拆分为请求的低阶页面块。这是伙伴分配器的核心拆分机制,将一个大内存块逐步拆分成所需大小的块,并将剩余部分放回相应的空闲链表
2. 逐行代码解析
static inline struct page *
expand(struct zone *zone, struct page *page,unsigned long index, int low, int high, struct free_area *area)
{
static inline struct page *
: 静态内联函数,返回拆分后的页面块指针zone
: 目标内存区域page
: 要拆分的高阶页面块的起始页面指针index
: 起始页面在zone内存映射中的索引low
: 请求的低阶数(目标阶数)high
: 当前的高阶数(实际找到的阶数)area
: 当前高阶数对应的free_area指针
unsigned long size = 1 << high;
- 计算当前块的大小:
1 << high
: 计算高阶块包含的页面数量size
表示当前要拆分的块的总页面数
while (high > low) {
- 循环拆分条件:
high > low
: 当当前阶数大于目标阶数时继续循环- 循环将高阶块逐步拆分成低阶块,直到达到目标阶数
area--;
- 移动到低一级的free_area:
area--
: 将free_area指针指向前一个(低一阶的)free_area- 因为free_area数组是按阶数递增排列的:
&free_area[0]
,&free_area[1]
, … area--
指向更小阶数的空闲区域管理结构
high--;
- 降低当前阶数:
high--
: 当前阶数减1- 例如:从order3降到order2,然后order2降到order1
size >>= 1;
- 块大小减半:
size >>= 1
: 右移一位,相当于除以2- 每次拆分块大小减半,符合伙伴系统的二分特性
- 例如:size从8→4→2(当high从3→1时)
BUG_ON(bad_range(zone, &page[size]));
- 范围检查:
&page[size]
: 计算后半部分块的起始页面指针page
是前半部分块的起始page + size
是后半部分块的起始
bad_range(zone, &page[size])
: 检查后半部分块是否在zone的有效范围内BUG_ON(...)
: 如果范围检查失败,触发内核错误(严重bug)
list_add(&page[size].lru, &area->free_list);
- 将后半部分块加入空闲链表:
&page[size].lru
: 后半部分块的链表节点地址&area->free_list
: 当前阶数的空闲链表头list_add(&page[size].lru, &area->free_list)
:- 将后半部分块添加到对应阶数的空闲链表头部
- 使用头插法,新块成为链表的第一个元素
MARK_USED(index + size, high, area);
-
标记后半部分块为已使用(在空闲链表中):
index + size
: 后半部分块的起始页面索引index
是原始块的起始索引index + size
是后半部分块的起始索引
high
: 当前阶数(后半部分块的阶数)area
: 当前阶数的free_area指针MARK_USED(index + size, high, area)
:- 在位图中标记该块的状态
- 虽然块在空闲链表中,但标记为"已使用"是指它在伙伴系统管理下
-
循环结束: 当
high == low
时退出循环,此时:- 原始高阶块已被拆分成目标阶数的块
- 所有剩余的半块都已放入相应的空闲链表
return page;
- 返回前半部分块:
- 返回原始页面指针
page
,现在它指向一个大小为1 << low
的块 - 这个块是拆分后留给调用者使用的目标块
- 返回原始页面指针
原子更新页面状态mod_page_state_zone
#define mod_page_state_zone(zone, member, delta) \do { \unsigned long flags; \local_irq_save(flags); \if (is_highmem(zone)) \__get_cpu_var(page_states).member##_high += (delta);\else if (is_normal(zone)) \__get_cpu_var(page_states).member##_normal += (delta);\else \__get_cpu_var(page_states).member##_dma += (delta);\local_irq_restore(flags); \} while (0)
1. 宏功能
以原子方式更新指定内存区域(zone)的页面统计状态。该宏根据zone的类型(DMA、NORMAL、HIGHMEM)将增量值安全地添加到对应的Per-CPU页面统计变量中。
2. 逐行代码解析
#define mod_page_state_zone(zone, member, delta) \
- 宏定义开始:
mod_page_state_zone
: 宏名称zone
: 内存区域指针参数member
: 统计成员名称参数(如pgalloc
、pgfree
等)delta
: 增量值参数(正数表示增加,负数表示减少)
unsigned long flags; \
- 中断状态变量:
flags
: 用于保存当前中断状态的无符号长整型变量- 在禁用/启用中断时使用,确保正确恢复中断状态
local_irq_save(flags); \
- 保存中断状态并禁用本地中断:
local_irq_save(flags)
: 宏,执行以下操作:- 保存当前CPU的中断使能状态到
flags
变量 - 禁用当前CPU的中断
- 保存当前CPU的中断使能状态到
- 目的:防止中断处理程序并发修改Per-CPU统计变量
if (is_highmem(zone)) \
- 检查是否为高端内存区域:
is_highmem(zone)
: 宏或函数,检查zone是否为HIGHMEM类型- 返回真表示这是高端内存区域
__get_cpu_var(page_states).member##_high += (delta);\
- 更新高端内存统计:
__get_cpu_var(page_states)
: 获取当前CPU的page_states变量page_states
是Per-CPU变量,每个CPU有自己独立的副本- 避免多CPU间的锁竞争
.member##_high
:- 将
member
参数和_high
拼接成完整的成员名 - 例如:如果
member
是pgalloc
,则变成pgalloc_high
- 将
+= (delta)
: 将增量值加到对应的统计变量
else if (is_normal(zone)) \
- 检查是否为普通内存区域:
is_normal(zone)
: 检查zone是否为NORMAL类型- 如果不是HIGHMEM,检查是否是NORMAL区域
__get_cpu_var(page_states).member##_normal += (delta);\
- 更新普通内存统计:
member##_normal
: 将member
和_normal
拼接- 例如:
pgalloc_normal
- 将增量值加到普通内存的统计变量
else \
- 默认情况(DMA内存区域):
- 如果既不是HIGHMEM也不是NORMAL,则假定为DMA区域
__get_cpu_var(page_states).member##_dma += (delta);\
- 更新DMA内存统计:
member##_dma
: 将member
和_dma
拼接- 例如:
pgalloc_dma
- 将增量值加到DMA内存的统计变量
local_irq_restore(flags); \
- 恢复中断状态:
local_irq_restore(flags)
: 恢复之前保存的中断状态- 如果之前中断是启用的,则重新启用中断
- 如果之前是禁用的,保持禁用状态
} while (0)
- 宏定义结束:
while (0)
确保循环只执行一次- 这是一种标准的宏封装技术
初始化新分配的页面prep_new_page
static void prep_new_page(struct page *page, int order)
{if (page->mapping || page_mapped(page) ||(page->flags & (1 << PG_private |1 << PG_locked |1 << PG_lru |1 << PG_active |1 << PG_dirty |1 << PG_reclaim |1 << PG_swapcache |1 << PG_writeback )))bad_page(__FUNCTION__, page);page->flags &= ~(1 << PG_uptodate | 1 << PG_error |1 << PG_referenced | 1 << PG_arch_1 |1 << PG_checked | 1 << PG_mappedtodisk);page->private = 0;set_page_refs(page, order);
}
1. 函数功能
准备和初始化新分配的页面,确保页面处于干净可用的状态。该函数执行页面状态验证、标志位清理和引用计数设置,是新分配页面的质量保证机制
2. 逐行代码解析
static void prep_new_page(struct page *page, int order)
{
static void
: 静态函数,无返回值page
: 要准备的新分配页面指针order
: 分配阶数,用于设置正确的引用计数
if (page->mapping || page_mapped(page) ||
- 页面状态检查第一部分:
page->mapping
: 检查页面是否有地址空间映射- 如果非NULL,表示页面已关联到某个文件的地址空间
page_mapped(page)
: 函数调用,检查页面是否被映射到进程页表- 返回真表示页面正在被某个进程使用
(page->flags & (1 << PG_private |
- 页面标志位检查开始:
- 使用位掩码检查多个页面标志位
1 << PG_private
:PG_private
标志表示页面有私有数据- 新分配的页面不应该有私有数据
1 << PG_locked |
- PG_locked 检查:
PG_locked
表示页面被锁定,通常用于IO操作- 新分配的页面不应该被锁定
1 << PG_lru |
- PG_lru 检查:
PG_lru
表示页面在LRU链表中- 新分配的页面不应该在LRU链表中
1 << PG_active |
- PG_active 检查:
PG_active
表示页面在活跃LRU链表中- 新分配的页面不应该在活跃链表中
1 << PG_dirty |
- PG_dirty 检查:
PG_dirty
表示页面内容已被修改但未写回存储设备- 新分配的页面应该是干净的
1 << PG_reclaim |
- PG_reclaim 检查:
PG_reclaim
表示页面被标记为可回收- 新分配的页面不应该是可回收状态
1 << PG_swapcache |
PG_swapcache
检查:PG_swapcache
表示页面在交换缓存中- 新分配的页面不应该在交换缓存中
1 << PG_writeback )))
- PG_writeback 检查:
PG_writeback
表示页面正在被写回存储设备- 新分配的页面不应该有正在进行的写操作
bad_page(__FUNCTION__, page);
- 坏页处理:
- 如果上述任何条件为真,调用
bad_page
函数 __FUNCTION__
: 预定义宏,展开为当前函数名 “prep_new_page”page
: 有问题的页面指针bad_page
打印错误信息并可能触发内核错误
- 如果上述任何条件为真,调用
page->flags &= ~(1 << PG_uptodate | 1 << PG_error |
- 清理页面标志位第一部分:
page->flags &= ~(...)
: 使用位与和位非操作清除指定的标志位1 << PG_uptodate
:PG_uptodate
表示页面数据是最新的- 新分配的页面内容未定义,所以清除此标志
1 << PG_referenced | 1 << PG_arch_1 |
- 清理更多标志位:
1 << PG_referenced
: 清除被引用标志1 << PG_arch_1
: 清除架构特定的标志1(不同架构有不同用途)
1 << PG_checked | 1 << PG_mappedtodisk);
- 完成标志位清理:
1 << PG_checked
: 清除已检查标志(用于文件系统)1 << PG_mappedtodisk
: 清除映射到磁盘标志
page->private = 0;
- 清零私有数据字段:
page->private
: 页面的私有数据字段- 设置为0,确保没有残留的私有数据指针
set_page_refs(page, order);
- 设置页面引用计数:
set_page_refs(page, order)
: 函数调用,根据order设置正确的引用计数- 对于单页(order=0),通常设置为1
- 对于复合页(order>0),设置适当的引用计数
3. 页面标志位清理详解
3.1. 被清理的标志位
标志位 | 含义 | 清理原因 |
---|---|---|
PG_uptodate | 页面数据是最新的 | 新页面内容未初始化 |
PG_error | 页面有IO错误 | 新页面没有IO历史 |
PG_referenced | 页面被引用过 | 清除访问历史 |
PG_arch_1 | 架构特定标志 | 确保架构无关性 |
PG_checked | 页面已被检查 | 文件系统相关,新页面不需要 |
PG_mappedtodisk | 页面映射到磁盘 | 清除文件系统映射信息 |
初始化复合页面prep_compound_page
static void prep_compound_page(struct page *page, unsigned long order)
{int i;int nr_pages = 1 << order;page[1].mapping = NULL;page[1].index = order;for (i = 0; i < nr_pages; i++) {struct page *p = page + i;SetPageCompound(p);p->private = (unsigned long)page;}
}
1. 函数功能
初始化复合页面(Compound Page),将多个连续的物理页面组合成一个大的逻辑页面。该函数设置复合页面的元数据,建立所有组成页面与首页面的关联关系
2. 逐行代码解析
static void prep_compound_page(struct page *page, unsigned long order)
{
static void
: 静态函数,无返回值page
: 复合页面的首页面指针(第一个页面的struct page)order
: 分配阶数,决定复合页面包含的页面数量(2^order个页面)
int i;
- 循环计数器:用于遍历复合页面中的所有组成页面
int nr_pages = 1 << order;
- 计算页面总数:
1 << order
: 计算复合页面包含的物理页面数量
page[1].mapping = NULL;
- 设置第二个页面的mapping字段:
page[1]
: 通过指针运算访问第二个页面结构page
指向第一个页面page + 1
或page[1]
指向第二个页面
mapping = NULL
: 将第二个页面的地址空间映射指针设为NULL- 特殊用途:在复合页面中,第二个页面通常用于存储复合页面的元数据
page[1].index = order;
- 在第二个页面中存储阶数信息:
page[1].index = order
: 将分配阶数存储在第二个页面的index字段中- 作用:后续可以通过第二个页面快速知道这个复合页面的大小
for (i = 0; i < nr_pages; i++) {
- 遍历所有页面:
i = 0
: 从第一个页面开始i < nr_pages
: 循环条件,处理所有nr_pages个页面i++
: 每次循环处理一个页面
struct page *p = page + i;
- 获取当前页面指针:
page + i
: 通过指针运算获取第i个页面的struct page指针page
是首页面,page + i
是第i个页面(0-based)
SetPageCompound(p);
- 设置复合页面标志:
SetPageCompound(p)
: 宏,设置页面的PG_compound标志位- 作用:标记这个页面是复合页面的一部分
- 重要性:这个标志告诉内核这个页面不是独立的,而是大页面的一部分
p->private = (unsigned long)page;
- 建立与首页面的关联:
p->private = (unsigned long)page
: 将首页面指针存储在每个页面的private字段中- 类型转换:
(unsigned long)page
将指针转换为无符号长整型存储 - 作用:任何组成页面都能通过private字段找到复合页面的首页面
- 关键设计:这是复合页面管理的核心机制