Linux中页面写回初始化page_writeback_init函数实现

页面写回初始化函数page_writeback_init

c 复制代码
void __init page_writeback_init(void)
{
        long buffer_pages = nr_free_buffer_pages();
        long correction;

        total_pages = nr_free_pagecache_pages();

        correction = (100 * 4 * buffer_pages) / total_pages;

        if (correction < 100) {
                dirty_background_ratio *= correction;
                dirty_background_ratio /= 100;
                vm_dirty_ratio *= correction;
                vm_dirty_ratio /= 100;

                if (dirty_background_ratio <= 0)
                        dirty_background_ratio = 1;
                if (vm_dirty_ratio <= 0)
                        vm_dirty_ratio = 1;
        }
        mod_timer(&wb_timer, jiffies + (dirty_writeback_centisecs * HZ) / 100);
        set_ratelimit();
        register_cpu_notifier(&ratelimit_nb);
}

函数整体功能

这个函数用于初始化Linux内核的页面写回子系统,根据系统内存情况调整脏页写回参数,并启动定时写回机制

代码分段详解

第一段:函数定义

c 复制代码
void __init page_writeback_init(void)
  • void: 无返回值
  • __init: 初始化函数宏,表示这个函数在内核初始化后会被释放
  • page_writeback_init: 页面写回初始化函数

第二段:变量声明和初始页面计数

c 复制代码
        long buffer_pages = nr_free_buffer_pages();
        long correction;

        total_pages = nr_free_pagecache_pages();

获取缓冲区页面数量:

c 复制代码
long buffer_pages = nr_free_buffer_pages();
  • nr_free_buffer_pages(): 内核函数,返回可用的缓冲区页面数量
  • 缓冲区页面用于文件系统缓存等操作

声明校正因子变量:

c 复制代码
long correction;
  • 用于存储内存调整的校正因子

获取总页面缓存数量:

c 复制代码
total_pages = nr_free_pagecache_pages();
  • nr_free_pagecache_pages(): 返回页面缓存中的总空闲页面数
  • total_pages: 全局变量,存储系统总页面数

第三段:计算校正因子

c 复制代码
        correction = (100 * 4 * buffer_pages) / total_pages;
  • correction = (100 * 4 * buffer_pages) / total_pages: 计算内存校正因子
  • 公式含义: 校正因子 = (100 × 4 × 缓冲区页面) / 总页面
  • 为什么要×4: 期望比例是1/4
  • 为什么要×100: 为了使用整数运算避免浮点数,保持精度
  • 作用: 根据缓冲区内存占总内存的比例来调整写回参数

第四段:应用校正因子

c 复制代码
        if (correction < 100) {
                dirty_background_ratio *= correction;
                dirty_background_ratio /= 100;
                vm_dirty_ratio *= correction;
                vm_dirty_ratio /= 100;

条件检查:

c 复制代码
if (correction < 100)
  • 只在校正因子小于100时进行调整
  • 如果校正因子≥100,说明缓冲区内存足够,使用默认参数

调整后台脏页比率:

c 复制代码
dirty_background_ratio *= correction;
dirty_background_ratio /= 100;
  • dirty_background_ratio: 全局变量,后台脏页比率阈值
  • 当脏页比例超过此值时,内核开始在后台写回脏页
  • 调整公式:新比率 = 原比率 × 校正因子 / 100

调整虚拟内存脏页比率:

c 复制代码
vm_dirty_ratio *= correction;
vm_dirty_ratio /= 100;
  • vm_dirty_ratio: 全局变量,系统脏页比率阈值
  • 当脏页比例超过此值时,应用程序可能会被阻塞以等待写回完成
  • 调整公式同上

第五段:边界检查

c 复制代码
                if (dirty_background_ratio <= 0)
                        dirty_background_ratio = 1;
                if (vm_dirty_ratio <= 0)
                        vm_dirty_ratio = 1;
        }

检查后台脏页比率:

