Skip to content

/dev/fb0

/dev/fb0:framebuffer mmap + IoPhys uncached + ioctl

设备形态分野:内存式 vs 流式

先点一个对照:/dev/event0 是「流式设备」(靠 read/poll,事件一条条流过去),/dev/fb0 是「内存式设备」(靠 mmap,把显存整个窗口暴露给用户态直接读写)。所以 FramebufferDevOps 只 override mmap + ioctl——read/writeInodeOps 默认 NotImplemented,基类替你拒掉(下一节展开)。看 fb_dev.hpp:

cpp
class FramebufferDevOps : public cinux::fs::InodeOps {
public:
    cinux::lib::ErrorOr<uint64_t> mmap(const cinux::fs::Inode* inode, uint64_t offset,
                                       uint64_t length) override;
    cinux::lib::ErrorOr<int64_t>  ioctl(const cinux::fs::Inode* inode, uint32_t request,
                                        uint64_t arg) override;
};

read/write/create/mkdir 一个都没 override。这是 VFS 多态的标准玩法:设备多样性 = override 子集不同。

mmap 钩子:只回物理地址,不建 PTE

mmap 全文见 fb_dev.cpp:

cpp
cinux::lib::ErrorOr<uint64_t> FramebufferDevOps::mmap(const cinux::fs::Inode*, uint64_t offset,
                                                      uint64_t length) {
    Framebuffer* fb = system_framebuffer();
    if (fb == nullptr) {
        return cinux::lib::Error::NotImplemented;  // no framebuffer initialised
    }
    if (offset > fb->size() || length > fb->size() - offset) {
        return cinux::lib::Error::InvalidArgument;
    }
    return fb->phys_base() + offset;
}

三件事:

  1. system_framebuffer() 拿单例,没有就 NotImplemented(没初始化显存就别让 mmap)。
  2. 越界检查防溢出:先比 offset > fb->size(),再用 fb->size() - offsetlength——顺序不能反,否则 fb->size() - offset 下溢成天文数字,length > 天文数字 永远 false,检查形同虚设。这是「先比再减」的无符号算术铁律。
  3. return fb->phys_base() + offset——这是全章的 punchline 之一:mmap 钩子只返物理地址,不建 PTE

phys_base()Framebuffer 暴露的 VBE PhysBasePtr,见 framebuffer.hpp:uint64_t phys_base() const { return phys_base_; }。这个字段是 Framebuffer::init 时存的,见 framebuffer.cpp:

cpp
void Framebuffer::init(const BootInfo& bi) {
    uint64_t fb_phys = bi.fb_addr;
    phys_base_       = fb_phys;  // F-GUI-USERSPACE b1: expose to /dev/fb0 mmap
    // ...
}

bootloader 给的 VBE 物理地址,init 存进 phys_base_,本章消费这个字段。013 讲过 init 里那个 2MB 大页映射进 KMEM_FB_BASE,这里不重讲页表——只点「phys_base_ 是 013 留的字段,本章消费」。

sys_mmap:登记意图(IoPhys VMA)

真正建映射不是 mmap 钩子的活,是 sys_mmap + page-fault 的活。看 sys_mmap.cpp 的 device probe:

cpp
bool     device_mmap = false;
uint64_t device_phys = 0;
if (backing_inode != nullptr && backing_inode->ops != nullptr) {
    auto mp = backing_inode->ops->mmap(backing_inode, offset, aligned_len);
    if (mp.ok()) {
        device_mmap = true;
        device_phys = mp.value();
    } else if (mp.error() != cinux::lib::Error::NotImplemented) {
        return -to_errno(mp.error());
    }
}

NotImplemented 当成「这不是设备 mmap」的信号——普通文件/匿名映射走下面那条 page-cache 路径。命中了就把物理地址存下来,给 VMA 打 IoPhys 标志,见 sys_mmap.cpp:

cpp
if (device_mmap) {
    vflags |= cinux::mm::VmaFlags::IoPhys;
}

然后在 VMA 落定之后,把物理地址写进 vma->phys_base,见 sys_mmap.cpp:

cpp
if (device_mmap) {
    // Bind the VMA to device memory.  No inode ref: device memory is
    // not page cache and the VMA is not file-backed in the PageCache
    // sense; phys_base is the per-page physical source for the fault
    // handler.  backing/file_offset stay null/0.
    v->phys_base = device_phys;
}

注意此刻用户态那段虚拟地址还没 PTE——demand paging。IoPhys 标志的定义见 vma.hpp:

cpp
/// F-GUI-USERSPACE batch 1: device / I-O physical mapping (e.g. framebuffer
/// mmap).  The VMA is pre-bound to a fixed physical range (phys_base); the
/// page-fault handler maps those pages verbatim with FLAG_PCD (uncached)
/// instead of allocating from the PMM.  Teardown (munmap) and fork never
/// free or CoW them -- device memory is not PMM-managed.
IoPhys    = 1 << 7,

IoPhys 这个标志就是「这不是 RAM 页,别动」的唯一标识——下面的 page-fault handler 看到它就跳过 PMM,直接把物理页原样映射进去;munmap/fork 看到它也跳过 free/CoW(设备内存不归 PMM 管)。

