Skip to content

leafxuzm/Zerofault

Repository files navigation

Zerofault

预分配物理页 + 一次 mmap 建立直接映射 → 用户态零缺页访问的 Linux 内核模块

Linux License LOC Version


解决什么问题

普通 mmap 在首次访问每页时触发缺页中断(page fault),逐页分配物理内存并填充 PTE——这对高频交易等低延迟场景不可接受。Zerofault 在 insmod 时一次性预分配所有物理页并 pin 住,mmap 时直接建立全部映射,用户态访问永不缺页。

传统 mmap:  mmap→[缺页→alloc→建PTE→TLB] × N页  (逐页,抖动)
Zerofault:  insmod[预分配全部物理页] → mmap[一次建PTE] → 零缺页访问

架构总览

flowchart TB
    subgraph User["用户态"]
        APP[测试程序 / HFT策略]
        DEV["/dev/ptemap"]
    end

    subgraph Kernel["内核态 (ptemap.ko)"]
        CDEV[ptemap_cdev.c<br/>cdev + mmap 回调]
        PTE[ptemap_pte.c<br/>v1.1: apply_to_page_range<br/>+ set_pte_at 直写]
        CORE[ptemap_core.c<br/>物理页分配/释放]
        MAIN[ptemap_main.c<br/>模块生命周期]
        DBGFS[ptemap_debugfs.c<br/>调试接口]
        PARAM[module_param<br/>phys_pages / target_pid<br/>use_direct_pte]
    end

    subgraph Debug["调试接口"]
        STATUS["/sys/kernel/debug/ptemap/status"]
        MAPS["/sys/kernel/debug/ptemap/mappings"]
        STATS["/sys/kernel/debug/ptemap/stats"]
        CACHE["/sys/kernel/debug/ptemap/cache_policy"]
    end

    subgraph HW["物理内存"]
        PAGES["预分配物理页<br/>alloc_page(GFP_KERNEL)<br/>+ get_page() pin住"]
    end

    APP -->|open/mmap| DEV --> CDEV
    CDEV -->|"v1.0: vm_insert_page"| PAGES
    CDEV -->|"v1.1: use_direct_pte=1"| PTE
    PTE -->|"apply_to_page_range<br/>逐页 pfn_pte + set_pte_at"| PAGES
    MAIN --> PARAM
    MAIN --> CORE
    MAIN --> CDEV
    MAIN --> DBGFS
    DBGFS --> STATUS
    DBGFS --> MAPS
    DBGFS --> STATS
Loading

mmap 流程

v1.1 PTE 直写(默认推荐)

sequenceDiagram
    participant U as 用户进程
    participant VFS as VFS 层
    participant CDEV as ptemap_cdev.c
    participant PTE as ptemap_pte.c
    participant MM as 物理页

    Note over MM: insmod 时已预分配<br/>alloc_page() + get_page()

    U->>VFS: open("/dev/ptemap")
    VFS->>CDEV: ptemap_open()
    CDEV-->>U: fd

    U->>VFS: mmap(size)
    VFS->>CDEV: ptemap_mmap(vma)
    CDEV->>CDEV: use_direct_pte=1 →<br/>ptemap_mmap_direct(vma)
    CDEV->>PTE: apply_to_page_range(mm, start, size, callback)
    Note over PTE: 内核分配 PGD→PUD→PMD→PTE 中间层级
    loop 每页
        PTE->>PTE: pfn_pte(pfn, pgprot) +<br/>set_pte_at(mm, addr, pte, new_pte)
        Note over PTE: 直接构造并写入 PTE<br/>零 rmap 开销
    end

    PTE-->>U: 虚拟地址 ptr
    Note over U: 访问 ptr[i] → MMU 页表命中 → 零缺页!
    U->>MM: load/store 直接命中
Loading

v1.0 vm_insert_page(兼容保留)

sequenceDiagram
    participant U as 用户进程
    participant CDEV as ptemap_cdev.c
    participant MM as 物理页

    Note over MM: insmod 时已预分配

    U->>CDEV: mmap(size)
    loop 每页
        CDEV->>MM: vm_insert_page(vma, vaddr, page[i])
        Note over CDEV,MM: 内核标准 API<br/>有 rmap 开销 ~60 cycles/页
    end
    CDEV-->>U: 虚拟地址 ptr
    U->>MM: 访问 → 零缺页
Loading

源码结构