c 复制代码
if (dirty_background_ratio <= 0)
    dirty_background_ratio = 1;
  • 如果调整后的比率小于等于0,设置为最小值1
  • 防止除零错误和无效参数

检查系统脏页比率:

c 复制代码
if (vm_dirty_ratio <= 0)
    vm_dirty_ratio = 1;
  • 同样的边界检查,确保比率至少为1%

第六段:启动写回定时器

c 复制代码
        mod_timer(&wb_timer, jiffies + (dirty_writeback_centisecs * HZ) / 100);
  • mod_timer(&wb_timer, ...): 修改定时器的到期时间
  • &wb_timer: 写回定时器指针
  • jiffies: 内核时间单位,系统启动后的时钟滴答数
  • dirty_writeback_centisecs: 全局变量,写回间隔(百分之一秒)
  • HZ: 系统时钟频率(每秒滴答数)
  • 计算公式: 到期时间 = 当前时间 + (写回间隔 × HZ) / 100
  • 作用: 启动周期性脏页写回定时器

第七段:设置速率限制

c 复制代码
        set_ratelimit();
  • set_ratelimit(): 设置写回速率限制函数
  • 初始化写回操作的速率控制参数
  • 防止写回操作消耗过多系统资源

第八段:注册CPU通知器

c 复制代码
        register_cpu_notifier(&ratelimit_nb);
}
  • register_cpu_notifier(&ratelimit_nb): 注册CPU热插拔通知器
  • &ratelimit_nb: 速率限制通知块指针
  • 作用: 当CPU被热添加或移除时,调整写回速率限制参数

内存调整策略可视化

是 否 系统内存状态 计算缓冲区比例 缓冲区比例小? 降低脏页阈值 使用默认阈值 更频繁的写回 正常的写回频率 避免内存压力 优化IO性能 调整目标 平衡内存和IO 防止系统卡顿

校正因子公式分解

c 复制代码
correction = (100 * 4 * buffer_pages) / total_pages;

1. 基本比例计算

c 复制代码
(buffer_pages) / total_pages

含义: 缓冲区页面占总页面缓存的比例

2. 乘以4的原因

c 复制代码
4 * buffer_pages

为什么要乘以4?

在Linux内核的早期版本中,这个设计基于以下观察:

内存使用模式:

复制代码
总内存 ≈ 缓冲区内存 + 文件缓存 + 应用程序内存

当缓冲区内存占总内存的 25% (1/4) 时,系统能达到较好的平衡。所以:

  • 基准线: 25% 的缓冲区比例被认为是"理想"状态
  • 校正逻辑: 如果实际比例小于25%,就需要调整脏页阈值

数学推导:

复制代码
期望比例 = 25% = 1/4
校正因子 = (实际比例) / (期望比例) 
         = (buffer_pages/total_pages) / (1/4)
         = 4 * (buffer_pages/total_pages)

3. 乘以100避免浮点数

c 复制代码
100 * 4 * buffer_pages

先放大100倍,然后进行除法运算,提高精度的同时不会出现浮点运算

校正因子语义解释

校正因子的取值范围
缓冲区比例 校正因子 25%缓冲区 校正因子=100 12.5%缓冲区 校正因子=50 6.25%缓冲区 校正因子=25 50%缓冲区 校正因子=200

具体计算示例:

缓冲区比例 计算过程 校正因子
25% (100×4×25)/100 = 100 100
12.5% (100×4×12.5)/100 = 50 50
6.25% (100×4×6.25)/100 = 25 25
50% (100×4×50)/100 = 200 200

条件判断的逻辑

c 复制代码
if (correction < 100)

为什么只在校正因子<100时调整?

设计哲学:

  • 校正因子 < 100 : 缓冲区内存不足,需要降低脏页阈值
  • 校正因子 ≥ 100: 缓冲区内存充足,使用默认参数

pdflush线程池提交一个写回操作任务pdflush_operation

