/dev/fb0
/dev/fb0:framebuffer mmap + IoPhys uncached + ioctl
设备形态分野:内存式 vs 流式
先点一个对照:/dev/event0 是「流式设备」(靠 read/poll,事件一条条流过去),/dev/fb0 是「内存式设备」(靠 mmap,把显存整个窗口暴露给用户态直接读写)。所以 FramebufferDevOps 只 override mmap + ioctl——read/write 走 InodeOps 默认 NotImplemented,基类替你拒掉(下一节展开)。看 fb_dev.hpp:
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:
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;
}三件事:
system_framebuffer()拿单例,没有就NotImplemented(没初始化显存就别让 mmap)。- 越界检查防溢出:先比
offset > fb->size(),再用fb->size() - offset比length——顺序不能反,否则fb->size() - offset下溢成天文数字,length > 天文数字永远 false,检查形同虚设。这是「先比再减」的无符号算术铁律。 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:
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:
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:
if (device_mmap) {
vflags |= cinux::mm::VmaFlags::IoPhys;
}然后在 VMA 落定之后,把物理地址写进 vma->phys_base,见 sys_mmap.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:
/// 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:
} 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;
}四个要点:
- 物理地址算出来:
io_phys = phys_base + (virt_page - vma->start),VMA 起始偏移 + fault 页在 VMA 内的偏移。 - flags 带
FLAG_PCD(uncached)——这是 MMIO 铁律的「内存映射版本」,跟 LocalAPIC/HPET 寄存器同一类:显存是设备内存不是 RAM,CPU cache 它会让 GUI 写像素只动 cache line 没刷到显存,屏幕不更新。 FLAG_USER+FLAG_WRITABLE+FLAG_NX——ring3 可写、不可执行(显存不需要执行权限,W^X)。- 不调 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:
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:
struct FbScreenInfo {
uint32_t width;
uint32_t height;
uint32_t pitch; // bytes per scan line
uint32_t bpp; // bits per pixel
};三件事得说清:
- 故意不照搬 Linux
fb_var_screeninfo——那个 ~160 字节几十个字段(xoffset、yoffset、red.length、green.offset、transp、pixclock、hsync_len…),Cinux 不建模这一堆。只暴露 GUI host 真正消费的 4 个字段:width、height、pitch、bpp。够用就好。 bpp写死 32——VBE mode0x144是 32-bpp XRGB(8 bit × 4 通道),Framebuffer全假设这个格式,不暴露给用户改。kFbioGetScreenInfo = 0x4600是 Cinux 自定义常量,见 fb_dev.hpp。注释明说「Mirrors Linux FBIOGET_VSCREENINFO in role, not in struct layout」——数值碰巧和 LinuxFBIOGET_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」的约定)。