文件 行数 职责
ptemap_main.c 177 模块生命周期:init 参数校验 → 找目标进程 → 分配页+缓存数组 → 注册 cdev → 创建 debugfs;exit 逆序清理 + PTE 回滚
ptemap_core.c 112 物理页管理:预分配/释放 (alloc_page+get_page/free_reserved_page),Per-page 缓存策略数组分配/释放
ptemap_cdev.c 299 /dev/ptemap 字符设备:open(访问控制)、mmap(v1.0 vm_insert_page / v1.1 apply_to_page_range)、ioctl(v1.3 QUERY/QUERY_RANGE/FLUSH_TLB)
ptemap_pte.c 232 v1.1 PTE 直写 + v1.2 逐页 cache 策略 + v1.3 TLB flush + v1.3.1 PTE 清除/回滚
ptemap_debugfs.c 265 4 个 debugfs 文件:statusmappingsstatscache_policy(rw,逐页设置 WC/WB/UC/WT)
ptemap_core.h 92 全局状态结构体 + API 声明 + cache 枚举 + 常量
ptemap.h 88 UAPI 头文件:ioctl 命令号 + 数据结构,用户态 #include "ptemap.h"
test_ptemap.c 295 用户态测试:open → mmap → 写/读验证 → 跨页边界 → ioctl QUERY → ioctl QUERY_RANGE → ioctl FLUSH_TLB
合计 1560

模块参数

参数 类型 默认值 说明
phys_pages int 256 预分配物理页数量(256 = 1MB)
target_pid int 0 允许访问的进程 PID(0 = 任意进程)
use_direct_pte int 0 PTE 直写模式:0=vm_insert_page(v1.0) 1=apply_to_page_range+set_pte_at(v1.1)
# v1.0 默认路径(vm_insert_page)
insmod ptemap.ko phys_pages=512 target_pid=0

# v1.1 PTE 直写路径(apply_to_page_range,零 rmap)
insmod ptemap.ko phys_pages=512 use_direct_pte=1

编译 & 测试

前置条件:Linux 6.16.2 内核编译环境,KERNELDIR 指向 pre-built kernel tree。

# ===== 编译 =====
cd ptemap
make KERNELDIR=/path/to/kernel/build

# ===== 加载 =====
insmod ptemap.ko phys_pages=256

# ===== 调试查看 =====
cat /sys/kernel/debug/ptemap/status
#  state:     LIVE
#  version:   1.3.0
#  pages:     256 (total)
#  size:      1048576 bytes (1 MB)
#  target_pid: 0
#  direct_pte: 1 (PTE direct write)
#  vaddr:     0x7f...-0x7f...
#  tlb_flush: 0

cat /sys/kernel/debug/ptemap/cache_policy
#  pages count  mode
#  ----- ------ ------
#  all   256    WC

echo "0-63 WB" > /sys/kernel/debug/ptemap/cache_policy
#  → 前 64 页切换为 Write-Back

cat /sys/kernel/debug/ptemap/mappings
#  idx   vaddr              pfn                 size
#  ----- ------------------ ------------------ ----------
#  0     0x00007f...        0x0000000000123abc 4KB
#  ...

# ===== 运行测试(v1.1 直写路径)=====
insmod ptemap.ko phys_pages=256 use_direct_pte=1
./test_ptemap 64
#  === ptemap test ===
#  device:    /dev/ptemap
#  nr_pages:  64 (256 KB)
#  [1] open  OK (fd=3)
#  ptemap: mmap DIRECT OK: vaddr=0x7f...-0x7f... pages=64 pid=61
#  [2] mmap OK (vaddr=0x7f..., size=256 KB)
#  [3] writing pattern...
#  [3] write OK (64 pages)
#  [4] verifying...
#  [4] verify OK (0 errors)
#  [5] cross-page boundary test...
#  [5] boundary OK
#  --- ioctl QUERY test ---
#    page[  0] pfn=0x5002 vaddr=0x7fc14036c000 cache=0
#    page[  1] pfn=0x4ffb vaddr=0x7fc14036d000 cache=0
#    ...
#  QUERY OK (0 errors)
#  --- ioctl QUERY_RANGE test ---
#  QUERY_RANGE OK (64 pages, 0 errors)
#  --- ioctl FLUSH_TLB test ---
#  FLUSH_TLB OK
#  FLUSH_TLB_RANGE OK (range 0x0-0x1000)
#  === result: PASS ===

# ===== 验证零缺页 =====
perf stat -e page-faults ./test_ptemap 256
#  page-faults: 0  ← 关键指标

# ===== 卸载 =====
rmmod ptemap

设备模型:物理内存伪装成字符设备

ptemap 利用 Linux 设备模型将一段预分配的物理内存包装成一个伪字符设备,用户态通过标准文件操作访问。

cdev 注册 → /dev/ptemap 自动创建

