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下线:自动放宽限制(分母变小)
  • 实时适应系统拓扑变化
相关推荐
FJW0208145 小时前
Linux编辑神器——vim工具的使用
linux·运维·vim
神也佑我橙橙6 小时前
conda 创建虚拟环境的一些坑
linux·运维·conda
IT技术分享社区6 小时前
IT运维干货:lnav开源日志分析工具详解与CentOS实战部署
linux·运维·服务器·开源·centos
IT曙光6 小时前
CentOS x86_64架构下载aarch64(arm64)包
linux·运维·centos
bkspiderx6 小时前
Linux网络与路由配置完全指南
linux·运维·网络·c++
退役小学生呀7 小时前
二十三、K8s企业级架构设计及落地
linux·云原生·容器·kubernetes·k8s
凤山老林7 小时前
SpringBoot 如何实现零拷贝:深度解析零拷贝技术
java·linux·开发语言·arm开发·spring boot·后端
云计算练习生7 小时前
linux shell编程实战 04 条件判断与流程控制
linux·运维·流程控制·shell编程·条件判断
至善迎风7 小时前
如何让 Ubuntu 服务器在你关机后继续执行命令
linux·服务器·ubuntu·进程