Files

554 lines
21 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 drivers.serial.uart.serial as serial
import sched.process as proc
import mm.mm as mm
import intr.gdt as gdt
import drivers.core.cpu.cpu as cpu
import viperlib
import asm
define THREAD_READY = 0
define THREAD_RUNNING = 1
define THREAD_BLOCKED = 2
define THREAD_DONE = 3
define MAX_THREADS = 64
define THREAD_STACK_SIZE = 65536
define SCHED_QUANTUM = 10
_yield_cnt: uint32 = 0
_yield_lock: int = 0
# ISR scratch 是全局槽(isr.s);抢占后一个 ISR 会跨越其它线程的 ISR,故切换时按线程保存/恢复。
export _isr_scratch_tbl: [MAX_THREADS * 6]uint64
export _switch_old_tid: int = 0
export _switch_new_tid: int = 0
# Per-thread IA32_FS_BASE (glibc TLS). Kept outside Thread: Thread has a
# hard-coded 600-byte layout checked by _thread_entry.
define IA32_FS_BASE = 0xC0000100
_fs_base_tbl: [MAX_THREADS]uint64
_syscall_frame_tbl: [MAX_THREADS]uint64
_clear_child_tid: [MAX_THREADS]uint64
def _msr_read(msr: uint32) -> uint64:
lo: uint32
hi: uint32
__asm__(f"""mov ecx, {asminp(msr, ASM_DESCR.REG_ANY)}
rdmsr
mov {asmout(lo, ASM_DESCR.OUTPUT_REG)}, eax
mov {asmout(hi, ASM_DESCR.OUTPUT_REG)}, edx""",
out=[asmout(lo, ASM_DESCR.OUTPUT_REG), asmout(hi, ASM_DESCR.OUTPUT_REG)],
op=[ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RDX])
return uint64(lo) | (uint64(hi) << 32)
def _msr_write(msr: uint32, val: uint64):
lo: uint32 = uint32(val & uint64(0xFFFFFFFF))
hi: uint32 = uint32(val >> 32)
__asm__(f"""mov ecx, {asminp(msr, ASM_DESCR.REG_ANY)}
mov eax, {asminp(lo, ASM_DESCR.REG_ANY)}
mov edx, {asminp(hi, ASM_DESCR.REG_ANY)}
wrmsr""",
op=[ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RAX, ASM_DESCR.CLOBBER_RDX])
class Thread:
rsp: uint64
stack: rawptr
stack_size: uint64
func: rawptr
arg: rawptr
state: int
tid: int
pid: int
# Ring3 → Ring0 的落栈点(TSS.RSP0 / gs:[8]):用户线程才非 0,内核线程为 0。
# 必须按线程切换 —— 同进程多用户线程共用进程级内核栈会互相压栈帧。
kern_stack: uint64
# FXSAVE 区:528 = 512 + 16 对齐余量;fpu_ptr 指向其中 16B 对齐起点(内嵌,无需单独释放)。
fpu_area: [528]uint8
fpu_ptr: rawptr
def block(self):
self.state = THREAD_BLOCKED
Scheduler._yield()
def unblock(self):
if self.state == THREAD_BLOCKED:
self.state = THREAD_READY
@property
def is_running(self) -> bool: return self.state == THREAD_RUNNING
@property
def is_blocked(self) -> bool: return self.state == THREAD_BLOCKED
@property
def is_done(self) -> bool: return self.state == THREAD_DONE
# 取 fpu_area 内 16B 对齐的 512B FXSAVE 区;直接写 fninit 默认镜像(不碰实时 FPU 状态)。
def _fpu_attach(th: Thread*):
fp: uint8* = uint8*((uint64(&th.fpu_area[0]) + 15) & ~uint64(0xF))
th.fpu_ptr = rawptr(uint64(fp))
for i in range(512):
*(fp + uint64(i)) = 0
# FCW=0x037F、MXCSR=0x1F80(全异常屏蔽、就近舍入)、MXCSR_MASK=0xFFFF
*(fp) = uint8(0x7F)
*(fp + uint64(1)) = uint8(0x03)
*(fp + uint64(24)) = uint8(0x80)
*(fp + uint64(25)) = uint8(0x1F)
*(fp + uint64(28)) = uint8(0xFF)
*(fp + uint64(29)) = uint8(0xFF)
class Scheduler:
threads: [MAX_THREADS]Thread
current_tid: int
thread_count: int
enabled: int
needs_reschedule: int
tick_count: int
def __init__(self):
global _sched_ptr
_sched_ptr = &_sched_storage
s: Scheduler* = _sched_ptr
s.current_tid = 0
s.thread_count = 1
s.enabled = 0
s.needs_reschedule = 0
s.tick_count = 0
for i in range(MAX_THREADS):
s.threads[i].rsp = 0
s.threads[i].stack = None
s.threads[i].stack_size = 0
s.threads[i].func = None
s.threads[i].arg = None
s.threads[i].state = THREAD_DONE
s.threads[i].tid = i
s.threads[i].pid = 0
_fpu_attach(&s.threads[i])
s.enabled = 1
s.threads[0].state = THREAD_RUNNING
s.threads[0].tid = 0
main_rsp_ptr: rawptr = &s.threads[0].rsp
__asm__(f"""mov [{asminp(main_rsp_ptr, ASM_DESCR.REG_ANY)}], rsp""",
op=[ASM_DESCR.CLOBBER_MEMORY])
# fail-fast:_thread_entry 的 asm 以硬编码偏移访问线程表(sizeof(Thread)=600、
# current_tid=+38400=64*600)。增删 Thread 字段会让它们失配 ⇒ 线程入口会按错误的
# 偏移取 func/arg 并 call 任意地址。这里直接核对,不符立即停机暴露。
thr0_addr: uint64 = uint64(&s.threads[0])
curt_addr: uint64 = uint64(&s.current_tid)
if Thread.__sizeof__() != 600 or (curt_addr - thr0_addr) != 38400:
serial.puts("[sched] FATAL: Thread 布局与 _thread_entry 硬编码偏移(600/38400)不一致\n")
while True:
asm.sti()
asm.hlt()
@staticmethod
def _yield():
global _switch_old_rsp, _switch_new_rsp, _yield_cnt, _yield_lock
global _switch_old_tid, _switch_new_tid
global _switch_old_fpu, _switch_new_fpu
global _fs_base_tbl
_if_saved: int = 0
__asm__(f"""pushfq
pop rax
shr rax, 9
and eax, 1
mov {asmout(_if_saved, ASM_DESCR.OUTPUT_REG)}, eax""",
op=[ASM_DESCR.CLOBBER_RAX])
asm.cli()
if _yield_lock:
if _if_saved:
asm.sti()
return
_yield_lock = 1
s: Scheduler* = _sched_ptr
s.needs_reschedule = 0
s.tick_count = 0
old_tid: int = s.current_tid
next_tid: int = old_tid + 1
if next_tid >= s.thread_count:
next_tid = 0
found: int = 0
for count in range(s.thread_count):
st: int = s.threads[next_tid].state
if st == THREAD_READY or st == THREAD_RUNNING:
found = 1
break
next_tid += 1
if next_tid >= s.thread_count:
next_tid = 0
if found == 0:
_yield_lock = 0
if _if_saved:
asm.sti()
return
if next_tid == old_tid:
_yield_lock = 0
if _if_saved:
asm.sti()
return
if s.threads[old_tid].state == THREAD_RUNNING:
s.threads[old_tid].state = THREAD_READY
s.threads[next_tid].state = THREAD_RUNNING
_switch_old_rsp = &s.threads[old_tid].rsp
_switch_new_rsp = &s.threads[next_tid].rsp
_switch_old_fpu = s.threads[old_tid].fpu_ptr
_switch_new_fpu = s.threads[next_tid].fpu_ptr
s.current_tid = next_tid
next_pid: int = s.threads[next_tid].pid
_yield_cnt += 1
proc.switch_pid(next_pid)
# 按**线程**设置 Ring3→Ring0 的落栈点:switch_pid 只按进程设置(会把全进程的用户线程
# 指到同一个内核栈上),两个线程都在 syscall 里 yield 时后者的栈帧会压掉前者的。
next_ks: uint64 = s.threads[next_tid].kern_stack
if next_ks != 0:
gdt.set_tss_rsp0(next_ks)
cpu.set_kernel_rsp0(next_ks)
_switch_old_tid = old_tid
_switch_new_tid = next_tid
# Per-thread TLS: save the outgoing FS base, load the incoming one.
_fs_base_tbl[old_tid] = _msr_read(uint32(IA32_FS_BASE))
_msr_write(uint32(IA32_FS_BASE), _fs_base_tbl[next_tid])
_yield_lock = 0
_do_switch()
if _if_saved:
asm.sti()
@staticmethod
def sched_tick():
s: Scheduler* = _sched_ptr
if not s.enabled: return
if s.thread_count <= 1: return
s.tick_count += 1
if s.tick_count >= SCHED_QUANTUM:
s.tick_count = 0
s.needs_reschedule = 1
@staticmethod
def try_reschedule():
s: Scheduler* = _sched_ptr
if s.needs_reschedule:
s.needs_reschedule = 0
Scheduler._yield()
@staticmethod
def yield_now():
"""显式让出 CPU:**无条件**切换。
与 try_reschedule() 的区别很关键:后者只在 needs_reschedule(时间片到期)时切换,
那是"抢占式调度器在中断里做的判断"。而 sched_yield 的语义是"我主动放弃剩余时间片",
必须立刻切走 —— 否则主动等待外设的进程会白等:它的 N 次 yield 全落在同一个时间片内、
一次都不切换。现场:app 用 4000 次 yield_cpu() 等 /dev/desk ready,全部超时退出,
而 explorer(共享区初始化者)直到所有 app 退完才被调度 ⇒ 建窗全失败 ⇒ 静默退出。
"""
Scheduler._yield()
@staticmethod
def disable():
s: Scheduler* = _sched_ptr
s.enabled = 0
@staticmethod
def enable():
s: Scheduler* = _sched_ptr
s.enabled = 1
@staticmethod
def current() -> Thread*:
s: Scheduler* = _sched_ptr
return &s.threads[s.current_tid]
@staticmethod
def get_thread(tid: int) -> Thread*:
s: Scheduler* = _sched_ptr
if tid < 0 or tid >= s.thread_count: return None
return &s.threads[tid]
@staticmethod
def create_thread(func: rawptr, arg: rawptr, thread_pid: int = 0) -> Thread*:
s: Scheduler* = _sched_ptr
if s.thread_count >= MAX_THREADS: return None
tid: int = s.thread_count
s.thread_count += 1
th: Thread* = &s.threads[tid]
th.state = THREAD_READY
th.tid = tid
th.pid = thread_pid
th.func = func
th.arg = arg
th.kern_stack = 0
_fpu_attach(th)
stack_size: uint64 = THREAD_STACK_SIZE
stack_ptr: rawptr = mm.malloc(stack_size)
if not stack_ptr:
s.thread_count -= 1
return None
th.stack = stack_ptr
th.stack_size = stack_size
stack_top: uint64 = uint64(stack_ptr) + stack_size - 16
stack_top = stack_top & ~uint64(0xF)
entry_addr: uint64 = uint64(&_thread_entry)
sp: rawptr = rawptr(stack_top)
__asm__(f"""mov rax, {asminp(sp, ASM_DESCR.REG_ANY)}
mov qword ptr [rax - 64], 0x202
mov qword ptr [rax - 56], 0
mov qword ptr [rax - 48], 0
mov qword ptr [rax - 40], 0
mov qword ptr [rax - 32], 0
mov qword ptr [rax - 24], 0
mov qword ptr [rax - 16], 0
mov qword ptr [rax - 8], 0
mov [rax], {asminp(entry_addr, ASM_DESCR.REG_ANY)}""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX])
stack_top = stack_top - 64
th.rsp = stack_top
return th
@staticmethod
def create_user_thread(thread_pid: int, entry: rawptr, arg: rawptr) -> Thread*:
"""创建**用户态**线程:入口在 Ring 3 执行(THREAD_CREATE 用)。
app 传来的函数指针是用户地址,绝不能用 create_thread 直接 call —— 那是在 Ring 0
执行用户代码,且与同进程其它线程共用一套内核栈。这里给线程配独立用户栈 + 独立
内核栈,由 _user_thread_entry 走 drop_to_user_mode 进 Ring 3。
"""
slot: int = proc.alloc_user_thread(thread_pid, entry, arg)
if slot < 0: return None
ctx: proc.UserThreadCtx* = proc.user_thread_ctx(slot)
if ctx is None: return None
th: Thread* = Scheduler.create_thread(&_user_thread_entry, rawptr(uint64(slot)), thread_pid)
if th is None: return None
th.kern_stack = ctx.kern_stack
ctx.tid = th.tid
# 出口跳板模板(process.vp 的 _tramp_code)仍写着旧调用号 34,而 THREAD_EXIT = 60:
# 用户线程自然 return 会执行一次无人处理的 syscall ⇒ iretq 回 Ring3 `jmp $` 空转;
# 内核只在 syscall 返回路径消费 needs_reschedule ⇒ 全系统不再切换(关 Scene3D 即冻结)。
# 模板首字节是 `mov eax, imm32`(B8),立即数从 +1 开始,把低字节改成 60 即可。
tramp_page: uint8* = uint8*(uint64(ctx.tramp) & ~uint64(0xFFF))
*(tramp_page + uint64(1)) = uint8(60)
return th
@staticmethod
def exit_current_thread():
"""用户线程显式退出(THREAD_EXIT):标记 DONE → 回收资源 → 让出,永不返回。
次序很重要:**先**置 THREAD_DONE,**再** free_user_thread。反过来的话,在
"已释放用户栈/上下文、但 state 还是 RUNNING" 这段窗口里只要发生一次再调度
(定时器 IRQ → try_reschedule → _yield 的选择只按 state 过滤),本线程就会被
再次选中并带着失效上下文回到用户态 ⇒ 在已回收的栈上取指 0:
现场 [0E] rip=0x0 err=0x15(用户态取指缺页)cr2=0x0 rsp=0x7fffffde0000,tid 为该 app 线程,
紧跟在 [syscall] exit called 之后。
"""
global _clear_child_tid
s: Scheduler* = _sched_ptr
tid: int = s.current_tid
ct: uint64 = _clear_child_tid[tid]
if ct != 0:
cw: uint32* = uint32*(ct)
cw[0] = uint32(0)
_clear_child_tid[tid] = 0
if tid > 0 and tid < MAX_THREADS:
s.threads[tid].state = THREAD_DONE
proc.free_user_thread(proc.find_user_slot_by_tid(tid))
Scheduler._yield()
# _yield 可能因"只有自己可运行"而直接返回:此后绝不能再跑用户代码(栈已回收)。
while True:
asm.sti()
asm.hlt()
Scheduler._yield()
@staticmethod
def exit_process(pid: int):
"""Linux exit_group: retire every thread of `pid`, release the process, never return."""
global _clear_child_tid
s: Scheduler* = _sched_ptr
i: int = 0
while i < s.thread_count:
if s.threads[i].pid == pid:
ct: uint64 = _clear_child_tid[i]
if ct != 0:
cw: uint32* = uint32*(ct)
cw[0] = uint32(0)
_clear_child_tid[i] = 0
s.threads[i].state = THREAD_DONE
slot: int = proc.find_user_slot_by_tid(i)
if slot >= 0:
proc.free_user_thread(slot)
i += 1
p: proc.Process* = proc.ProcessManager.get_process(pid)
if p is not None:
p.exit()
Scheduler._yield()
while True:
asm.sti()
asm.hlt()
Scheduler._yield()
_sched_storage: Scheduler = Scheduler()
export _sched_ptr: Scheduler* = &_sched_storage
export _switch_old_rsp: rawptr
export _switch_new_rsp: rawptr
export _switch_old_fpu: rawptr
export _switch_new_fpu: rawptr
def set_fs_base(base: uint64):
global _fs_base_tbl
s: Scheduler* = _sched_ptr
_fs_base_tbl[s.current_tid] = base
_msr_write(uint32(IA32_FS_BASE), base)
def set_thread_fs_base(tid: int, base: uint64):
global _fs_base_tbl
if tid >= 0 and tid < MAX_THREADS:
_fs_base_tbl[tid] = base
def get_fs_base() -> uint64:
return _msr_read(uint32(IA32_FS_BASE))
def set_syscall_frame(f: uint64):
global _syscall_frame_tbl
s: Scheduler* = _sched_ptr
_syscall_frame_tbl[s.current_tid] = f
def get_syscall_frame() -> uint64:
s: Scheduler* = _sched_ptr
return _syscall_frame_tbl[s.current_tid]
def set_clear_child_tid(tid: int, addr: uint64):
global _clear_child_tid
if tid >= 0 and tid < MAX_THREADS:
_clear_child_tid[tid] = addr
@__attribute__(naked)
def _do_switch():
__asm__(f"""push rax
push rbx
push rbp
push r12
push r13
push r14
push r15
pushfq
# 切栈前保存 FPU/ISR
mov rax, [rip + _switch_old_fpu]
fxsave [rax]
# 保存 ISR scratch
lea rsi, [rip + _isr_scratch]
mov eax, dword ptr [rip + _switch_old_tid]
imul rax, rax, 48
lea rdx, [rip + _isr_scratch_tbl]
add rdx, rax
mov rcx, [rsi]
mov [rdx], rcx
mov rcx, [rsi + 8]
mov [rdx + 8], rcx
mov rcx, [rsi + 16]
mov [rdx + 16], rcx
mov rcx, [rsi + 24]
mov [rdx + 24], rcx
mov rcx, [rsi + 32]
mov [rdx + 32], rcx
mov rcx, [rsi + 40]
mov [rdx + 40], rcx
mov rax, [rip + _switch_old_rsp]
mov [rax], rsp
mov rax, [rip + _switch_new_rsp]
mov rsp, [rax]
# 换栈后恢复 FPU/ISR
mov rax, [rip + _switch_new_fpu]
fxrstor [rax]
# 恢复 ISR scratch
lea rsi, [rip + _isr_scratch]
mov eax, dword ptr [rip + _switch_new_tid]
imul rax, rax, 48
lea rdx, [rip + _isr_scratch_tbl]
add rdx, rax
mov rcx, [rdx]
mov [rsi], rcx
mov rcx, [rdx + 8]
mov [rsi + 8], rcx
mov rcx, [rdx + 16]
mov [rsi + 16], rcx
mov rcx, [rdx + 24]
mov [rsi + 24], rcx
mov rcx, [rdx + 32]
mov [rsi + 32], rcx
mov rcx, [rdx + 40]
mov [rsi + 40], rcx
popfq
pop r15
pop r14
pop r13
pop r12
pop rbp
pop rbx
pop rax
ret""",
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX])
# Thread 布局:rsp0/stack8/stack_size16/func24/arg32/state40/tid44/pid48/kern_stack56/fpu_area64/fpu_ptr592
# ⇒ sizeof(Thread) = 600,Scheduler.threads[64] 占 38400 字节 ⇒ current_tid 在 +38400。
# 增删 Thread 字段时必须同步下面 asm 里的 38400 / 600(以及 +24/+32/+40 三个字段偏移)。
@__attribute__(naked)
def _thread_entry():
__asm__(f"""sub rsp, 8
mov rax, [rip + _sched_ptr]
mov ecx, dword ptr [rax + 38400]
movsxd rcx, ecx
imul rcx, rcx, 600
mov rdi, [rax + rcx + 32]
mov rax, [rax + rcx + 24]
call rax
mov rax, [rip + _sched_ptr]
mov ecx, dword ptr [rax + 38400]
movsxd rcx, ecx
imul rcx, rcx, 600
mov dword ptr [rax + rcx + 40], 3
add rsp, 8""",
# `call rax` 破坏全部 caller-saved 寄存器(x64 SysV):除 RAX/RCX/RDX/
# R8-R11 外,RSI 与 RDI 也必须声明,否则编译器可能假设其值存活(N9)。
# RSP 由 `sub rsp,8` / `add rsp,8` 配对抵消,净变化为 0,无需声明。
op=[ASM_DESCR.CLOBBER_MEMORY, ASM_DESCR.CLOBBER_RAX,
ASM_DESCR.CLOBBER_RCX, ASM_DESCR.CLOBBER_RDX,
ASM_DESCR.CLOBBER_RSI, ASM_DESCR.CLOBBER_RDI,
ASM_DESCR.CLOBBER_R8, ASM_DESCR.CLOBBER_R9,
ASM_DESCR.CLOBBER_R10, ASM_DESCR.CLOBBER_R11])
Scheduler._yield()
while True:
asm.sti()
asm.hlt()
Scheduler._yield()
def _user_thread_entry(arg: rawptr) -> int:
"""用户态线程的内核侧引导:配好 TSS/GS 落栈点后 iretq 进 Ring 3,永不返回。"""
slot: int = int(uint64(arg))
ctx: proc.UserThreadCtx* = proc.user_thread_ctx(slot)
if ctx is None: return 0
# 与 execrunner/elf.vp 的 _elf_entry_wrapper 同款:直到 iretq 之前必须关中断
# (@pml4/TSS/GS 都是全局量,被定时器抢占时别的线程的 switch_pid 会把它们改掉);
# iretq 用用户态帧里的 RFLAGS.IF(0x202) 重新开中断。
asm.cli()
proc.switch_pid(ctx.pid)
gdt.set_tss_rsp0(ctx.kern_stack)
cpu.set_kernel_rsp0(ctx.kern_stack)
proc.drop_to_user_mode(ctx.entry, ctx.user_stack_entry_rsp, ctx.kern_stack, ctx.arg)
return 0
def thread_current() -> int:
s: Scheduler* = _sched_ptr
return s.current_tid
def needs_reschedule() -> int:
s: Scheduler* = _sched_ptr
return s.needs_reschedule
def sched_dump():
s: Scheduler* = _sched_ptr
dl: [80]char
viperlib.snprintf(&dl, 80, "[sched] dump: cur=%d cnt=%d yields=%u\n", s.current_tid, s.thread_count, _yield_cnt)
serial.puts(dl)
for i in range(s.thread_count):
sl: [80]char
viperlib.snprintf(&sl, 80, " t%d: st=%d pid=%d rsp=0x%lx\n", i, s.threads[i].state, s.threads[i].pid, s.threads[i].rsp)
serial.puts(sl)