cdev_init(&cdev, &ptemap_fops)         ① 绑定 file_operations 到 cdev 结构体
cdev_add(&cdev, dev_num, 1)            ② 向 VFS 注册:设备号 → cdev 的映射
class_create("ptemap")                 ③ 在 /sys/class/ptemap/ 创建设备类目录
device_create(class, ..., dev_num, "ptemap")
                                       ④ 写入设备属性 → 触发 uevent (KOBJ_ADD)
                                           │
                                           ▼
udevd 收到 KOBJ_ADD → 解析设备号 → mknod /dev/ptemap

device_create() 是连接内核态和用户态的桥梁:内核在 /sys/class/ptemap/ 下写入设备号等信息,然后通过 netlink socket 广播 KOBJ_ADD 事件。用户态的 udevd 守护进程收到后,自动执行 mknod/dev/ 下创建设备节点。这样驱动模块做到了"插拔即用",无需手动 mknod

文件操作 → 内核回调的路由

当用户态程序操作 /dev/ptemap 时,内核根据设备号查找已注册的 file_operations,将系统调用路由到模块的回调函数:

用户态调用 内核路由 模块回调 实际效果
open("/dev/ptemap", O_RDWR) VFS → 设备号查找 → cdev ptemap_open() PID 访问控制
mmap(..., fd, 0) VFS → 设备号查找 → cdev ptemap_mmap()ptemap_mmap_direct() apply_to_page_range + set_pte_at 直写 PTE
ioctl(fd, PTEMAP_IOC_QUERY, &req) VFS → 设备号查找 → cdev ptemap_ioctl()ptemap_ioctl_query() 查询 PFN/VA/cache
close(fd) VFS → 设备号查找 → cdev ptemap_release() 清理私有数据

本质

ptemap 没有真实的硬件。它把内核预分配的物理页(带独立 cache 策略),通过 cdev 框架暴露为一个"内存设备"。用户态的 ptr = mmap(fd) 返回的虚拟地址,由模块在 mmap 回调中直接写入 PTE,指向 alloc_page() 预分配的物理页。此后用户态对 ptr 的 load/store 直接命中物理页,全程不经过内核。(详见 ptemap 设备模型分析报告

关键设计决策

flowchart LR
    A[物理页何时分配?] -->|"insmod 时 (选择)"| B
    A -->|mmap 时| C

    B --> B1[✅ 访问路径零缺页]
    B --> B2[❌ 模块加载稍慢]
    B --> B3[❌ 占用物理内存]

    C --> C1[❌ 首次访问触发缺页]
    C --> C2[✅ 按需分配]

    style B fill:#c8e6c9
    style B1 fill:#a5d6a7
Loading
决策 选择 理由
物理页分配时机 insmod 时 避免 mmap 后首次访问的缺页延迟抖动
页映射方式(v1.0) vm_insert_page 内核标准 API,不用手动走 page table walk
页映射方式(v1.1) apply_to_page_range + set_pte_at 直写 PTE,零 rmap 开销,逐页独立 pgprot_t 控制
Cache 策略 WC 默认,逐页可配 WC 折中延迟与吞吐,debugfs/ioctl 可查询和设置 WB/UC/WT
Cache 属性生效时机 mmap 时一锤定音 运行时热切需 TLB shootdown + cache flush,HFT 场景不可接受
调试接口 debugfs 无 API 兼容性承诺,适合开发期快速迭代

版本状态

版本 状态 内容
v1.0 完成 模块生命周期、物理页预分配/pin、cdev + mmap (vm_insert_page)、debugfs
v1.1 完成 PTE 直写 (apply_to_page_range + set_pte_at + pfn_pte)、双路径可切换 (use_direct_pte)
v1.2 完成 逐页 cache 策略 (WC/WB/UC/WT)、debugfs cache_policy 读写接口
v1.2.1 完成 RSS 计数器修复 (_PAGE_SPECIAL bit 9)
v1.3 完成 ioctl 查询接口 (QUERY/QUERY_RANGE)、运行时 TLB flush (FLUSH_TLB/FLUSH_TLB_RANGE)
v1.3.1 完成 修复 free_reserved_page() 释放路径(替代手动 ClearPageReserved+put_page)、模块卸载 PTE 回滚 + TLB flush 安全机制

TODO (v1.4+)

  • 模块卸载安全ptemap_exit() 回滚 PTE + flush TLB,防止悬挂页表项(v1.3.1)
  • Huge page 支持 — 2MB/1GB 大页减少 TLB miss
  • NUMA 感知alloc_page_node() 按 NUMA node 分配物理页
  • insmod 全局 cache_mode 参数 — 设置默认 cache 策略,不必每次通过 debugfs
  • 多进程共享ptemap_share() 跨进程 mm 共享
  • 性能基准报告 — mmap 延迟对比、读写吞吐、TLB miss rate

License

GPL-2.0

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors