Lab 012 · 用户态拿到事件、画到屏幕
对应
document/book/08-filesystem/012-userspace-device-interfaces.md。验证档 A 档:这一章交付的是「用户态能read/dev/event0拿事件、能mmap/dev/fb0画像素」。核心机制靠源码阅读 + host 纯逻辑测罩(队列算术、字段填充、边界检查),端到端链路靠 kernel 内测 smoke(/fb_mmap_test+/input_event_test,test kernel 重挂了/dev并初始化了测试 framebuffer)+make run起真内核冒烟(真 VBE + 真鼠标键盘)。
目标
确认六件事:
/dev/event0是 InodeOps 子类:InputEventDeviceOps只 overrideread/poll_events/poll_detach_waiter,基类接口一行不动;- 双生产者逼出 Spinlock:
InputEventDevice自带RingBuffer<Event,128>+Spinlock(不复用 GUI 的 SPSC EventQueue);同一把锁兼任prepare_to_wait的 waiter-lock; /dev/fb0的 mmap 只回物理地址:FramebufferDevOps::mmap校验越界后return fb->phys_base() + offset,不建 PTE;- IoPhys VMA + page-fault 落 uncached PTE:device mmap 给 VMA 打
IoPhys+ 存phys_base,用户态首次写触发#PF,page_fault.cpp的 IoPhys 分支map_nolock + FLAG_PCD(不调 PMM);fork/execve 对FLAG_PCD跳 CoW; ioctl(FBIOGET_SCREENINFO)回 4 字段:width/height/pitch/bpp=32,copy_to_user直写 user arg(不是 staging);- boot 真挂两个节点 + test kernel 也挂:
make run见[DEVFS] mounted,ls /dev见event0/fb0;test kernel 在main_test.cpp:237也调了devfs::init()(专为让/dev/fb0解析),所以 smoke 能跑。
步骤
1. 源码确认:InodeOps 子类的 override 子集
sed -n '81,88p' kernel/drivers/input/input_event_device.hpp
sed -n '23,29p' kernel/drivers/video/fb_dev.hpp应看到 InputEventDeviceOps : public InodeOps 只 override read + poll_events + poll_detach_waiter;FramebufferDevOps : public InodeOps 只 override mmap + ioctl。两个子类都没 override write/create/mkdir——那些走基类默认。
确认基类默认:
sed -n '46,59p' kernel/fs/inode.cpp应看到 InodeOps::ioctl 和 InodeOps::mmap 默认都返 NotImplemented,注释明说 sys_* 会翻成 -ENOTTY/走 file-backed 路径。「/dev/fb0 没有 read 语义」就是基类替你拒掉,不是写了个空 read。
2. 双生产者逼出 Spinlock
sed -n '1,20p' kernel/drivers/input/input_event_device.hpp
sed -n '44,72p' kernel/drivers/input/input_event_device.hpp头注释应自陈 MPSC 设计决策:「two producers (mouse IRQ12 + keyboard via the GUI listener), so the lock-free SPSC EventQueue used by the GUI cannot be shared here」。class 体里应看到 RingBuffer<Event, kInputEventQueueSize> events_ + Spinlock lock_ + Task* read_waiters_。
确认 mouse.cpp 的 7 处 dual-write:
grep -n "push_event" kernel/drivers/mouse/mouse.cpp应看到 7 个命中(update_absolute 内 move + 3 down + 3 up),分布在 281/292/301/310/321/330/339,每处都是 g_event_queue_.enqueue(ev) 紧跟 InputEventDevice::instance().push_event(ev)。
确认 keyboard 走 listener(零侵入 keyboard.cpp):
grep -n "push_event\|on_key_event\|register_key_listener" kernel/gui/gui_init.cpp应看到 on_key_event 里 push + Keyboard::register_key_listener(on_key_event)。keyboard.cpp 本身应零 push_event 命中——driver 不沾 GUI 依赖(§14)。
3. read 路径:memcpy staging + prepare_to_wait
sed -n '52,90p' kernel/drivers/input/input_event_device.cpp三个要点逐个核:
count < sizeof(Event)返InvalidArgument(evdev 语义);prepare_to_wait(self)在irq_guard内(自标Blocked原子,lost-wakeup 闭合),schedule_blocked()在锁外(真正切走);std::memcpy(buf, &ev, sizeof(ev))写 staging——不是copy_to_user(那是坑 1 的现场)。
对照 ioctl 的 arg 是 user pointer:
sed -n '49,67p' kernel/drivers/video/fb_dev.cpp应看到 copy_to_user(reinterpret_cast<void*>(arg), &info, sizeof(info))——arg 是 syscall 透传的 user pointer,直接 copy_to_user 是对的。read 的 buf 是 kernel staging,ioctl 的 arg 是 user pointer——性质两样。
4. mmap 钩子:只回物理地址
sed -n '35,47p' kernel/drivers/video/fb_dev.cpp
sed -n '17,23p' kernel/drivers/video/framebuffer.cpp
sed -n '95,101p' kernel/drivers/video/framebuffer.hppfb_dev.cpp 的 mmap 应只做三件事:拿 system_framebuffer()、越界检查(先 offset > size 再 size - offset,顺序防溢出)、return fb->phys_base() + offset。全函数找不到建 PTE 的代码——这是 demand paging,mmap 是登记意图。
phys_base_ 是 Framebuffer::init 时存的 VBE PhysBasePtr(013 讲过大页映射,本章只消费这个字段)。
5. sys_mmap + page-fault:IoPhys 分支
sed -n '131,143p' kernel/syscall/sys_mmap.cpp
sed -n '181,183p' kernel/syscall/sys_mmap.cpp
sed -n '191,196p' kernel/syscall/sys_mmap.cpp
sed -n '63,68p' kernel/mm/vma.hpp
sed -n '256,283p' kernel/arch/x86_64/page_fault.cppsys_mmap 应看到 device probe(inode->ops->mmap 调钩子,NotImplemented 落 file-backed 路径,在 131-143)+ VMA 打 IoPhys 标志(181-183)+ 存 vma->phys_base(191-196,不持 inode ref)。IoPhys = 1 << 7 定义在 vma.hpp。
page_fault.cpp 的 IoPhys 分支应:io_phys = phys_base + (virt_page - vma->start) 算物理地址,flags 带 FLAG_PCD(uncached)+ FLAG_USER + FLAG_WRITABLE + FLAG_NX,map_nolock 落 PTE。不调 PMM、不进 page cache、不 pte_count_inc——设备内存不归 PMM 管。
6. fork/execve 跳 CoW(v1.0.0 已落地)
sed -n '89,94p' kernel/proc/fork.cpp
sed -n '122,123p' kernel/proc/execve.cppfork 应看到 if (entry_flags & FLAG_PCD) { dst_table[i].raw = src_table[i].raw; continue; }——IoPhys 页直接共享原物理映射,跳 CoW、跳 pte_count。execve 同款跳过。注释明说「writing the framebuffer must hit the real fb, not a private RAM copy」。
7. ioctl FBIOGET_SCREENINFO
sed -n '23,37p' kernel/drivers/video/fb_dev.hpp
sed -n '27,32p' kernel/drivers/video/fb_dev.cpp
sed -n '49,67p' kernel/drivers/video/fb_dev.cppkFbioGetScreenInfo = 0x4600 是 Cinux 自定义常量(数值碰巧和 Linux FBIOGET_VSCREENINFO 一致,struct 布局不同)。FbScreenInfo 只有 4 字段(width/height/pitch/bpp),bpp 写死 32(VBE mode 0x144)。非该 request 返 NotImplemented → -ENOTTY。
8. DevFS 注册(test kernel 也挂)
sed -n '161,172p' kernel/fs/devfs/devfs_init.cpp
sed -n '319,330p' kernel/fs/devfs/devfs.cpp
sed -n '237,238p' kernel/test/main_test.cpp
sed -n '179,186p' kernel/test/main_test.cppdevfs_init.cpp 应看到 add_node("ptmx") + add_node("fb0")(164)+ add_node("event0")(167)+ block loop,跟 064 的 null/zero/console 同一个 add_node 机制;块设备走 add_block_node(085 讲过);子目录 /dev/pts/<N> 走 set_dynamic_lookup(084 讲过,本章 fb0/event0 是 flat 节点不走那条)。
main_test.cpp 应看到 test kernel 也调 cinux::fs::devfs::init()(注释明说「re-mount /dev (vfs_mount_init cleared it) so /dev/fb0 resolves」),并在 musl_hello_smoke_entry 里 init 了测试 framebuffer——所以 /dev/event0、/dev/fb0 在 test kernel 里都解析得到,smoke 才跑得起来。别照搬 064 那条「test kernel 不挂 /dev」的判据——本章不适用。
9. boot 真挂
sed -n '182,186p' kernel/main.cpp应看到 Framebuffer fb; fb.init(*boot_info); set_system_framebuffer(&fb);——/dev/fb0 mmap 靠这个 system singleton 解析物理地址。
make run起 QEMU,boot 序列应打 [BIG] Framebuffer initialised: <W>x<H> <bpp>bpp + [DEVFS] mounted at /dev (<N> nodes)。ls /dev 应见 event0/fb0。
10. kernel 内测 smoke(test kernel 真跑)
sed -n '300,345p' kernel/test/main_test.cpp
sed -n '352,414p' kernel/test/main_test.cpp应看到两条 smoke:
/fb_mmap_test(300-345):fork+execve 用户态 demo,demoopen("/dev/fb0")+ioctl(FBIOGET_SCREENINFO)+mmap+ 写读一个像素,验 ioctl + mmap + page-fault IoPhys 全链路。/input_event_test(352-414):每轮 mock pushMouseMove(123,45)+KeyDown('A'),fork+execve demo,demoopen("/dev/event0")+read2 事件,验 type + payload。
dev note 记录两条 smoke 在两腿(单核 + -smp 2)各 5/5 iters PASS。
11. 进阶:写个最小用户态 GUI demo(A 档动手)
骨架,不给答案:
展开代码 (共 36 行)收起代码
// 用户态镜像 Event 布局(字段顺序+类型严格 mirror,padding 自动一致别上 packed)
typedef struct {
int32_t x, y, dx, dy;
uint8_t buttons;
bool left, right, middle;
} MouseEvent;
typedef struct {
char ascii; uint8_t scancode;
bool pressed, shift, ctrl, alt;
} KeyEvent;
typedef struct {
uint8_t type_; // EventType
union { MouseEvent mouse; KeyEvent key; };
} Event; // static_assert(sizeof(Event) == 24)
typedef struct { uint32_t width, height, pitch, bpp; } FbScreenInfo;
#define FBIOGET_SCREENINFO 0x4600 // 必须用 Cinux 这个值,struct 跟 Linux vscreeninfo 不同
int main() {
// 1. ioctl 拿屏几何
int ffd = open("/dev/fb0", O_RDWR);
FbScreenInfo info;
ioctl(ffd, FBIOGET_SCREENINFO, &info);
// 2. mmap 拿到帧缓冲虚拟地址
uint8_t* fb = mmap(NULL, info.pitch * info.height, PROT_READ|PROT_WRITE,
MAP_SHARED, ffd, 0);
// 3. 画一个红矩形到 fb(0xFFFF0000 = XRGB 红)
// 4. open /dev/event0 + read 拿鼠标 move 事件
int efd = open("/dev/event0", O_RDONLY);
Event ev;
while (read(efd, &ev, sizeof(ev)) == sizeof(ev)) {
if (ev.type_ == /* MouseMove */) {
// 擦旧矩形、按 ev.mouse.x/y 画新矩形
}
}
}验证判据:屏幕真出红矩形 + 矩形跟着鼠标动 = mmap 绑 VMA + read 事件链路全通。
踩坑方向(不给答案,给方向):
- Event 字段顺序/类型:用户态 struct 必须 mirror
cinux::gui::Event的字段顺序和类型(MouseEvent:x/y/dx/dy/buttons/left/right/middle;KeyEvent:ascii/scancode/pressed/shift/ctrl/alt)。用static_assert(sizeof(Event) == 24)+static_assert(offsetof(Event, mouse) == 4)比对。别上__attribute__((packed))——会取消 padding 让用户态 sizeof 变 21,跟内核 24 不一致。 - mmap 首次写才建 PTE:
mmap返回的虚拟地址,第一次写像素才触发#PF落 PTE(demand paging)。别在 mmap 钩子里找建表逻辑。 - fork 后子进程写 fb 没反应(进阶):IoPhys VMA 的 PTE 在 fork 时已经跳 CoW(fork.cpp:89-94),所以子进程应该能写屏。如果你跑出子进程写屏没反应,先核 fork 路径的
FLAG_PCD跳 CoW 是否真生效(本 tag 源码已落地),再核 mmap 的 VMA 是否真打了IoPhys标志(sys_mmap.cpp:181-183)。 kFbioGetScreenInfo必须是0x4600:用户态 mirror 这个常量,别照搬 LinuxFBIOGET_VSCREENINFO的宏(数值碰巧一致,但 struct 布局不同)。
验收清单
- [ ]
input_event_device.hpp:81-88InputEventDevOps只 override read/poll;fb_dev.hpp:23-29FramebufferDevOps只 override mmap/ioctl;inode.cpp:46-59默认NotImplemented。 - [ ]
input_event_device.hpp:44-72RingBuffer<Event,128>+Spinlock+read_waiters_;头注释自陈 MPSC 决策。 - [ ]
mouse.cpp7 处 dual-write(281/292/301/310/321/330/339);gui_init.cpplistener 双写;keyboard.cpp零push_event。 - [ ]
input_event_device.cpp:52-90read 三要点:count < sizeofEINVAL +prepare_to_wait锁内 +memcpystaging(非 copy_to_user)。 - [ ]
fb_dev.cpp:35-47mmap 只回phys_base()+offset,不建 PTE;越界检查顺序对(先 offset 后 size-offset)。 - [ ]
sys_mmap.cpp:181-183VMA 打IoPhys;sys_mmap.cpp:191-196存vma->phys_base;page_fault.cpp:256-283IoPhys 分支map_nolock + FLAG_PCD,不调 PMM。 - [ ]
fork.cpp:89-94/execve.cpp:122-123对FLAG_PCD跳 CoW(v1.0.0 已落地)。 - [ ]
fb_dev.cpp:49-67ioctl FBIOGET_SCREENINFO 回 4 字段,copy_to_user直写 arg;kFbioGetScreenInfo=0x4600。 - [ ]
devfs_init.cpp:164,167add_node 注册 fb0/event0;main_test.cpp:237test kernel 也调devfs::init();make run见[DEVFS] mounted,ls /dev见两节点。 - [ ]
main_test.cpp:300-345/fb_mmap_testsmoke +main_test.cpp:352-414/input_event_testsmoke。 - [ ] A 档 demo:屏幕出红矩形 + 跟着鼠标动。
别做这些
- 别照搬 064 那条「test kernel 不挂 /dev」判据——本章不适用。test kernel 在
main_test.cpp:237显式调了devfs::init()(注释明说为让/dev/fb0解析),并在musl_hello_smoke_entry里 init 了测试 framebuffer。boot 与 test kernel 的真实差异在「真 VBE vs 测试 framebuffer」,不在「挂不挂 /dev」。要验真 VBE + 真鼠标键盘,还是make run。 - 别用
copy_to_user写 read 的buf——它是 kernel staging(sys_read提供),is_user_vaddr会拒 kernel 地址,返-EFAULT。用std::memcpy。对比/dev/fb0的ioctlarg 是真 user pointer,直接copy_to_user是对的。 - 别以为
poll路径在 QEMU 实跑过——dev note 明说「poll path 未跑机制测(smoke 只 read);用户态 host 用 poll 时再验」。设计到位,实跑待 ring3 host 用 poll 时盖。 - 别照搬 Linux
fb_var_screeninfo的 struct 布局——Cinux 的FbScreenInfo只有 4 字段(width/height/pitch/bpp),数值常量0x4600碰巧一致但布局完全不同。用户态必须 mirror Cinux 的 4 字段。 - 别以为
Event跟 Linuxinput_event一样——它是 Cinux 自己的cinux::gui::Event(EventType1 字节 + union),用户态要 mirror 字段顺序和类型。别上__attribute__((packed))——padding 两边 ABI 自动一致,packed 反而会让 sizeof 变 21 跟内核 24 不一致。 - 别在 mmap 钩子里找建 PTE 逻辑——那在
page_fault.cpp的 IoPhys 分支。mmap 钩子只回物理地址,真正建表是用户态首次写触发#PF时做的。 - 别给 IoPhys VMA 走 CoW——设备内存不归 PMM 管,fork 时
FLAG_PCDleaf 已跳 CoW(fork.cpp:89-94,v1.0.0 已落地)。否则父进程写屏被截胡到 RAM 副本,屏幕不更新。 - 别用 64-bit MMIO 写——QEMU 丢 64-bit 写,helper 名字带
32就是锁死访问宽度。64-bit 计数器拆两次 32-bit 半读 + rollover 保护。 - 别把 HPET Config 寄存器当
0x008——它在0x010(通用寄存器间距0x10)。0x008是 reserved 区,写全丢。QEMUhw/timer/hpet.c的HPET_CFG宏是权威。诊断时记一条不对称:对0x000的读是对的(能拿 100MHz period),只有对0x008的写丢——「读对写错」指向偏移错。