Files
TermiNexus f4c653958e feat(devfs): S1 —— 暴露 /dev/fb0 并让用户态可达 /dev/*(含 ioctl)
devfs:新增 /dev/fb0(framebuffer_device),提供 ioctl(FB_GET_INFO) 回填几何/格式/物理区间,并新增 devfs_file.mmap_info 虚方法(默认不可映射)把物理区间交给内核 mmap 用;main.vp 在 devfs_init 后登记它。输入设备(/dev/input/keyboard、/dev/input/mouse)此前已存在,本轮起从用户态可达。

用户态可达性:Process 增 fd_kind[](0=FAT32 fileobj*,1=devfs_fd+1),OPEN 对 "/dev/" 前缀改走 devfs,READ/WRITE/CLOSE 按 kind 分派;Process.exit 只对 kind=0 调 fat32.close(把 devfs fd 当 fileobj* 会跑飞)。fd 种类常量放在 process.vp 供 syscall.vp 引用,避免 process 反向 import devfs(devfs→keyboard/mouse→intr.idt→sched→process 会成环)。

新增 IOCTL syscall(Linux 号 16,本轮开始实现):只放给 devfs 设备,SDK 侧 vpsdk/fs.vp 加 ioctl() 助手。
2026-10-16 18:38:21 +08:00

448 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import mm.mm as mm
import paging.paging as paging
import drivers.serial.uart.serial as serial
import drivers.fs.fat32.fat32 as fat32
import drivers.fs.fat32.fat32_types as fat32_types
import drivers.core.cpu.cpu as cpu
import intr.gdt as gdt
import string
define PROC_READY = 0
define PROC_RUNNING = 1
define PROC_BLOCKED = 2
define PROC_DONE = 3
define MAX_PROCESSES = 8
define MAX_PROC_THREADS = 8
define PROC_MAX_FDS = 16
# fd 种类(process.fd_kind[])。定义放这里、由 intr/syscall.vp 以 process.FD_KIND_* 引用,
# 这样 process.vp 不必 import devfs —— devfs → keyboard/mouse → intr.idt → sched → process
# 会形成环,不能让 process 反向依赖 devfs。
define FD_KIND_FAT32 = 0
define FD_KIND_DEVFS = 1
define PROC_MAX_DIRS = 8
define KERN_STACK_SIZE = 8192
define USER_STACK_SIZE = 65536
# 用户栈虚拟区:主线程栈顶 = USER_STACK_VTOP(见 execrunner/elf.vp 的映射),
# 额外用户线程(THREAD_CREATE)从它下方按 USER_THREAD_STACK_GAP 逐个下移。
define USER_STACK_VTOP = 0x7FFFFFE00000
define USER_STACK_PAGES = 16
define USER_THREAD_STACK_GAP = 0x20000
define MAX_USER_THREADS = 16
class Process:
pid: int
pml4_root: uint64
threads: [8]int
thread_count: int
state: int
entry: rawptr
elf_base: uint64
heap_start: uint64
heap_size: uint64
name: [32]char
fd_table: [PROC_MAX_FDS]rawptr
fd_used: [PROC_MAX_FDS]uint8
# 0 = FAT32 文件(fd_table[i] 里是 fileobj*);1 = devfs 设备(fd_table[i] 里是
# devfs_fd+1,+1 只为避开"devfs fd 0"与空指针的歧义)。READ/WRITE/CLOSE/IOCTL 与
# Process.exit 都必须按它分派 —— 把 devfs fd 当 fileobj* 解引用会直接跑飞。
fd_kind: [PROC_MAX_FDS]uint8
dir_table: [PROC_MAX_DIRS]rawptr
dir_used: [PROC_MAX_DIRS]uint8
dir_pool: [PROC_MAX_DIRS]fat32_types.dirobj
kern_stack: uint64
user_stack: uint64
def addThread(self, tid: int) -> int:
if self.thread_count >= MAX_PROC_THREADS: return -1
self.threads[self.thread_count] = tid
self.thread_count += 1
return 0
def exit(self):
self.state = PROC_DONE
for i in range(PROC_MAX_FDS):
if self.fd_used[i]:
fp: rawptr = self.fd_table[i]
# devfs 设备 fd 绝不能当 fileobj* 交给 fat32.close —— 那会把 devfs_fd+1
# 当指针解引用而跑飞。devfs 的 fd 表是独立的另一张;正规退出路径本来就不
# 回收 fd(见 intr/syscall.vp 的 EXIT 不调 Process.exit),这里只清槽位,
# app 自己调 CLOSE 时才走 devfs_close。
if fp and self.fd_kind[i] == FD_KIND_FAT32:
fat32.close(fp)
self.fd_used[i] = 0
self.fd_table[i] = None
self.fd_kind[i] = 0
for i in range(PROC_MAX_DIRS):
if self.dir_used[i]:
dp: rawptr = self.dir_table[i]
if dp:
fat32.closedir(dp)
self.dir_used[i] = 0
self.dir_table[i] = None
m: ProcessManager* = _proc_mgr_ptr
if m.current_pid == self.pid:
m.current_pid = 0
m.processes[0].state = PROC_RUNNING
if self.pml4_root != 0:
kernel_cr3: uint64 = m.processes[0].pml4_root
if kernel_cr3 != 0:
__asm__(f"mov cr3, {asminp(kernel_cr3, ASM_DESCR.REG_ANY)}",
op=[ASM_DESCR.CLOBBER_MEMORY])
paging.set_active_pml4(kernel_cr3)
@property
def is_running(self) -> bool: return self.state == PROC_RUNNING
@property
def is_done(self) -> bool: return self.state == PROC_DONE
class ProcessManager:
processes: [MAX_PROCESSES]Process
current_pid: int
count: int
def __init__(self):
global _proc_mgr_ptr
_proc_mgr_ptr = &_proc_mgr
m: ProcessManager* = _proc_mgr_ptr
m.current_pid = 0
m.count = 1
p: Process* = &m.processes[0]
p.pid = 0
p.state = PROC_RUNNING
p.thread_count = 0
p.pml4_root = 0
p.entry = None
p.elf_base = 0
p.heap_start = 0
p.heap_size = 0
name_str: str = "kernel"
i: int = 0
for ch in name_str:
if i >= 31: break
p.name[i] = ch
i += 1
p.name[i] = 0
j: int
for j in range(MAX_PROC_THREADS):
p.threads[j] = -1
for j in range(PROC_MAX_FDS):
p.fd_used[j] = 0
p.fd_table[j] = None
p.fd_kind[j] = 0
for j in range(PROC_MAX_DIRS):
p.dir_used[j] = 0
p.dir_table[j] = None
__asm__(f"""mov {asmout(p.pml4_root, ASM_DESCR.OUTPUT_REG)}, cr3""",
op=[ASM_DESCR.CLOBBER_MEMORY])
@staticmethod
def _switch(pid: int) -> int:
global _saved_cr3
m: ProcessManager* = _proc_mgr_ptr
if pid < 0 or pid >= m.count: return -1
p: Process* = &m.processes[pid]
if p.state == PROC_DONE: return -1
old_pid: int = m.current_pid
if old_pid == pid: return 0
if old_pid >= 0 and old_pid < m.count:
m.processes[old_pid].state = PROC_READY
p.state = PROC_RUNNING
m.current_pid = pid
__asm__(f"""mov {asmout(_saved_cr3, ASM_DESCR.OUTPUT_REG)}, cr3
mov rax, {asminp(p.pml4_root, ASM_DESCR.REG_ANY)}
mov cr3, rax""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX])
return 0
@staticmethod
def current() -> Process*:
m: ProcessManager* = _proc_mgr_ptr
return &m.processes[m.current_pid]
@staticmethod
def get_process(pid: int) -> Process*:
m: ProcessManager* = _proc_mgr_ptr
if pid < 0 or pid >= m.count: return None
return &m.processes[pid]
@staticmethod
def create_process(name: str, entry: rawptr) -> Process*:
m: ProcessManager* = _proc_mgr_ptr
if m.count >= MAX_PROCESSES: return None
pid: int = m.count
m.count += 1
p: Process* = &m.processes[pid]
p.pid = pid
p.state = PROC_READY
p.entry = entry
p.elf_base = 0
p.thread_count = 0
p.heap_start = 0
p.heap_size = 0
string.memset(&p.name, 0, 32)
i: int = 0
for ch in name:
if i >= 31: break
p.name[i] = ch
i += 1
p.name[i] = 0
for j in range(MAX_PROC_THREADS):
p.threads[j] = -1
for j in range(PROC_MAX_FDS):
p.fd_used[j] = 0
p.fd_table[j] = None
p.fd_kind[j] = 0
for j in range(PROC_MAX_DIRS):
p.dir_used[j] = 0
p.dir_table[j] = None
ks: rawptr = mm.malloc(KERN_STACK_SIZE)
if ks is None:
m.count = m.count - 1
return None
ks_size: uint64 = KERN_STACK_SIZE
p.kern_stack = uint64(ks) + ks_size
us: rawptr = mm.malloc(USER_STACK_SIZE)
if us is None:
serial.puts("[process] user stack alloc failed\n")
mm.free(ks)
m.count = m.count - 1
return None
us_size: uint64 = USER_STACK_SIZE
p.user_stack = uint64(us) + us_size
__asm__(f"""mov {asmout(p.pml4_root, ASM_DESCR.OUTPUT_REG)}, cr3""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX])
return p
_proc_mgr: ProcessManager = ProcessManager()
_proc_mgr_ptr: ProcessManager* = &_proc_mgr
export _saved_cr3: uint64
def current() -> int:
m: ProcessManager* = _proc_mgr_ptr
return m.current_pid
def switch_pid(new_pid: int):
m: ProcessManager* = _proc_mgr_ptr
if m.current_pid == new_pid: return
if m.current_pid >= 0 and m.current_pid < m.count:
old_p: Process* = &m.processes[m.current_pid]
if old_p.state != PROC_DONE:
old_p.state = PROC_READY
if new_pid >= 0 and new_pid < m.count:
new_p: Process* = &m.processes[new_pid]
new_p.state = PROC_RUNNING
new_cr3: uint64 = new_p.pml4_root
if new_cr3 != 0:
__asm__(f"mov cr3, {asminp(new_cr3, ASM_DESCR.REG_ANY)}",
op=[ASM_DESCR.CLOBBER_MEMORY])
paging.set_active_pml4(new_cr3)
# Update TSS RSP0 and GS kernel_rsp0 for the new process
# This is needed so interrupts from Ring 3 use the correct kernel stack
if new_p.kern_stack != 0:
gdt.set_tss_rsp0(new_p.kern_stack)
cpu.set_kernel_rsp0(new_p.kern_stack)
m.current_pid = new_pid
def get_pid(tid: int) -> int:
m: ProcessManager* = _proc_mgr_ptr
for i in range(m.count):
p: Process* = &m.processes[i]
for j in range(p.thread_count):
if p.threads[j] == tid:
return p.pid
return -1
def drop_to_user_mode(entry: rawptr, user_rsp: uint64, kern_rsp: uint64):
# NOTE: Do NOT swapgs here. This OS uses reverse GS convention:
# GS_BASE = 0 (kernel & user), KERNEL_GS_BASE = &_per_cpu
# syscall/ISR entry does swapgs to get GS_BASE = &_per_cpu
# syscall/ISR return does swapgs to restore GS_BASE = 0
# GS state is restored in isrCommonhandler before _yield() if needed.
# [FIX] Normalize GS to the user convention (GS_BASE = 0) before returning to ring 3.
# This thread may have been switched in from ANOTHER thread's user-mode timer interrupt:
# that path never runs the ISR exit swapgs, so GS_BASE is still the kernel view (pointing
# at the per-cpu struct). Running user code in that state makes the app's first syscall
# swapgs the wrong way; the kernel-stack lookup then reads garbage from linear address 8,
# the stack pointer becomes garbage, and even the page-fault frame cannot be pushed
# => triple fault with zero output (not even the ISR entry probe can fire).
# Swapgs only when GS_BASE != 0 (idempotent; same idiom as the handler's GS restore).
__asm__(f"""cli
mov rsp, {asminp(kern_rsp, ASM_DESCR.REG_ANY)}
mov ecx, 0xC0000101
rdmsr
shl rdx, 32
or rax, rdx
test rax, rax
jz 1f
swapgs
1:
mov rdx, {asminp(user_rsp, ASM_DESCR.REG_ANY)}
mov rcx, {asminp(entry, ASM_DESCR.REG_ANY)}
mov r11, 0x202
push 0x23
push rdx
push r11
push 0x2B
push rcx
iretq""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX,
ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX,
ASM_DESCR.CLOBBER_R11, ASM_DESCR.CLOBBER_RSI,
ASM_DESCR.CLOBBER_RDI, ASM_DESCR.CLOBBER_R8,
ASM_DESCR.CLOBBER_R9, ASM_DESCR.CLOBBER_R10,
ASM_DESCR.CLOBBER_RBP])
# ============================================================
# 用户态线程(THREAD_CREATE)上下文
# 每个用户线程需要两套栈,否则同进程多线程会互相踩:
# · 用户栈(Ring 3 执行用):从 USER_STACK_VTOP 下方按固定间隔分配,映射 PTE_USER;
# · 内核栈(Ring 3 → Ring 0 的 syscall/中断落栈点 = TSS.RSP0 / gs:[8]):
# 必须按**线程**写入(由调度器切线程时设置),不能按进程 —— 否则一个线程在
# syscall 里 yield 时,另一个线程的 syscall 会压掉它的栈帧,返回即跑飞。
# ============================================================
class UserThreadCtx:
pid: int
tid: int
entry: rawptr
arg: rawptr
# 入口 rsp(= 栈顶下一格;该格里放出口跳板的 VA,见 alloc_user_thread)
user_stack_entry_rsp: uint64
# 用户栈映射上界(UnMap 用)
user_stack_vtop: uint64
user_stack_phys: rawptr
kern_stack: uint64
# 出口跳板物理页(映射在线程栈顶上方那一页,回收时一起交还)
tramp: rawptr
_user_ctx: [MAX_USER_THREADS]UserThreadCtx
_user_ctx_used: [MAX_USER_THREADS]int
_user_stack_next: int = 0
# 用户线程"自然 return"的出口跳板:一页只含下面 9 字节的用户态代码,映射到线程栈顶
# 上方那一页 [vtop, vtop+0x1000)(本线程栈与上一个线程栈之间的 64KB 间隙,栈向下增长
# 永远到不了)。栈顶下一格写它的 VA 当哨兵返回地址 ⇒ 线程函数 return 时直接落到
# `int 0x80(THREAD_EXIT)`,由内核干净回收本线程。
#
# 为什么不能用原来的哨兵 0:return 会把 0 弹进 rip ⇒ 在 Ring3 取指 VA 0。VA 0 在内核
# identity map 里是**存在但 supervisor-only** 的页,于是 #PF 现场是
# rip=0x0 cr2=0x0 err=0x15(P=1 保护违例 | U=1 用户态 | I/D=1 取指)rsp=线程栈 vtop,
# 日志以 "exc in tid=N, terminating" 收场 —— 一次正常的"线程结束"被报成异常崩溃
# (实测:Scene3D 渲染线程在窗口被关掉后 return 即此现场)。
define USER_THREAD_TRAMPOLINE_SIZE = 0x1000
define TRAMP_CODE_SIZE = 9
_tramp_code: [TRAMP_CODE_SIZE]uint8 = [
0xB8, 0x22, 0x00, 0x00, 0x00, # mov eax, 34 = THREAD_EXIT(与 intr/syscall.vp 一致)
0xCD, 0x80, # int 0x80(DPL=3,硬件按 TSS.RSP0 落到本线程内核栈)
0xEB, 0xFE # jmp $(THREAD_EXIT 不该返回;Ring3 不能 hlt)
]
def alloc_user_thread(pid: int, entry: rawptr, arg: rawptr) -> int:
"""登记一个用户线程:分配内核栈 + 用户栈 + 出口跳板页,并把后两者映射进该进程地址空间。
必须在调用者进程上下文里调用(MapPage 作用于当前 pml4 = THREAD_CREATE 调用者的)。
返回槽位号;失败返回 -1(不兜底,调用方显式处理失败)。
"""
global _user_stack_next
slot: int = -1
i: int
for i in range(MAX_USER_THREADS):
if _user_ctx_used[i] == 0:
slot = i
break
if slot < 0:
serial.puts("[proc] no free user-thread slot\n")
return -1
ks: rawptr = mm.malloc(KERN_STACK_SIZE)
if ks is None:
serial.puts("[proc] user-thread kern stack alloc failed\n")
return -1
us: rawptr = mm.malloc(USER_STACK_SIZE)
if us is None:
serial.puts("[proc] user-thread user stack alloc failed\n")
mm.free(ks)
return -1
tramp: rawptr = mm.malloc(uint64(USER_THREAD_TRAMPOLINE_SIZE))
if tramp is None:
serial.puts("[proc] user-thread trampoline alloc failed\n")
mm.free(us)
mm.free(ks)
return -1
us_size: uint64 = uint64(USER_STACK_SIZE)
phys_bottom: uint64 = uint64(us) & ~uint64(0xFFF)
vtop: uint64 = USER_STACK_VTOP - uint64(_user_stack_next + 1) * uint64(USER_THREAD_STACK_GAP)
_user_stack_next = _user_stack_next + 1
si: int
for si in range(USER_STACK_PAGES):
vaddr: uint64 = (vtop - us_size) + uint64(si) * uint64(0x1000)
paddr: uint64 = phys_bottom + uint64(si) * uint64(0x1000)
paging.MapPage(vaddr, paddr, paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER)
# 出口跳板页:映射在 [vtop, vtop+0x1000) —— 位于本线程栈顶之上,栈向下增长永不到达。
# 字节拷贝沿用 apic.vp 的 _setup_trampoline 惯用法(uint8* 逐字节写)。
tramp_phys: uint64 = uint64(tramp) & ~uint64(0xFFF)
dest: uint8* = uint8*(tramp_phys)
src: uint8* = &_tramp_code
for bi in range(TRAMP_CODE_SIZE):
*(dest + uint64(bi)) = *(src + uint64(bi))
paging.MapPage(vtop, tramp_phys, paging.PTE_PRESENT | paging.PTE_WRITABLE | paging.PTE_USER)
# 入口 rsp 指向栈顶下一格,并在该格写出口跳板 VA 当哨兵返回地址:用户线程函数若
# return,ret 会跳进跳板执行 int 0x80(THREAD_EXIT) ⇒ 内核干净回收本线程。
entry_rsp: uint64 = vtop - uint64(8)
ret_slot: uint64* = uint64*(entry_rsp)
*ret_slot = vtop
_user_ctx_used[slot] = 1
_user_ctx[slot].pid = pid
_user_ctx[slot].tid = -1
_user_ctx[slot].entry = entry
_user_ctx[slot].arg = arg
_user_ctx[slot].user_stack_entry_rsp = entry_rsp
_user_ctx[slot].user_stack_vtop = vtop
_user_ctx[slot].user_stack_phys = us
_user_ctx[slot].kern_stack = uint64(ks) + uint64(KERN_STACK_SIZE)
_user_ctx[slot].tramp = tramp
return slot
def free_user_thread(slot: int):
"""回收用户线程资源(线程已不在用户态执行)。
· 解映射用户栈 VA 并交还物理页;
· 解映射出口跳板页并交还物理页;
· 清槽位标记(允许复用)。
内核栈**故意不在这里 free**:终止路径(异常 ISR / THREAD_EXIT)此刻正跑在该线程
自己的内核栈上,free 后它会被别的线程分配走并覆盖,直到 _do_switch 切走为止。
"""
if slot < 0 or slot >= MAX_USER_THREADS: return
if _user_ctx_used[slot] == 0: return
vtop: uint64 = _user_ctx[slot].user_stack_vtop
si: int
for si in range(USER_STACK_PAGES):
vaddr: uint64 = (vtop - uint64(USER_STACK_SIZE)) + uint64(si) * uint64(0x1000)
paging.UnMapPage(vaddr)
us: rawptr = _user_ctx[slot].user_stack_phys
if us is not None:
mm.free(us)
_user_ctx[slot].user_stack_phys = None
# 出口跳板页同样解映射并交还物理页(与用户栈同批回收)
paging.UnMapPage(vtop)
tramp: rawptr = _user_ctx[slot].tramp
if tramp is not None:
mm.free(tramp)
_user_ctx[slot].tramp = None
_user_ctx_used[slot] = 0
def find_user_slot_by_tid(tid: int) -> int:
i: int
for i in range(MAX_USER_THREADS):
if _user_ctx_used[i] != 0 and _user_ctx[i].tid == tid:
return i
return -1
def user_thread_ctx(slot: int) -> UserThreadCtx*:
if slot < 0 or slot >= MAX_USER_THREADS: return None
if _user_ctx_used[slot] == 0: return None
return &_user_ctx[slot]