编程 Linux 7.1/7.2 内核深度实战:当 NTFS 获得"重生"——从 iomap 延迟分配到 folio 内存管理、从 FRED 事件分发到三行代码撬动 5% IOPS 的存储性能革命(2026)

2026-06-22 06:32:06 +0800 CST views 9

Linux 7.1/7.2 内核深度实战:当 NTFS 获得"重生"——从 iomap 延迟分配到 folio 内存管理、从 FRED 事件分发到三行代码撬动 5% IOPS 的存储性能革命(2026)

引言:Linus 说了个词——Resurrection

2026 年 4 月 17 日,一个 Git commit 出现在 kernel.org 上,提交信息平淡无奇。但 Linus Torvalds 在合并时用了一个意味深长的词:"ntfs resurrection"——NTFS 重生。

这不仅仅是一次驱动重写。这是 Linux 对 Windows 主流文件系统长达 20 年的"二等公民"待遇的终结,也是内核文件系统子系统现代化进程中的一个里程碑。新驱动由开发者 Namjae Jeon 历时四年重构,采用 iomap 框架和 folio 内存模型,实现了完整读写支持,在多线程写入场景下性能提升 35%-110%,挂载 4TB 大容量硬盘的速度更是提升至原来的 4 倍。

紧接着,Linux 7.2 开发版中,字节跳动工程师 Fengnan Chang 仅调整了两行代码的位置,就让 EXT4 和 XFS 在高 IOPS 场景下性能提升约 5%。

这两件事放在一起看,勾勒出了 2026 年 Linux 内核存储栈的全景图:从文件系统驱动的彻底重构,到 I/O 路径上两行代码的精密手术,Linux 正在经历一次从上到下的存储性能革命

本文将从 NTFS 新驱动的源码架构、iomap 框架的设计哲学、folio 内存模型、FRED 事件分发机制、三行代码的性能魔法五个维度,对 Linux 7.1/7.2 的存储子系统进行全面拆解。


一、NTFS 的二十年困局:为什么"重生"如此重要

1.1 三代 NTFS 驱动的演进

NTFS 是 Windows 的核心文件系统,自 1993 年随 Windows NT 3.1 发布以来,一直是 PC 世界的存储基石。但在 Linux 世界里,NTFS 的支持一直是痛点:

时间线:Linux NTFS 支持的三代演进

第一代(2001-2022):只读驱动
├── 功能:仅支持读取 NTFS 分区
├── 限制:无写入支持,NTFS 特性覆盖不完整
├── 状态:2022 年后基本停维
└── 代码:约 8 万行,架构老旧

第二代(2020-2025):NTFS3(Paragon)
├── 功能:读写支持,但维护停滞
├── 问题:补丁审核缓慢,社区参与度低
├── 状态:2023 年后维护基本停滞
└── 争议:Paragon 的社区协作模式引发不满

第三代(2022-2026):新 NTFS 驱动(Namjae Jeon)
├── 功能:完整读写 + iomap + folio + 延迟分配
├── 性能:多线程写入提升 35%-110%
├── 状态:Linux 7.1 合入主线
└── 代码:架构清晰,社区积极维护

第一代只读驱动的问题是显而易见的——你只能读,不能写。对于双系统用户、数据恢复工程师、跨平台文件交换场景,这是致命的。

第二代 NTFS3 驱动由 Paragon Software 在 2020 年提交,虽然支持读写,但维护模式存在根本性问题:Paragon 是一家商业公司,其贡献驱动的主要目的是推广付费版本。当社区开发者提交 Bug 报告和补丁时,响应速度缓慢,代码审核流程不透明。2023 年后,NTFS3 的维护基本停滞,多个已知的损坏风险 Bug 无人修复。

1.2 新驱动的核心突破

Namjae Jeon 的新驱动从第一代只读驱动出发,进行了四年的全面重构。核心突破包括:

(1)完整写入支持

旧驱动只实现了读取路径,写入路径完全缺失。新驱动从零实现了 NTFS 的写入语义:

// 新 NTFS 驱动的写入路径核心 — fs/ntfs3/inode.c(简化)
// 基于 iomap 框架的写入实现

static int ntfs_write_begin(struct file *file, struct address_space *mapping,
                            loff_t pos, unsigned len,
                            struct page **pagep, void **fsdata)
{
    // 采用 iomap 框架替代旧的 buffer_head 方案
    // iomap 提供了统一的块映射接口,支持延迟分配
    return iomap_write_begin(mapping, pos, len, pagep,
                             &ntfs_iomap_ops);
}

static int ntfs_write_end(struct file *file, struct address_space *mapping,
                          loff_t pos, unsigned len, unsigned copied,
                          struct page *page, void *fsdata)
{
    // 写入完成后的元数据更新
    // 包括 MFT 记录更新、目录项更新、时间戳更新
    return iomap_write_end(mapping, pos, len, copied, page,
                           &ntfs_iomap_ops);
}

const struct address_space_operations ntfs_aops = {
    .write_begin  = ntfs_write_begin,
    .write_end    = ntfs_write_end,
    .dirty_folio  = filemap_dirty_folio,  // folio 版本的脏页标记
    .migrate_folio = buffer_migrate_folio,
    .invalidate_folio = iomap_invalidate_folio,
    .release_folio = iomap_release_folio,
    .direct_IO    = ntfs_direct_IO,
};

(2)延迟分配(Delayed Allocation)

这是新驱动性能大幅提升的关键。传统 NTFS 写入采用"立即分配"策略——数据写入时立即在磁盘上分配空间并更新元数据。延迟分配则将空间分配推迟到数据实际刷回磁盘时:

// 延迟分配的核心逻辑

static int ntfs_iomap_begin(struct inode *inode, loff_t offset,
                            loff_t length, unsigned flags,
                            struct iomap *iomap, struct iomap *srcmap)
{
    struct ntfs_inode *ni = NTFS_I(inode);
    
    if (flags & IOMAP_WRITE && (iomap_flags & IOMAP_F_DIRTY)) {
        // 延迟分配:不立即分配物理块
        // 而是标记为 DELAY,等待 writeback 时再分配
        iomap->type = IOMAP_DELALLOC;
        iomap->flags |= IOMAP_F_DIRTY;
        iomap->addr = IOMAP_NULL_ADDR;  // 尚无物理地址
        return 0;
    }
    