c 复制代码
int pdflush_operation(void (*fn)(unsigned long), unsigned long arg0)
{
        unsigned long flags;
        int ret = 0;

        if (fn == NULL)
                BUG();          /* Hard to diagnose if it's deferred */

        spin_lock_irqsave(&pdflush_lock, flags);
        if (list_empty(&pdflush_list)) {
                spin_unlock_irqrestore(&pdflush_lock, flags);
                ret = -1;
        } else {
                struct pdflush_work *pdf;

                pdf = list_entry(pdflush_list.next, struct pdflush_work, list);
                list_del_init(&pdf->list);
                if (list_empty(&pdflush_list))
                        last_empty_jifs = jiffies;
                pdf->fn = fn;
                pdf->arg0 = arg0;
                wake_up_process(pdf->who);
                spin_unlock_irqrestore(&pdflush_lock, flags);
        }
        return ret;
}

函数整体功能

这个函数用于向pdflush(页面脏数据刷新)线程池提交一个写回操作任务,是Linux内核旧版本中页面写回机制的核心调度函数

代码分段详解

第一段:函数定义和变量声明

c 复制代码
int pdflush_operation(void (*fn)(unsigned long), unsigned long arg0)
{
        unsigned long flags;
        int ret = 0;
  • int: 返回整型状态码,0成功,-1失败
  • void (*fn)(unsigned long): 函数指针参数,指向要执行的回调函数
  • unsigned long arg0: 回调函数的参数
  • flags: 用于保存中断状态的变量
  • ret = 0: 返回值,初始化为0(成功)

第二段:函数指针有效性检查

c 复制代码
        if (fn == NULL)
                BUG();          /* Hard to diagnose if it's deferred */
  • if (fn == NULL): 检查传入的函数指针是否为空
  • BUG(): 如果为空,触发内核致命错误
  • 作用: 确保传入有效的回调函数,避免后续执行空指针

第三段:获取锁并保存中断状态

c 复制代码
        spin_lock_irqsave(&pdflush_lock, flags);
  • spin_lock_irqsave(&pdflush_lock, flags):
    • 获取pdflush锁,保护共享数据结构
    • 同时保存当前中断状态到flags,并禁用中断
    • 防止并发访问pdflush_list和中断处理程序的竞争条件

第四段:检查pdflush线程池是否为空

c 复制代码
        if (list_empty(&pdflush_list)) {
                spin_unlock_irqrestore(&pdflush_lock, flags);
                ret = -1;
        }

检查空列表:

c 复制代码
if (list_empty(&pdflush_list))
  • 检查pdflush线程列表是否为空
  • pdflush_list: 全局变量,存储所有可用的pdflush线程

释放锁并返回错误:

c 复制代码
spin_unlock_irqrestore(&pdflush_lock, flags);
ret = -1;
  • 恢复中断状态并释放锁
  • 设置返回值为-1(失败)
  • 场景: 没有可用的pdflush线程来处理任务

第五段:有可用线程的情况

c 复制代码
        else {
                struct pdflush_work *pdf;
  • else: pdflush线程池非空,可以处理任务
  • struct pdflush_work *pdf: 声明pdflush工作结构指针

第六段:获取第一个可用线程

c 复制代码
                pdf = list_entry(pdflush_list.next, struct pdflush_work, list);
  • list_entry(pdflush_list.next, struct pdflush_work, list):
    • pdflush_list.next: 获取链表第一个节点的指针
    • struct pdflush_work: 目标结构体类型
    • list: 在结构体中的链表成员名称
    • 作用: 通过链表节点指针获取包含它的完整结构体指针

第七段:从链表中移除线程

c 复制代码
                list_del_init(&pdf->list);
  • list_del_init(&pdf->list):
    • 将线程从可用列表中移除
    • 并重新初始化链表节点(设置为空链表状态)
    • 作用: 标记这个线程为"工作中"状态

第八段:更新最后空列表时间

c 复制代码
                if (list_empty(&pdflush_list))
                        last_empty_jifs = jiffies;
  • if (list_empty(&pdflush_list)): 检查移除后列表是否为空
  • last_empty_jifs = jiffies: 如果为空,记录当前时间戳
  • jiffies: 内核时间单位,系统启动后的时钟滴答数
  • 作用: 记录线程池耗尽的时间,用于监控和统计

第九段:设置任务参数

c 复制代码
                pdf->fn = fn;
                pdf->arg0 = arg0;
  • pdf->fn = fn: 将回调函数指针保存到工作结构
  • pdf->arg0 = arg0: 将参数保存到工作结构
  • 作用: 准备任务执行所需的所有信息

第十段:唤醒pdflush线程

c 复制代码
                wake_up_process(pdf->who);
  • wake_up_process(pdf->who):
    • pdf->who: 指向pdflush线程的task_struct指针
    • 唤醒对应的内核线程,让它开始执行任务
    • 作用: 触发实际的写回操作执行

第十一段:释放锁并返回

c 复制代码
                spin_unlock_irqrestore(&pdflush_lock, flags);
        }
        return ret;
}
  • spin_unlock_irqrestore(&pdflush_lock, flags): 恢复中断状态并释放锁
  • return ret: 返回操作结果(0成功,-1失败)