page-fault:IoPhys 分支落 uncached PTE

用户态拿到 mmap 返回的虚拟地址,首次写像素时触发 #PF——这时候才真正建 PTE。page-fault handler 的 IoPhys 分支见 page_fault.cpp:

cpp
} else if (vma != nullptr &&
           cinux::mm::has_flag(vma->flags, cinux::mm::VmaFlags::IoPhys)) {
    const uint64_t io_phys = vma->phys_base + (virt_page - vma->start);
    uint64_t       ioflags =
        cinux::arch::FLAG_PRESENT | cinux::arch::FLAG_USER | cinux::arch::FLAG_PCD;
    if (cinux::mm::has_flag(vma->flags, cinux::mm::VmaFlags::Write)) {
        ioflags |= cinux::arch::FLAG_WRITABLE;
    }
    if (!cinux::mm::has_flag(vma->flags, cinux::mm::VmaFlags::Exec)) {
        ioflags |= cinux::arch::FLAG_NX;
    }
    uint64_t cur_cr3 = cinux::arch::read_cr3();
    if (g_vmm.map_nolock(virt_page, io_phys, ioflags, &cur_cr3)) {
        return;
    }
    return;
}

四个要点:

  1. 物理地址算出来:io_phys = phys_base + (virt_page - vma->start),VMA 起始偏移 + fault 页在 VMA 内的偏移。
  2. flags 带 FLAG_PCD(uncached)——这是 MMIO 铁律的「内存映射版本」,跟 LocalAPIC/HPET 寄存器同一类:显存是设备内存不是 RAM,CPU cache 它会让 GUI 写像素只动 cache line 没刷到显存,屏幕不更新。
  3. FLAG_USER + FLAG_WRITABLE + FLAG_NX——ring3 可写、不可执行(显存不需要执行权限,W^X)。
  4. 不调 PMM、不进 page cache、不 pte_count_inc——设备内存不是 PMM 管的,分配/释放都绕开。map_nolock 直接落 PTE(用当前 CR3,因为 fault 时已经持有 address space 锁,map_nolock 是无锁版本)。

读代码别在 mmap 钩子里找建表逻辑——要去 page_fault.cpp 找。这是 demand paging 应用到设备内存: mmap 是登记意图, fault 才兑现。

ioctl:FBIOGET_SCREENINFO 拿屏几何

光有 mmap 不够——用户态 GUI host 还得知道屏幕多大、一行多少字节,才能 size 自己的 canvas。这就是 ioctl(FBIOGET_SCREENINFO) 的活,见 fb_dev.cpp:

cpp
cinux::lib::ErrorOr<int64_t> FramebufferDevOps::ioctl(const cinux::fs::Inode*, uint32_t request,
                                                      uint64_t arg) {
    if (request != kFbioGetScreenInfo) {
        return cinux::lib::Error::NotImplemented;  // sys_ioctl maps this to -ENOTTY
    }
    Framebuffer* fb = system_framebuffer();
    if (fb == nullptr) {
        return cinux::lib::Error::NotImplemented;
    }
    FbScreenInfo info{};
    info.width  = fb->width();
    info.height = fb->height();
    info.pitch  = fb->pitch();
    info.bpp    = 32;  // VBE mode 0x144 is 32-bpp XRGB; Framebuffer assumes it
    if (!cinux::user::copy_to_user(reinterpret_cast<void*>(arg), &info, sizeof(info))) {
        return cinux::lib::Error::Fault;
    }
    return 0;
}

回填结构是简化的 4 字段,见 fb_dev.cpp:

cpp
struct FbScreenInfo {
    uint32_t width;
    uint32_t height;
    uint32_t pitch;  // bytes per scan line
    uint32_t bpp;    // bits per pixel
};

三件事得说清:

  1. 故意不照搬 Linux fb_var_screeninfo——那个 ~160 字节几十个字段(xoffsetyoffsetred.lengthgreen.offsettransppixclockhsync_len…),Cinux 不建模这一堆。只暴露 GUI host 真正消费的 4 个字段:width、height、pitch、bpp。够用就好。
  2. bpp 写死 32——VBE mode 0x144 是 32-bpp XRGB(8 bit × 4 通道),Framebuffer 全假设这个格式,不暴露给用户改。
  3. kFbioGetScreenInfo = 0x4600 是 Cinux 自定义常量,见 fb_dev.hpp。注释明说「Mirrors Linux FBIOGET_VSCREENINFO in role, not in struct layout」——数值碰巧和 Linux FBIOGET_VSCREENINFO 一致,但 struct 布局完全不同,用户态别照搬 Linux 用户态结构体,得 mirror 这 4 个字段。

注意 ioctl 的 arg 是 user pointer(syscall 层透传,不像 read 的 staging),所以这里直接 copy_to_user(arg, &info, sizeof(info)),失败返 Fault。这跟 read 路径的 staging buf 性质两样——下一节调试现场会对比。

非该 request 一律 NotImplemented,被 sys_ioctl 翻译成 -ENOTTY(Linux 对「这个 inode 不处理这个 ioctl」的约定)。

035_multi_terminal-45-gf25de18 · f25de18 · 2026-08-04