    // 已分配的块:直接映射
    iomap->type = IOMAP_MAPPED;
    iomap->addr = ni->run.rb_mappings[offset >> inode->i_blkbits];
    return 0;
}

// writeback 时分配物理块
static int ntfs_iomap_get_delayed_blks(struct inode *inode,
                                        struct iomap *iomap)
{
    // 在 writeback 阶段执行实际的空间分配
    // 优势:可以批量分配连续块,减少碎片
    struct ntfs_inode *ni = NTFS_I(inode);
    CLST vcn = iomap->offset >> inode->i_blkbits;
    CLST len = iomap->length >> inode->i_blkbits;
    
    // 在 MFT 中分配簇
    int err = ntfs_alloc_clusters(ni->mi.sbi, &vcn, &len);
    if (err)
        return err;
    
    // 更新 NTFS 的 $DATA 运行列表
    err = ntfs_update_run(ni, vcn, len, ALLOCATED);
    return err;
}

延迟分配的好处有三:

  1. 减少元数据更新:不需要为每次写入都更新 MFT(Master File Table)记录
  2. 批量分配连续块:writeback 时可以根据全局视图选择最优物理位置
  3. 合并小写入:多个小写入可以在页缓存中合并,减少实际 I/O 次数

(3)iomap 与 folio 的深度整合

iomap 是 Linux 内核在 4.8 引入的文件系统 I/O 映射框架,folio 是 5.16 引入的多页内存描述符。新 NTFS 驱动是第一个同时深度整合这两个特性的 NTFS 实现:

// folio 版本的页操作 — 替代旧的 page-based API

static bool ntfs_release_folio(struct folio *folio, gfp_t gfp_flags)
{
    // folio 替代 page:一个 folio 可以包含多个连续 page
    // 减少 page->mapping 等指针的开销
    if (folio_test_private(folio)) {
        if (!iolock_trylock(folio))
            return false;
        iomap_release_folio(folio, gfp_flags);
        iolock_unlock(folio);
    }
    return true;
}

static void ntfs_invalidate_folio(struct folio *folio, size_t offset,
                                  size_t length)
{
    // folio 级别的缓存失效处理
    // 支持大 folio(order > 0)的部分失效
    if (offset || length < folio_size(folio)) {
        iomap_invalidate_folio(folio, offset, length);
    } else {
        // 整个 folio 失效
        filemap_release_folio(folio, GFP_NOFS);
    }
}

1.3 性能测试:35%-110% 提升背后的故事

Phoronix 的测试数据令人印象深刻,但需要深入理解这些数字的含义:

多线程写入 35%-110% 提升的原因分析

// 旧 NTFS3 驱动的写入瓶颈

// 1. 每次写入都获取全局锁
static ssize_t ntfs3_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
    struct inode *inode = file_inode(iocb->ki_filp);
    
    // 全局 inode 锁 — 串行化所有写入操作
    inode_lock(inode);
    
    // 立即分配:每次写入都更新 MFT
    // 多线程写入时,MFT 成为竞争热点
    ret = ntfs3_write_begin(iocb->ki_filp, inode->i_mapping, ...);
    
    inode_unlock(inode);
    return ret;
}

// 新 NTFS 驱动的并发优化

// 1. 延迟分配消除了写入时的 MFT 竞争
// 数据写入时只修改页缓存,不获取 MFT 锁
// MFT 更新延迟到 writeback 阶段,此时可以批量合并

// 2. iomap 支持 DIO(Direct I/O)的无锁路径
static ssize_t ntfs_dio_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
    // iomap 的 DIO 路径使用 rwsem 读写锁
    // 多个读者可以并行,写者与读者互斥
    return iomap_dio_rw(iocb, to, &ntfs_iomap_ops,
                        &ntfs_dio_read_ops, 0, 0);
}

挂载 4TB 硬盘速度提升 4 倍的原因

旧驱动在挂载时需要逐个扫描 MFT 条目以构建内存索引。对于 4TB 硬盘,这可能需要数十秒。新驱动采用了两种优化:

  1. 延迟加载 MFT 索引:只在首次访问时加载对应目录的索引
  2. 并行初始化:利用内核的多线程基础设施并行初始化卷信息
// 新驱动的挂载优化

static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
    struct ntfs_sb_info *sbi = ntfs_sb(sb);
    
    // Phase 1: 只读取最基本的卷信息
    err = ntfs_init_volume(sbi);
    if (err)
        goto out;
    
    // Phase 2: 延迟加载 MFT 和目录索引
    // 旧驱动在这里会扫描整个 MFT 表
    // 新驱动只初始化根目录索引
    err = ntfs_load_root(sbi);
    if (err)
        goto out;
    
    // Phase 3: 启动后台线程异步加载元数据
    // 用户可以立即开始使用文件系统
    queue_work(system_long_wq, &sbi->meta_load_work);
    
    return 0;
}

// 后台异步加载
static void ntfs_meta_load_work(struct work_struct *work)
{
    struct ntfs_sb_info *sbi = container_of(work, struct ntfs_sb_info,
                                            meta_load_work);
    // 异步加载扩展属性、安全描述符等非关键元数据
    ntfs_load_metadata_later(sbi);
}

二、iomap 框架:Linux 文件系统的现代化基石

2.1 从 buffer_head 到 iomap 的范式转移

Linux 内核的文件系统 I/O 路径经历了三代演进:

第一代(2.4-4.7):buffer_head
├── 每个 page 关联一组 buffer_head
├── 每个 buffer_head 描述一个磁盘块
├── 问题:大量小对象,内存开销大,锁粒度粗
└── 适用:小容量磁盘,块大小 512B/4KB

第二代(4.8-5.15):iomap
├── 统一的块映射接口
├── 支持大块映射(一次映射多个块)
├── 支持 DIO、DAX、writeback 统一路径
└── 适用:大容量磁盘,多 TB 文件

第三代(5.16+):iomap + folio
├── folio 替代 page 作为内存管理单元
├── 一个 folio 可以是多个连续 page
├── iomap 原生支持 folio 操作
└── 适用:大内存系统,减少 page 指针开销

iomap 的核心思想是声明式的块映射:文件系统不需要自己管理 buffer_head,只需要告诉 iomap "这个文件的 [offset, offset+len) 区域在磁盘上的位置",剩余的读、写、DIO、writeback 全部由 iomap 框架处理。

// iomap 的核心数据结构

struct iomap {
    u64                 offset;     // 文件内偏移
    u64                 length;     // 映射长度
    u64                 addr;       // 磁盘物理地址(IOMAP_NULL_ADDR 表示未分配)
    enum iomap_type     type;       // IOMAP_MAPPED / IOMAP_DELALLOC / IOMAP_UNWRITTEN / IOMAP_HOLE
    u16                 flags;      // IOMAP_F_* 标志
    struct block_device *bdev;      // 块设备
    struct dax_device   *dax_dev;   // DAX 设备(NVDIMM)
    void                *private;   // 文件系统私有数据
    const struct iomap_ops *ops;    // 操作回调
};

// 文件系统只需实现 iomap_ops

struct iomap_ops {
    int (*iomap_begin)(struct inode *inode, loff_t offset,
                       loff_t length, unsigned flags,
                       struct iomap *iomap, struct iomap *srcmap);
    int (*iomap_end)(struct inode *inode, loff_t offset,
                     loff_t length, ssize_t written,
                     unsigned flags, struct iomap *iomap);
};

2.2 iomap 的四种映射类型

// iomap_type 枚举 — 理解这四种类型是理解 iomap 的关键

enum iomap_type {
    IOMAP_HOLE,       // 文件空洞:无数据,读取返回零
    IOMAP_DELALLOC,   // 延迟分配:数据在页缓存中,尚未分配磁盘块
    IOMAP_MAPPED,     // 已映射:数据在磁盘上,有物理地址
    IOMAP_UNWRITTEN,  // 已分配但未写入:磁盘空间已预留,数据尚未写入
};

/*
 * 四种类型的状态转换:
 *
 * 写入开始 → IOMAP_DELALLOC(延迟分配)
 * writeback → IOMAP_UNWRITTEN(分配了物理块但数据未写入)
 * 写入完成 → IOMAP_MAPPED(数据已安全落盘)
 * 截断文件 → IOMAP_HOLE(释放空间)
 *
 * 对于 NTFS 新驱动:
 * - 读取路径:HOLE → 返回零页;MAPPED → 读取磁盘
 * - 写入路径:先 DELALLOC,后 writeback 时 UNWRITTEN → MAPPED
 * - DIO 路径:直接 MAPPED,跳过延迟分配
 */

2.3 iomap 的统一 I/O 路径

iomap 最大的架构优势是统一了所有 I/O 路径:缓冲写入、DIO、DAX、writeback 全部共享同一套块映射逻辑:

// iomap 统一 I/O 路径示意

// 1. 缓冲写入(Buffered Write)
static ssize_t iomap_write_iter(struct iomap_iter *iter, 
                                 struct iov_iter *i)
{
    // iomap_begin → 获取块映射
    // 如果是 DELALLOC → 在页缓存中分配 folio
    // 如果是 MAPPED → 读取磁盘块到页缓存,然后写入
    // 如果是 HOLE → 分配新 folio,写入后标记为 DELALLOC
}

// 2. 直接 I/O(Direct I/O)
static ssize_t iomap_dio_rw(struct kiocb *iocb, struct iov_iter *iter,
                             const struct iomap_ops *ops,
                             const struct iomap_dio_ops *dops,
                             unsigned int dio_flags, size_t done_before)
{
    // iomap_begin → 获取块映射
    // 必须是 MAPPED 类型(DIO 不支持延迟分配)
    // 直接在用户缓冲区和磁盘之间传输数据
    // 不经过页缓存
}

// 3. Writeback(脏页回写)
static int iomap_writepages(struct address_space *mapping,
                            struct writeback_control *wbc,
                            const struct iomap_writeback_ops *ops)
{
    // 遍历脏 folio
    // 对每个 DELALLOC folio → 先分配物理块(变为 UNWRITTEN)
    // 提交 bio 写入磁盘
    // 写入完成 → 标记为 MAPPED
}

// 4. DAX(直接访问 NVDIMM)
static loff_t iomap_dax_rw(struct kiocb *iocb, struct iov_iter *iter,
                            const struct iomap_ops *ops)
{
    // iomap_begin → 获取块映射
    // 必须是 MAPPED 类型
    // 直接映射 NVDIMM 的物理地址到用户空间
    // 无 I/O 操作,纯内存拷贝
}

2.4 新 NTFS 驱动的 iomap 实现

// fs/ntfs3/iomap.c — 新 NTFS 驱动的 iomap 操作(简化)

static int ntfs_iomap_begin(struct inode *inode, loff_t offset,
                            loff_t length, unsigned flags,
                            struct iomap *iomap, struct iomap *srcmap)
{
    struct ntfs_inode *ni = NTFS_I(inode);
    struct ntfs_sb_info *sbi = ni->mi.sbi;
    CLST vcn = offset >> sbi->cluster_bits;
    CLST len = (length + sbi->cluster_mask) >> sbi->cluster_bits;
    struct runs_tree *run = &ni->file.run;
    
    // 查找簇映射
    CLST lcn;
    bool found = run_get_entry(run, vcn, &lcn, &len);
    
    if (!found || lcn == SPARSE_LCN) {
        // 空洞或未分配
        if (flags & IOMAP_WRITE) {
            // 写入时创建延迟分配
            iomap->type = IOMAP_DELALLOC;
            iomap->flags |= IOMAP_F_DIRTY;
            iomap->addr = IOMAP_NULL_ADDR;
        } else {
            // 读取时返回空洞
            iomap->type = IOMAP_HOLE;
            iomap->addr = IOMAP_NULL_ADDR;
        }
    } else {
        // 已分配的簇
        iomap->type = IOMAP_MAPPED;
        iomap->addr = (u64)lcn << sbi->cluster_bits;
        iomap->flags |= IOMAP_F_MERGED;  // 连续簇,可合并
    }
    
    iomap->offset = offset;
    iomap->length = (u64)len << sbi->cluster_bits;
    iomap->bdev = sbi->sb->s_bdev;
    
    return 0;
}

static int ntfs_iomap_end(struct inode *inode, loff_t offset,
                          loff_t length, ssize_t written,
                          unsigned flags, struct iomap *iomap)
{
    // 写入结束后的处理
    if (written <= 0)
        return written;
    
    // 更新 inode 的时间戳
    if (flags & IOMAP_WRITE) {
        inode->i_mtime = inode->i_ctime = current_time(inode);
        mark_inode_dirty(inode);
    }
    
    return 0;
}

const struct iomap_ops ntfs_iomap_ops = {
    .iomap_begin = ntfs_iomap_begin,
    .iomap_end   = ntfs_iomap_end,
};

对比旧 NTFS3 的 buffer_head 实现,iomap 方案的优势非常明显:

// 旧 NTFS3 的写入路径(buffer_head 版本)— 问题重重

static int ntfs3_write_begin(struct file *file,
                             struct address_space *mapping,
                             loff_t pos, unsigned len,
                             struct page **pagep, void **fsdata)
{
    // 问题 1:每个 page 需要创建 buffer_head 链表
    // 大文件写入时,buffer_head 对象数量爆炸
    struct buffer_head *bh;
    struct page *page = grab_cache_page_write_begin(mapping, ...);
    
    // 问题 2:每个 buffer_head 需要单独的块映射查找
    // 4KB page × 512B 块 = 8 次 block_read_full_page
    create_empty_buffers(page, sb->s_blocksize, 0);
    bh = page_buffers(page);
    while (bh) {
        // 逐块映射,效率低下
        map_buffer_to_page(mapping, bh, ...);
        bh = bh->b_this_page;
    }
    
    // 问题 3:无法支持延迟分配
    // 必须立即分配物理块并更新 MFT
    err = ntfs3_alloc_clusters(sbi, &lcn, &clen);
    
    *pagep = page;
    return 0;
}

三、folio 内存模型:比 page 更大的内存管理单元

3.1 folio 解决了什么问题

Linux 内核从 2.4 时代就使用 struct page 作为内存管理的基本单位,每个 page 固定 4KB。这在 2026 年看来有几个严重问题:

  1. 指针开销:每个 page 有 64 字节的 struct page,1TB 内存需要 16GB 仅仅存放 page 结构
  2. 操作放大:处理一个大文件(比如 4TB NTFS 分区)需要操作 10 亿个 page
  3. 锁粒度太细:每个 page 一个锁,导致大量细粒度竞争

folio 的核心思想:允许将多个连续 page 组合为一个 folio,用单个结构体和单个锁管理

// struct folio — page 的替代品

struct folio {
    // 这些字段与 struct page 的前几个字段兼容
    // 使得 folio 可以安全地 cast 为 page
    unsigned long flags;       // PG_locked, PG_dirty, PG_writeback 等
    struct address_space *mapping;  // 所属的地址空间
    pgoff_t index;             // 在地址空间中的位置
    union {
        struct list_head lru;  // LRU 链表
        struct {
            void *__pad;
            unsigned long mlock_count;
        };
    };
    // ... 更多字段
};

// folio 的关键 API
size_t folio_size(struct folio *folio);        // folio 的大小(可以是 4KB, 16KB, ..., 2MB)
struct page *folio_page(struct folio *folio, unsigned n);  // 获取 folio 中的第 n 个 page
unsigned folio_nr_pages(struct folio *folio);  // folio 包含的 page 数量
bool folio_test_large(struct folio *folio);    // 是否是大 folio

3.2 folio 在文件系统中的实际收益

对于 NTFS 新驱动来说,folio 的收益主要体现在三方面:

(1)减少 address_space 操作的调用次数

// 旧方案:逐 page 操作
// 写入 64KB 数据需要操作 16 个 page

for (i = 0; i < 16; i++) {
    struct page *page = grab_cache_page(mapping, index + i);
    // 每个 page 单独获取锁
    lock_page(page);
    // 每个 page 单独标记脏
    set_page_dirty(page);
    unlock_page(page);
    put_page(page);
}
// 总共:16 次 lock/unlock + 16 次 dirty 标记

// 新方案:folio 操作
// 写入 64KB 数据只需要操作 1 个 order-4 folio

struct folio *folio = filemap_grab_folio(mapping, index);
// 一次锁覆盖整个 64KB
folio_lock(folio);
// 一次脏标记覆盖整个 64KB
folio_mark_dirty(folio);
folio_unlock(folio);
folio_put(folio);
// 总共:1 次 lock/unlock + 1 次 dirty 标记

(2)减少 writeback 的 bio 提交次数

// writeback 路径的 folio 优化

static int ntfs_submit_folio(struct folio *folio,
                              struct writeback_control *wbc)
{
    struct bio *bio;
    struct inode *inode = folio->mapping->host;
    
    // 一个 order-4 folio(64KB)只需要一个 bio_vec
    // 而不是 16 个 page 各需要一个 bio_vec
    bio = bio_alloc(inode->i_sb->s_bdev, 
                    folio_nr_pages(folio),  // 一次分配足够的 bio_vec
                    REQ_OP_WRITE, 
                    GFP_NOFS);
    
    // 整个 folio 一次性加入 bio
    bio_add_folio(bio, folio, folio_size(folio), 
                  folio_pos(folio) - folio->mapping->host->i_ino);
    
    // 一次提交,而不是 16 次
    submit_bio(bio);
    return 0;
}

(3)减少内存分配器压力

// folio 分配 vs page 分配

// 旧方案:分配 16 个 page
for (i = 0; i < 16; i++) {
    pages[i] = alloc_page(GFP_NOFS);
    // 16 次分配器调用,16 次 per-cpu 操作
}

// 新方案:分配 1 个 order-4 folio
struct folio *folio = filemap_alloc_folio(GFP_NOFS, 4);
// 1 次分配器调用,连续物理内存
// 更好的 TLB 局部性

3.3 NTFS 新驱动的 folio 适配

// NTFS 新驱动中 folio 相关的操作实现

const struct address_space_operations ntfs_aops = {
    // folio 版本的脏页标记
    .dirty_folio      = filemap_dirty_folio,
    
    // folio 版本的迁移(用于内存规整)
    .migrate_folio    = buffer_migrate_folio,
    
    // folio 版本的缓存失效
    .invalidate_folio = iomap_invalidate_folio,
    
    // folio 版本的释放判断
    .release_folio    = iomap_release_folio,
    
    // folio 版本的写入开始/结束
    .write_begin      = iomap_write_begin,  // iomap 统一入口
    .write_end        = iomap_write_end,
    
    // DIO 支持
    .direct_IO        = iomap_dio_rw,
    
    // writeback 支持
    .writepages       = iomap_writepages,
};

四、FRED:Intel 的事件分发革命

4.1 FRED 是什么

Linux 7.1 引入了对 Intel FRED(Flexible Return and Event Delivery)的支持,这是 Intel 为 Panther Lake 及后续处理器设计的全新异常/中断/事件处理机制。

传统 x86 的中断和异常处理路径是从 8086 时代继承下来的,已经积累了 40 多年的技术债务:

传统 IDT(Interrupt Descriptor Table)路径:

CPU 触发异常/中断
    → 查找 IDT 表获取门描述符
    → 切换栈(如果特权级变化)
    → 保存部分寄存器(CPU 自动保存的 5 个)
    → 软件保存剩余寄存器(中断处理程序手动保存)
    → 执行中断处理程序
    → 软件恢复寄存器
    → iret 指令返回

问题:
1. iret 是非原子操作,NMI 嵌套时可能破坏栈
2. 栈切换逻辑复杂,依赖 TR(Task Register)
3. 寄存器保存/恢复路径不统一
4. 安全漏洞面大(Spectre/MDS 类攻击常利用中断路径)

FRED 重新设计了这条路径:

FRED 路径:

CPU 触发事件(异常/中断/NMI/#MC)
    → CPU 自动保存完整的寄存器状态到 FRED 栈帧
    → 切换到 FRED 专用栈
    → 一次性设置新的 CS:RIP
    → 执行统一的事件处理程序
    → ERETS 指令原子返回

改进:
1. ERETS 是原子操作,无 iret 的嵌套风险
2. CPU 自动保存所有寄存器,无软件保存需求
3. 统一的事件入口,消除了 IDT 的碎片化
4. 更小的攻击面,更安全

4.2 Linux 7.1 的 FRED 内核适配

// arch/x86/kernel/fred.c — FRED 事件入口(简化)

/*
 * FRED 事件入口点
 * 所有异常和中断通过统一的 FRED 入口进入内核
 * 不再需要 IDT 中的多个入口点
 */

// FRED 栈帧布局 — CPU 自动保存
struct fred_stack {
    u64 r15, r14, r13, r12, rbp, rbx;
    u64 r11, r10, r9, r8, rax, rcx, rdx, rsi, rdi;
    u64 orig_rax;    // 事件向量号
    u64 error_code;  // 错误码(如果适用)
    u64 rip;
    u64 cs;
    u64 rflags;
    u64 rsp;
    u64 ss;
};

// FRED 统一事件处理器
DEFINE_FRED_HANDLER(fred_event_entry)
{
    struct fred_stack *frame = (struct fred_stack *)__fred_rsp();
    unsigned int vector = frame->orig_rax;
    
    // FRED 将所有事件路由到统一入口
    // 根据向量号分发到具体处理程序
    switch (vector) {
    case X86_TRAP_DE:   // 除零错误
        return exc_divide_error(frame);
    case X86_TRAP_PF:   // 页错误
        return exc_page_fault(frame);
    case X86_TRAP_NMI:  // NMI
        return exc_nmi(frame);
    case X86_TRAP_MC:   // 机器检查
        return exc_machine_check(frame);
    default:
        // 通用中断处理
        if (vector >= FIRST_EXTERNAL_VECTOR) {
            return fred_handle_irq(frame, vector);
        }
        return fred_handle_exception(frame, vector);
    }
}

// ERETS — FRED 的原子返回指令
// 替代了 iret,消除了 iret 的嵌套风险
static inline void fred_return(struct fred_stack *frame)
{
    // ERETS 从 FRED 栈帧恢复所有寄存器
    // 这是一个原子操作,不会被 NMI 打断后破坏
    asm volatile("ERETS");
}

4.3 FRED 对性能的影响

FRED 的性能收益主要体现在减少中断处理的开销:

// 传统 IDT 路径的寄存器保存(软件完成)

// entry_64.S — 传统中断入口
ENTRY(interrupt_entry)
    // 手动保存寄存器 — 大约 20+ 条指令
    pushq   %rdi
    pushq   %rsi
    pushq   %rdx
    pushq   %rcx
    pushq   %rax
    pushq   %r8
    pushq   %r9
    pushq   %r10
    pushq   %r11
    // ... 更多寄存器
    // 还需要处理 SWAPGS、内核栈切换等
    // 总计约 40-60 条指令才能进入中断处理
END(interrupt_entry)

// FRED 路径 — CPU 自动完成
// fred_event_entry 直接从完整栈帧开始
// 不需要手动保存任何寄存器
// 节省约 40-60 条指令的中断入口开销
// 在高频中断场景(网络、存储)下收益显著

对 NTFS 新驱动的影响:在高 IOPS 的存储场景下,中断处理的开销占比可观。FRED 减少了每次 I/O 完成中断的入口开销,对于 NVMe 设备的轮询模式和中断模式都有实际收益。


五、三行代码的魔法:Linux 7.2 iomap 优化深度分析

5.1 问题发现

2026 年 6 月,字节跳动工程师 Fengnan Chang 在审查 iomap 框架代码时发现了一个看似无害但实际上浪费巨大的模式:

// 优化前的 iomap_iter() 函数(Linux 7.1)

/*
 * iomap_iter() — iomap 迭代器的核心循环
 * 每次迭代处理一个块映射区间
 */
int iomap_iter(struct iomap_iter *iter, const struct iomap_ops *ops)
{
    struct iomap *iomap = &iter->iomap;
    int ret;
    
    // 调用文件系统的 iomap_begin 获取块映射
    ret = ops->iomap_begin(iter->inode, iter->pos, iter->len,
                           iter->flags, iomap, &iter->srcmap);
    if (ret)
        return ret;
    
    // 执行实际的 I/O 操作(读/写)
    ret = iomap_iter_handle(iter);
    
    // 调用文件系统的 iomap_end 清理
    if (ops->iomap_end) {
        ret2 = ops->iomap_end(iter->inode, iter->pos, iter->len,
                              ret, iter->flags, iomap);
    }
    
    // *** 问题在这里 ***
    // 每次迭代结束后,清空 iomap 结构体
    // 但调用者在迭代结束后会直接丢弃迭代器!
    // 这次 memset 是完全无用的
    memset(iomap, 0, sizeof(*iomap));  // ← 浪费的内存写入
    
    return ret;
}

5.2 优化的原理

// 优化后的 iomap_iter() 函数(Linux 7.2)

int iomap_iter(struct iomap_iter *iter, const struct iomap_ops *ops)
{
    struct iomap *iomap = &iter->iomap;
    int ret;
    
    ret = ops->iomap_begin(iter->inode, iter->pos, iter->len,
                           iter->flags, iomap, &iter->srcmap);
    if (ret)
        return ret;
    
    ret = iomap_iter_handle(iter);
    
    if (ops->iomap_end) {
        ret2 = ops->iomap_end(iter->inode, iter->pos, iter->len,
                              ret, iter->flags, iomap);
    }
    
    // 移除无用的 memset
    // 调用者要么会重新初始化 iomap(下次迭代)
    // 要么会丢弃整个迭代器
    // 无论哪种情况,memset 都是浪费
    
    return ret;
}

看起来只移除了一行 memset,但实际上是三行代码的位置调整——包括相关的条件判断和注释。

5.3 为什么两行代码能提升 5% IOPS

关键在于调用频率缓存行效应

// iomap_iter 的调用频率分析

// 在 fio 高 IOPS 测试中:
// - NVMe SSD 可以做到 100 万+ IOPS
// - 每个 I/O 操作至少调用一次 iomap_iter
// - 大文件操作可能多次调用(多次迭代)

// 每次 iomap_iter 调用中的 memset 开销:
// sizeof(struct iomap) ≈ 80-120 字节
// memset(iomap, 0, sizeof(*iomap)) ≈ 80-120 字节的内存写入

// 在高 IOPS 场景下的总开销:
// 1,000,000 IOPS × 100 字节 = 100MB/s 的无用内存写入
// 这 100MB/s 完全是浪费的——写入的数据从未被读取

// 移除后的收益:
// 1. 减少了 100MB/s 的内存带宽占用
//    → 内存带宽可以用于实际数据传输
// 2. 减少了 iomap 结构体所在的缓存行失效
//    → iomap 更可能留在 L1/L2 缓存中
// 3. 减少了 store buffer 的压力
//    → CPU 的 store buffer 不再被无用写入填满

5.4 实测验证

# 使用 fio 验证 iomap memset 优化的效果

# 测试环境
# - CPU: AMD EPYC 9654 (96 cores)
# - NVMe: Samsung PM9A3 3.84TB
# - 内核: Linux 7.2-rc1 (含优化) vs Linux 7.1 (无优化)
# - 文件系统: XFS, ext4

# 4K 随机读 — 基准测试(不受 iomap 写入路径影响)
fio --name=rand-read --ioengine=io_uring --iodepth=128 \
    --rw=randread --bs=4k --size=10G --numjobs=16 \
    --time_based --runtime=60

# 4K 随机写 — 受 iomap 影响
fio --name=rand-write --ioengine=io_uring --iodepth=128 \
    --rw=randwrite --bs=4k --size=10G --numjobs=16 \
    --time_based --runtime=60

# 结果(示意):
# ext4 randread:  基本无变化(读路径不触发 iomap memset)
# ext4 randwrite: +4.8% IOPS
# xfs randread:   基本无变化
# xfs randwrite:  +5.2% IOPS
#
# 在 io_uring + 高 iodepth 场景下收益最大
# 因为 io_uring 的批量提交放大了 iomap_iter 的调用频率

六、Linux 7.1/7.2 存储栈的全景图

把所有这些改进放在一起,我们可以看到 Linux 存储栈在 2026 年的整体演进:

Linux 7.1/7.2 存储栈演进全景

┌──────────────────────────────────────────────────────┐
│                   用户空间 (User Space)                │
│  fio / io_uring / fsp / 直接 I/O 应用                  │
├──────────────────────────────────────────────────────┤
│                 VFS / 系统调用层                        │
│  read/write → iomap_iter → writepages                 │
├──────────────────────────────────────────────────────┤
│              iomap 框架 (统一映射层)                    │
│  [7.2] 移除无用 memset → +5% IOPS                    │
│  四种映射类型: HOLE / DELALLOC / MAPPED / UNWRITTEN   │
├──────────────────────────────────────────────────────┤
│             文件系统层 (File Systems)                   │
│  [7.1] 新 NTFS → iomap + folio + 延迟分配            │
│  [7.1] ext4, XFS → folio 优化持续进行                 │
│  [7.1] Btrfs → 进一步 iomap 适配                      │
├──────────────────────────────────────────────────────┤
│            页缓存 / folio 层                           │
│  [7.1] folio 全面替代 page                            │
│  大 folio 减少锁竞争和操作放大                          │
├──────────────────────────────────────────────────────┤
│             块设备 / bio 层                            │
│  bio → NVMe / SCSI / virtio                          │
├──────────────────────────────────────────────────────┤
│              硬件中断层                                │
│  [7.1] FRED → 统一事件入口,减少中断开销               │
│  传统 IDT → 仍支持,逐步淘汰                          │
└──────────────────────────────────────────────────────┘

七、实战:在 Linux 7.1 上编译和测试新 NTFS 驱动

7.1 编译启用新 NTFS 驱动的内核

# 获取 Linux 7.1 源码
git clone --depth 1 --branch v7.1 \
    https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux

# 配置内核 — 启用新 NTFS 驱动
make menuconfig

# File systems → DOS/FAT/EXFAT/NT Filesystems
#   <*> NTFS file system support (new driver)  ← 启用新驱动
#   < > NTFS 3.0 driver (Paragon)              ← 禁用旧驱动
#   < > NTFS read-only driver                  ← 禁用只读驱动

# 或者直接修改 .config
echo "CONFIG_NTFS_FS=y" >> .config
echo "# CONFIG_NTFS3_FS is not set" >> .config
echo "# CONFIG_NTFS_FS_OLD is not set" >> .config

# 编译
make -j$(nproc)

# 安装
sudo make modules_install
sudo make install

# 更新 GRUB
sudo update-grub
sudo reboot

7.2 验证新驱动已加载

# 检查 NTFS 模块
cat /proc/filesystems | grep ntfs
# 应该只看到一行 ntfs(新驱动)

# 检查模块信息
modinfo ntfs
# 应该看到 iomap 相关的依赖

# 挂载 NTFS 分区
sudo mount -t ntfs /dev/nvme0n1p3 /mnt/windows

# 验证延迟分配已启用
# 查看挂载选项
mount | grep ntfs
# 应该看到 delalloc 选项

# 基本性能测试
# 写入测试
dd if=/dev/zero of=/mnt/windows/testfile bs=1M count=1024 oflag=direct
# 读取测试
dd if=/mnt/windows/testfile of=/dev/null bs=1M iflag=direct

# 使用 fio 进行精确测试
fio --name=ntfs-write --filename=/mnt/windows/testfile \
    --ioengine=libaio --iodepth=64 \
    --rw=write --bs=1M --size=4G \
    --numjobs=4 --group_reporting

7.3 对比新旧驱动的性能

# 测试脚本 — 对比 NTFS3 和新 NTFS 驱动

#!/bin/bash
# ntfs_benchmark.sh

MOUNT_POINT="/mnt/ntfs_test"
TEST_DEVICE="/dev/nvme0n1p3"
FIO_JOB="/tmp/ntfs_fio.job"

cat > $FIO_JOB << EOF
[global]
ioengine=io_uring
iodepth=128
size=2G
numjobs=4
group_reporting
time_based
runtime=30

[seq-write]
rw=write
bs=1M

[rand-write]
rw=randwrite
bs=4k

[seq-read]
rw=read
bs=1M

[rand-read]
rw=randread
bs=4k
EOF

# 测试新 NTFS 驱动
echo "=== Testing new NTFS driver ==="
sudo mount -t ntfs $TEST_DEVICE $MOUNT_POINT
fio $FIO_JOB --directory=$MOUNT_POINT --output=new_ntfs.json --output-format=json
sudo umount $MOUNT_POINT

# 测试 NTFS3 驱动(需要重启并切换内核配置)
echo "=== Switch to NTFS3 and reboot to test ==="
echo "After reboot with NTFS3 enabled, run:"
echo "fio $FIO_JOB --directory=$MOUNT_POINT --output=ntfs3.json --output-format=json"

八、Linux 7.1 的其他重要特性

8.1 移除 Intel 486 支持

Linux 7.1 正式移除了对 Intel 486 处理器的支持。这不是一个简单的清理——它允许内核使用 486 不支持的指令和特性:

// 移除 486 支持后的优化示例

// 旧代码需要兼容 486(无 cmpxchg8b)
static inline int atomic64_cmpxchg(atomic64_t *v, long long old, long long new)
{
    // 486 上需要用 cmpxchg8b 指令模拟
    // 或者回退到自旋锁
    unsigned long flags;
    long long val;
    raw_spin_lock_irqsave(&atomic64_lock, flags);
    val = v->counter;
    if (val == old)
        v->counter = new;
    raw_spin_unlock_irqrestore(&atomic64_lock, flags);
    return val;
}

// 新代码可以直接使用 64 位原子操作
static inline long long atomic64_cmpxchg(atomic64_t *v, long long old,
                                          long long new)
{
    // 586+ 有 cmpxchg8b,可以直接使用
    // 更快,无锁竞争
    return cmpxchg8b(&v->counter, old, new);
}

8.2 Apple Silicon 电池状态报告

Linux 7.1 新增了 Apple Silicon 设备的电池状态报告支持,这对在 M1/M2/M3 Mac 上运行 Linux 的用户来说是实用改进。

8.3 AMD CPU 电源管理优化

针对 AMD Ryzen/EPYC 处理器的电源管理状态切换进行了优化,减少了不必要的功耗。

8.4 Intel Arc Battlemage 图形支持

对 Intel Arc Battlemage 系列显卡的驱动支持进一步成熟。


九、Linux 7.2 前瞻:M3 启动支持与更多优化

Linux 7.2 的合并窗口已在 6 月中旬开启,已知的重大特性包括:

9.1 Apple M3 系列设备启动支持

开发者 Sven Peter 提交的补丁将使 Linux 7.2 主线内核首次支持苹果 M3 系列芯片设备启动,覆盖 iMac、MacBook Air 和 MacBook Pro。

9.2 移除上古驱动

继 Linux 7.1 移除 486 支持后,Linux 7.2 继续清理旧硬件驱动,包括超过 20 年未更新的 DoubleTalk ISA 语音合成卡驱动。

9.3 NVK 开源驱动获得 DLSS 支持

Mesa 26.2 合并了 NVK(NVIDIA 开源 Vulkan 驱动)的 DLSS 支持,Linux 上的 NVIDIA 游戏体验将显著改善。


十、对开发者的启示:从内核优化中学到什么

10.1 无用代码是最贵的代码

iomap 的 memset 优化告诉我们:即使是一行看似无害的初始化代码,在高频路径上也可能成为性能杀手。在你的项目中,检查以下高频路径是否有类似的"安全但无用"的操作:

// 常见的高频路径性能陷阱

// 陷阱 1:循环内的冗余初始化
for (int i = 0; i < N; i++) {
    memset(&ctx, 0, sizeof(ctx));  // 每次循环都清零
    ctx.handle_request(requests[i]);
    // ctx 在下次循环开始时又被 memset 清零
    // 但 handle_request 内部已经初始化了所有需要的字段
}

// 优化:移除冗余 memset,或确保只在必要时清零

// 陷阱 2:热路径上的日志
void process_request(Request *req) {
    log_debug("Processing request %p", req);  // debug 日志在生产环境可能关闭
    // 但函数调用和参数评估的开销仍然存在
    do_process(req);
}

// 优化:使用编译期条件或 unlikely 标注
void process_request(Request *req) {
    if (unlikely(log_level >= LOG_DEBUG))
        log_debug("Processing request %p", req);
    do_process(req);
}

// 陷阱 3:过度防御性的错误检查
int read_data(int fd, void *buf, size_t len) {
    if (fd < 0) return -EINVAL;      // 在热路径上反复检查
    if (!buf) return -EINVAL;        // 调用者保证非 NULL
    if (len == 0) return 0;          // 调用者保证 len > 0
    return do_read(fd, buf, len);
}

// 优化:在入口点检查一次,内部函数信任调用者

10.2 延迟分配思想的应用

NTFS 新驱动的延迟分配策略不仅适用于文件系统,在应用层同样有用:

# 应用层的"延迟分配"模式

class BatchProcessor:
    """延迟分配的应用层实现"""
    
    def __init__(self):
        self.pending_ops = []  # 延迟的操作队列
        self.batch_size = 1000
    
    def write(self, key: str, value: str):
        """写入操作不立即执行,而是缓存起来"""
        self.pending_ops.append(("write", key, value))
        
        # 达到批量阈值时一次性提交
        if len(self.pending_ops) >= self.batch_size:
            self.flush()
    
    def flush(self):
        """批量提交所有延迟的操作"""
        if not self.pending_ops:
            return
        
        # 一次性提交 → 减少网络往返和事务开销
        # 类似于 NTFS 延迟分配的 writeback
        batch = self.pending_ops
        self.pending_ops = []
        
        # 可以在 flush 时做优化:合并相同 key 的写入
        optimized = self._merge_writes(batch)
        self._submit_batch(optimized)
    
    def _merge_writes(self, ops):
        """合并同一 key 的多次写入,只保留最后一次"""
        latest = {}
        for op_type, key, value in ops:
            latest[key] = value
        return [("write", k, v) for k, v in latest.items()]

10.3 folio 思想:批量操作的力量

folio 的核心思想——用更大的操作单元减少操作次数——在分布式系统中同样适用:

# folio 思想在分布式系统中的应用

class BatchRPC:
    """批量 RPC — folio 思想的网络版"""
    
    def __init__(self, stub, max_batch_size=100, max_latency_ms=5):
        self.stub = stub
        self.max_batch_size = max_batch_size
        self.max_latency_ms = max_latency_ms
        self.pending = []
        self.timer = None
    
    async def call(self, request):
        """单个请求不立即发送,而是攒批"""
        future = asyncio.get_event_loop().create_future()
        self.pending.append((request, future))
        
        if len(self.pending) >= self.max_batch_size:
            await self._flush()
        elif not self.timer:
            self.timer = asyncio.ensure_future(
                asyncio.sleep(self.max_latency_ms / 1000, 
                              result=self._flush())
            )
        
        return await future
    
    async def _flush(self):
        """批量发送 — 减少网络往返次数"""
        if not self.pending:
            return
        
        batch = self.pending
        self.pending = []
        self.timer = None
        
        requests = [r for r, _ in batch]
        futures = [f for _, f in batch]
        
        try:
            # 一次 RPC 调用发送所有请求
            responses = await self.stub.BatchCall(requests)
            for future, response in zip(futures, responses):
                future.set_result(response)
        except Exception as e:
            for future in futures:
                future.set_exception(e)

总结:从"重生"到"革命"

Linux 7.1 的 NTFS 重生和 Linux 7.2 的两行代码优化,看似是两个不同层面的改进,但它们共享同一个底层逻辑:消除不必要的开销,让每一行代码都为实际工作服务

NTFS 新驱动消除了 20 年来旧驱动的不必要限制(只读、无延迟分配、buffer_head 开销),而 iomap 的 memset 移除则消除了每次迭代的不必要内存写入。前者是架构级的重构,后者是微秒级的优化,但目标一致:让 I/O 路径上的每一个操作都是有意义的

对 Linux 内核开发者来说,这是 2026 年最好的时代:iomap + folio 的现代化基础设施已经就绪,FRED 的事件分发机制为新处理器铺平了道路,NTFS 的重生让跨平台文件交换不再痛苦。

对应用开发者来说,这些内核级的优化提供了宝贵的思维模型:延迟分配、批量操作、消除无用代码——这些原则不分内核态和用户态,在任何高性能系统中都适用。

Linus Torvalds 用 "resurrection" 形容 NTFS 的重生。但在我看来,2026 年 Linux 内核存储栈的变革不仅仅是某个驱动的重生,而是一次从底层到顶层的系统性革命。从 FRED 的中断路径优化,到 iomap 的映射框架统一,到 folio 的内存管理现代化,再到文件系统驱动的全面重构——每一层都在向同一个方向演进:更快、更简洁、更少浪费

这就是 Linux 的方式。不是推倒重来,而是一层一层地打磨,直到每个路径都无可优化。


参考资源:

  • Linux 7.1 NTFS 合并提交:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=cdd4dc3aebeab43a72ce0bc2b5bab6f0a80b97a5
  • iomap 框架文档:https://www.kernel.org/doc/html/latest/filesystems/iomap/index.html
  • folio 设计文档:https://lwn.net/Articles/849538/
  • FRED 规范:Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3, Chapter 6
  • Linux 7.2 iomap 优化:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/log/?qt=fengnan+chang+iomap
复制全文 生成海报 Linux 内核 NTFS iomap folio FRED 存储 性能优化

推荐文章

JavaScript中的常用浏览器API
2024-11-18 23:23:16 +0800 CST
推荐几个前端常用的工具网站
2024-11-19 07:58:08 +0800 CST
如何使用go-redis库与Redis数据库
2024-11-17 04:52:02 +0800 CST
快手小程序商城系统
2024-11-25 13:39:46 +0800 CST
程序员茄子在线接单