数据结构关系图

pdflush_list全局链表 pdf1: pdflush_work pdf2: pdflush_work pdf3: pdflush_work task_struct who 函数指针 fn 参数 arg0 链表节点 list 操作过程 从链表获取pdf 设置fn和arg0 唤醒对应线程

定义写回定时器wb_timer

c 复制代码
static struct timer_list wb_timer =
                        TIMER_INITIALIZER(wb_timer_fn, 0, 0);
static void wb_timer_fn(unsigned long unused)
{
        if (pdflush_operation(wb_kupdate, 0) < 0)
                mod_timer(&wb_timer, jiffies + HZ); /* delay 1 second */
}

代码整体功能

这个代码定义了一个写回定时器,用于定期触发脏页写回操作,并在操作失败时实现退避重试机制

代码分段详解

第一段:定时器静态初始化

c 复制代码
static struct timer_list wb_timer =
                        TIMER_INITIALIZER(wb_timer_fn, 0, 0);

变量声明:

c 复制代码
static struct timer_list wb_timer
  • static: 静态变量,限制作用域在当前文件
  • struct timer_list: 内核定时器结构体类型
  • wb_timer: 写回定时器变量名

定时器初始化宏:

c 复制代码
TIMER_INITIALIZER(wb_timer_fn, 0, 0)
  • TIMER_INITIALIZER: 内核定时器静态初始化宏
  • wb_timer_fn: 定时器到期时调用的回调函数
  • 第一个 0: 定时器到期时间,0表示未激活
  • 第二个 0: 传递给回调函数的参数,这里为0

第二段:定时器回调函数定义

c 复制代码
static void wb_timer_fn(unsigned long unused)
{
  • static void: 静态函数,无返回值
  • wb_timer_fn: 函数名,写回定时器回调函数
  • unsigned long unused: 参数,名为unused表示未使用
    • 虽然传递了参数,但函数体内没有使用它
    • 保持函数签名与定时器回调要求一致

第三段:尝试执行写回操作

c 复制代码
        if (pdflush_operation(wb_kupdate, 0) < 0)

函数调用:

c 复制代码
pdflush_operation(wb_kupdate, 0)
  • pdflush_operation: 前面分析的pdflush任务提交函数
  • wb_kupdate: 实际的写回操作函数指针
  • 0: 传递给wb_kupdate函数的参数

返回值检查:

c 复制代码
if (... < 0)
  • 检查pdflush_operation的返回值
  • < 0 表示操作失败(通常是没有可用的pdflush线程)

wb_kupdate函数的作用:

  • 这是实际的脏页写回函数
  • 遍历内存中的脏页并写回到存储设备
  • 是页面写回系统的核心逻辑

第四段:失败时的退避重试

c 复制代码
                mod_timer(&wb_timer, jiffies + HZ); /* delay 1 second */

mod_timer函数:

c 复制代码
mod_timer(&wb_timer, jiffies + HZ)
  • mod_timer: 内核函数,修改定时器的到期时间
  • &wb_timer: 要修改的定时器指针
  • jiffies + HZ: 新的到期时间

时间计算:

  • jiffies: 系统当前的时钟滴答数
  • HZ: 系统时钟频率,表示每秒的时钟滴答数
  • jiffies + HZ: 当前时间 + 1秒

作用: 如果写回操作失败,在1秒后重试

实际执行场景

场景1:正常执行

复制代码
时间点T: 定时器到期
→ 调用wb_timer_fn
→ pdflush_operation成功提交任务
→ 函数返回,定时器停止
→ 需要其他代码在适当时候重新启动定时器

场景2:资源紧张

复制代码
时间点T: 定时器到期
→ 调用wb_timer_fn  
→ pdflush_operation失败(无可用线程)
→ 设置1秒后重试
→ 时间点T+1秒: 定时器再次到期
→ 重试写回操作...

与其他组件的交互

pdflush_operation的关系

c 复制代码
// 这是一个生产者-消费者模式
wb_timer_fn (生产者) → pdflush_operation → pdflush线程 (消费者)

与系统初始化的关系

page_writeback_init 中:

c 复制代码
mod_timer(&wb_timer, jiffies + (dirty_writeback_centisecs * HZ) / 100);

这里设置了定时器的第一次触发时间

定期将过期的脏页写回到存储设备wb_kupdate

c 复制代码
static void wb_kupdate(unsigned long arg)
{
        unsigned long oldest_jif;
        unsigned long start_jif;
        unsigned long next_jif;
        long nr_to_write;
        struct writeback_state wbs;
        struct writeback_control wbc = {
                .bdi            = NULL,
                .sync_mode      = WB_SYNC_NONE,
                .older_than_this = &oldest_jif,
                .nr_to_write    = 0,
                .nonblocking    = 1,
                .for_kupdate    = 1,
        };

        sync_supers();

        get_writeback_state(&wbs);
        oldest_jif = jiffies - (dirty_expire_centisecs * HZ) / 100;
        start_jif = jiffies;
        next_jif = start_jif + (dirty_writeback_centisecs * HZ) / 100;
        nr_to_write = wbs.nr_dirty + wbs.nr_unstable +
                        (inodes_stat.nr_inodes - inodes_stat.nr_unused);
        while (nr_to_write > 0) {
                wbc.encountered_congestion = 0;
                wbc.nr_to_write = MAX_WRITEBACK_PAGES;
                writeback_inodes(&wbc);
                if (wbc.nr_to_write > 0) {
                        if (wbc.encountered_congestion)
                                blk_congestion_wait(WRITE, HZ/10);
                        else
                                break;  /* All the old data is written */
                }
                nr_to_write -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
        }
        if (time_before(next_jif, jiffies + HZ))
                next_jif = jiffies + HZ;
        if (dirty_writeback_centisecs)
                mod_timer(&wb_timer, next_jif);
}

函数整体功能

这个函数是Linux内核脏页写回的核心实现,负责定期将过期的脏页写回到存储设备,维护系统内存的清洁度

代码分段详解

第一段:函数定义和变量声明

c 复制代码
static void wb_kupdate(unsigned long arg)
{
        unsigned long oldest_jif;
        unsigned long start_jif;
        unsigned long next_jif;
        long nr_to_write;
        struct writeback_state wbs;
  • static void: 静态函数,无返回值
  • wb_kupdate: 函数名,写回更新操作
  • unsigned long arg: 参数(未使用)
  • oldest_jif: 最老脏页的时间戳阈值
  • start_jif: 本次写回开始时间
  • next_jif: 下一次写回计划时间
  • nr_to_write: 需要写回的页面数量估计
  • wbs: 写回状态结构,用于获取系统脏页统计

第二段:写回控制结构初始化

c 复制代码
        struct writeback_control wbc = {
                .bdi            = NULL,
                .sync_mode      = WB_SYNC_NONE,
                .older_than_this = &oldest_jif,
                .nr_to_write    = 0,
                .nonblocking    = 1,
                .for_kupdate    = 1,
        };

写回控制参数:

  • .bdi = NULL: 后备设备信息,NULL表示所有设备
  • .sync_mode = WB_SYNC_NONE: 异步模式,不等待写回完成
  • .older_than_this = &oldest_jif: 指向时间阈值,只写回比这个时间老的脏页
  • .nr_to_write = 0: 初始写回页面数为0,后面会设置
  • .nonblocking = 1: 非阻塞模式
  • .for_kupdate = 1: 标记为定期更新操作

第三段:同步超级块

c 复制代码
        sync_supers();
  • sync_supers(): 内核函数,将文件系统超级块同步到磁盘
  • 超级块包含文件系统元数据,需要定期持久化
  • 确保文件系统结构的一致性

第四段:获取系统脏页状态

c 复制代码
        get_writeback_state(&wbs);
  • get_writeback_state(&wbs): 获取当前系统的写回状态
  • 填充wbs结构,包含:
    • nr_dirty: 脏页数量
    • nr_unstable: 不稳定页数量
    • 其他脏页统计信息

第五段:计算时间阈值

c 复制代码
        oldest_jif = jiffies - (dirty_expire_centisecs * HZ) / 100;
  • jiffies: 当前系统时间戳
  • dirty_expire_centisecs: 全局变量,脏页过期时间(百分之一秒)
  • HZ: 系统时钟频率
  • 计算公式: 最老时间 = 当前时间 - 过期时间
  • 作用: 只写回存在时间超过dirty_expire_centisecs的脏页

第六段:记录开始时间和计算下次执行时间

c 复制代码
        start_jif = jiffies;
        next_jif = start_jif + (dirty_writeback_centisecs * HZ) / 100;
  • start_jif = jiffies: 记录本次写回开始时间
  • next_jif = ...: 计算下一次写回执行时间
  • dirty_writeback_centisecs: 全局变量,写回间隔时间
  • 作用: 建立定期写回的时间计划

第七段:估算需要写回的页面数量

c 复制代码
        nr_to_write = wbs.nr_dirty + wbs.nr_unstable +
                        (inodes_stat.nr_inodes - inodes_stat.nr_unused);
  • wbs.nr_dirty: 当前脏页数量
  • wbs.nr_unstable: 不稳定页数量(正在写回中的页)
  • inodes_stat.nr_inodes - inodes_stat.nr_unused: 活跃inode数量估计
  • 作用: 粗略估算可能需要写回的最大页面数量
  • 这是一个保守估计,确保不会遗漏脏页

第八段:写回循环

c 复制代码
        while (nr_to_write > 0) {
  • while (nr_to_write > 0): 循环直到所有需要写回的页面都处理完
  • 或者遇到无法继续写回的情况

第九段:设置本次写回参数

c 复制代码
                wbc.encountered_congestion = 0;
                wbc.nr_to_write = MAX_WRITEBACK_PAGES;
  • wbc.encountered_congestion = 0: 重置拥塞标志
  • wbc.nr_to_write = MAX_WRITEBACK_PAGES: 设置本次最大写回页面数
  • MAX_WRITEBACK_PAGES: 常量,通常为1024,表示每次最多写回1024页

第十段:执行实际写回操作

c 复制代码
                writeback_inodes(&wbc);
  • writeback_inodes(&wbc): 核心写回函数
  • 遍历所有inode,将其中的脏页写回到存储设备
  • 根据wbc参数控制写回行为

第十一段:检查写回完成情况

c 复制代码
                if (wbc.nr_to_write > 0) {
  • if (wbc.nr_to_write > 0): 检查是否还有剩余的写回额度
  • 如果nr_to_write > 0,说明没有用完本次配额,可能因为:
    1. 所有旧数据都已写回
    2. 遇到IO拥塞无法继续

第十二段:处理IO拥塞

c 复制代码
                        if (wbc.encountered_congestion)
                                blk_congestion_wait(WRITE, HZ/10);
                        else
                                break;  /* All the old data is written */
                }

拥塞情况:

c 复制代码
if (wbc.encountered_congestion)
    blk_congestion_wait(WRITE, HZ/10);
  • wbc.encountered_congestion: 写回过程中检测到IO拥塞
  • blk_congestion_wait(WRITE, HZ/10): 等待写操作拥塞缓解
  • HZ/10: 等待0.1秒

无拥塞情况:

c 复制代码
else
    break;  /* All the old data is written */
  • 所有旧数据都已写回
  • 跳出循环,结束本次写回操作

第十三段:更新剩余写回数量

c 复制代码
                nr_to_write -= MAX_WRITEBACK_PAGES - wbc.nr_to_write;
        }
  • MAX_WRITEBACK_PAGES - wbc.nr_to_write: 本次实际写回的页面数
  • nr_to_write -= ...: 从总估计值中减去已写回的数量
  • 循环继续处理剩余的脏页

第十四段:确保最小间隔

c 复制代码
        if (time_before(next_jif, jiffies + HZ))
                next_jif = jiffies + HZ;
  • time_before(next_jif, jiffies + HZ): 检查计划的下次时间是否在1秒内
  • next_jif = jiffies + HZ: 如果在1秒内,设置为至少1秒后
  • 作用: 防止写回操作过于频繁,保证最小1秒间隔

第十五段:重新启动定时器

c 复制代码
        if (dirty_writeback_centisecs)
                mod_timer(&wb_timer, next_jif);
}
  • if (dirty_writeback_centisecs): 检查写回间隔是否非零
  • mod_timer(&wb_timer, next_jif): 重新设置写回定时器
  • 作用: 建立下一次定期写回

控制脏页写回的速度set_ratelimit

c 复制代码
static struct notifier_block ratelimit_nb = {
        .notifier_call  = ratelimit_handler,
        .next           = NULL,
};
static int
ratelimit_handler(struct notifier_block *self, unsigned long u, void *v)
{
        set_ratelimit();
        return 0;
}
static void set_ratelimit(void)
{
        ratelimit_pages = total_pages / (num_online_cpus() * 32);
        if (ratelimit_pages < 16)
                ratelimit_pages = 16;
        if (ratelimit_pages * PAGE_CACHE_SIZE > 4096 * 1024)
                ratelimit_pages = (4096 * 1024) / PAGE_CACHE_SIZE;
}

代码整体功能

这个代码实现了基于CPU数量的动态速率限制机制,用于控制脏页写回的速度,防止在多CPU系统中产生IO风暴

代码分段详解

第一段:通知器块定义

c 复制代码
static struct notifier_block ratelimit_nb = {
        .notifier_call  = ratelimit_handler,
        .next           = NULL,
};

结构体定义:

  • static struct notifier_block ratelimit_nb: 静态通知器块变量
  • struct notifier_block: 内核通知链机制的结构体

成员初始化:

  • .notifier_call = ratelimit_handler: 通知回调函数指针
  • .next = NULL: 下一个通知器指针,NULL表示链表结束

作用: 注册到CPU热插拔通知链,当CPU状态变化时自动调用ratelimit_handler

第二段:通知器回调函数

c 复制代码
ratelimit_handler(struct notifier_block *self, unsigned long u, void *v)
{
        set_ratelimit();
        return 0;
}

函数签名:

  • ratelimit_handler: 回调函数名
  • struct notifier_block *self: 指向自身的通知器块指针
  • unsigned long u: 事件类型(CPU上线、下线等)
  • void *v: 事件相关数据

函数体:

  • set_ratelimit(): 调用设置速率限制函数
  • return 0: 返回0表示处理成功

特点:

  • 忽略具体的事件类型和参数
  • 任何CPU状态变化都触发速率限制重计算
  • 保持简单的响应逻辑

第三段:速率限制设置函数

c 复制代码
static void set_ratelimit(void)
{
        ratelimit_pages = total_pages / (num_online_cpus() * 32);

计算公式:

  • ratelimit_pages = total_pages / (num_online_cpus() * 32)
  • total_pages: 全局变量,系统总页面数
  • num_online_cpus(): 返回当前在线的CPU数量
  • 32: 经验系数,每个CPU分配32分之一的总页面作为速率限制

设计原理:

  • CPU越多,允许的并发写回页面越少
  • 防止多CPU同时产生大量写回IO
  • 平衡并行性和IO带宽

第四段:下限保护

c 复制代码
        if (ratelimit_pages < 16)
                ratelimit_pages = 16;
  • if (ratelimit_pages < 16): 检查计算值是否小于16页
  • ratelimit_pages = 16: 如果太小,设置为最小值16页
  • 作用: 确保即使在大规模多CPU系统中也有基本的写回能力

第五段:上限保护

c 复制代码
        if (ratelimit_pages * PAGE_CACHE_SIZE > 4096 * 1024)
                ratelimit_pages = (4096 * 1024) / PAGE_CACHE_SIZE;
}

条件检查:

  • ratelimit_pages * PAGE_CACHE_SIZE > 4096 * 1024
  • PAGE_CACHE_SIZE: 页面缓存大小,通常4096字节(4KB)
  • 4096 * 1024: 4MB(4096KB)

上限设置:

  • ratelimit_pages = (4096 * 1024) / PAGE_CACHE_SIZE
  • 如果超过4MB,限制为4MB对应的页面数
  • 对于4KB页面:4096 * 1024 / 4096 = 1024

作用: 防止在少CPU大内存系统中单次写回过多数据

设计原理分析

1. 基于CPU数量的自适应

c 复制代码
// 核心思想:CPU越多,限制越严格
ratelimit_pages = total_pages / (num_online_cpus() * 32)

为什么是32?

  • 经验值,平衡并行性和IO效率
  • 每个CPU允许写回总内存的1/32份额
  • 防止多CPU同时发起写回产生IO竞争

2. 边界保护的意义

下限保护(16页):

  • 确保基本功能:即使在大规模系统中也能进行写回
  • 维持吞吐量:64KB的最小写回单元

上限保护(4MB):

  • 控制单次写回规模:避免大块IO阻塞系统
  • 公平性:防止少CPU系统占用过多IO带宽
  • 响应性:保证系统及时响应其他IO请求

3. 热插拔感知

通过通知器机制:

  • CPU上线:自动收紧限制(分母变大)
  • CPU下线:自动放宽限制(分母变小)
  • 实时适应系统拓扑变化
相关推荐
jimy13 小时前
安卓里运行Linux
linux·运维·服务器
爱凤的小光4 小时前
Linux清理磁盘技巧---个人笔记
linux·运维
耗同学一米八5 小时前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
知星小度S6 小时前
系统核心解析:深入文件系统底层机制——Ext系列探秘:从磁盘结构到挂载链接的全链路解析
linux
2401_890443026 小时前
Linux 基础IO
linux·c语言
智慧地球(AI·Earth)7 小时前
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南
linux·ssh·ai编程
老王熬夜敲代码8 小时前
解决IP不够用的问题
linux·网络·笔记
zly35008 小时前
linux查看正在运行的nginx的当前工作目录(webroot)
linux·运维·nginx
QT 小鲜肉8 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
问道飞鱼9 小时前
【Linux知识】Linux 虚拟机磁盘扩缩容操作指南(按文件系统分类)
linux·运维·服务器·磁盘扩缩容