From: Alan Cox Date: Sat, 30 May 2015 02:03:49 +0000 (+0100) Subject: process: rewrite the interrupt handling logic to be sane X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=f37e0b3929a7d0a7501775d99087560e727f9190;p=FUZIX.git process: rewrite the interrupt handling logic to be sane We get rid of the whole UZI inherited irq stack madness as it leaves us with a bunch of hard/unfixable problems. Instead - We only need one interrupt stack (although several if the banking is easier still works just fine) - We don't task switch on the IRQ stack, instead if we are going to be pre-empting we pull a stunt with push/reti to complete the IRQ to the devices and then task switch on the syscall stack and do signal processing This fixes all the pre-emption related crashes and mess with map saving. We never have to bail from an IRQ because we are already in it. Various other bits become cleaner. In the process also rewrite (hopefully correctly this time) the Z80 signal return paths. This will probably break everything except Z80 platform devices using the bankfixed helpers. Note: The new IRQ code breaks sdltrs. I'll upload some patches for that when I get a bit of time. Elements of the IRQ emulation and also reti emulation are buggy in sdltrs it seems. FIXME: - At least one additional di to work around bits of the trs80 bugs can go away --- diff --git a/Kernel/include/kernel.h b/Kernel/include/kernel.h index ab32dc08..7aa66114 100644 --- a/Kernel/include/kernel.h +++ b/Kernel/include/kernel.h @@ -621,6 +621,7 @@ extern void switchout(void); extern void doexec(uaddr_t start_addr); extern void switchin(ptptr process); extern int16_t dofork(ptptr child); +extern uint8_t need_resched; /* devio.c */ extern uint8_t *bread (uint16_t dev, blkno_t blk, bool rewrite); diff --git a/Kernel/lib/z80fixedbank.s b/Kernel/lib/z80fixedbank.s index 10a22d02..78ad933e 100644 --- a/Kernel/lib/z80fixedbank.s +++ b/Kernel/lib/z80fixedbank.s @@ -23,6 +23,7 @@ .globl unix_syscall_entry .globl interrupt_handler .globl _swapper + .globl _need_resched .globl map_kernel .globl map_process @@ -55,10 +56,6 @@ _switchout: push iy ld (U_DATA__U_SP), sp ; this is where the SP is restored in _switchin - ; set inint to false - xor a - ld (_inint), a - ; Stash the uarea back into process memory call map_process_always ld hl, #U_DATA @@ -120,6 +117,12 @@ _switchin: pop hl ld a, (hl) not_swapped: +; ld hl, (U_DATA__U_PTAB) +; or a +; sbc hl, de +; jr z, skip_copyback ; Tormod's optimisation: don't copy the + ; the stash back if we are the task who + ; last owned the real udata ; Pages please ! call map_process_a @@ -138,9 +141,10 @@ not_swapped: ; check u_data->u_ptab matches what we wanted ld hl, (U_DATA__U_PTAB) ; u_data->u_ptab or a ; clear carry flag - sbc hl, de ; subtract, result will be zero if DE==IX + sbc hl, de ; subtract, result will be zero if DE==HL jr nz, switchinfail +skip_copyback: ; wants optimising up a bit ld ix, (U_DATA__U_PTAB) ; next_process->p_status = P_RUNNING @@ -162,10 +166,10 @@ not_swapped: pop ix pop hl ; return code - ; enable interrupts, if the ISR isn't already running - ld a, (_inint) + ; enable interrupts, if we didn't pre-empt in an ISR + ld a, (U_DATA__U_ININTERRUPT) or a - ret z ; in ISR, leave interrupts off + ret nz ; Not an ISR, leave interrupts off ei ret ; return with interrupts on @@ -317,3 +321,4 @@ bouncebuffer: ; banked fork() call. ; _swapstack: +_need_resched: .db 0 diff --git a/Kernel/lowlevel-z80.s b/Kernel/lowlevel-z80.s index e79dc8ff..79af2d4d 100644 --- a/Kernel/lowlevel-z80.s +++ b/Kernel/lowlevel-z80.s @@ -28,10 +28,11 @@ .globl map_save .globl map_restore .globl outchar - .globl _kernel_flag .globl _inint .globl _platform_interrupt .globl platform_interrupt_all + .globl _need_resched + .globl _switchout ; exported symbols .globl _chksigs @@ -65,8 +66,68 @@ CPU_NMOS_Z80 .equ Z80_TYPE-1 CPU_Z180 .equ Z80_TYPE-2 .area _COMMONMEM +; +; Called on the user stack in order to process signals that +; are pending. A user process can longjmp out of this loop so +; care is needed. Call with interrupts disabled and user mapped. +; +; Returns with interrupts disabled and user mapped, but may +; enable interrupts and change mappings. +; +deliver_signals: + ; Pending signal + ld a, (U_DATA__U_CURSIG) + or a + ret z + +deliver_signals_2: + ld l, a + ld h, #0 + push hl ; signal number as C argument to the handler + + ; Handler to use + add hl, hl + ld de, #U_DATA__U_SIGVEC + add hl, de + ld e, (hl) + inc hl + ld d,(hl) + + ; Indicate processed + xor a + ld (U_DATA__U_CURSIG), a + + ; Semantics for now: signal delivery clears handler + ld (hl), a + dec hl + ld (hl), a + + ld bc, #signal_return + push bc ; bc is passed in as the return vector + + ex de, hl + ei + jp (hl) ; return to user space. This will then return via + ; the return path handler passed in BC -; entry point for UZI system calls +; +; Syscall signal return path +; +signal_return: + pop hl ; argument + di + ld (U_DATA__U_SYSCALL_SP), sp + ld sp, #kstack_top + call map_kernel + call _chksigs + call map_process_always + ld sp, (U_DATA__U_SYSCALL_SP) + jr deliver_signals + + +; +; Syscall processing path +; unix_syscall_entry: di ; store processor state @@ -78,12 +139,11 @@ unix_syscall_entry: push de push hl exx - ; push af ;; WRS: also skip this push bc push de - ; push hl ;; WRS: we could skip this since we always set HL on return push ix push iy + ; We don't save AF or HL ; locate function call arguments on the userspace stack ld hl, #18 ; 16 bytes machine state, plus 2 bytes return address @@ -107,51 +167,51 @@ unix_syscall_entry: ; map in kernel keeping common call map_kernel - ; make sure the interrupt logic knows we are in kernel mode (flag lives in kernel bank) - ld a, #1 - ld (_kernel_flag), a - ; re-enable interrupts ei ; now pass control to C call _unix_syscall - di - ; let the interrupt logic know we are not in kernel mode any more - ; kernel_flag is not in common so write it before we map it away - xor a - ld (_kernel_flag), a ; - ; We restart from here on a unix syscall signal return path + ; WARNING: There are two special cases to beware of here + ; 1. fork() will return twice from _unix_syscall + ; 2. execve() will not return here but will hit _doexec() ; -unix_sig_exit: - ; map process memory back in based on common (common may have - ; changed on a task switch) - call map_process_always - ; switch back to user stack - ld sp, (U_DATA__U_SYSCALL_SP) + di + + call map_process_always - ; check for signals, call the handlers - call dispatch_process_signal + ; Back to the user stack + ld sp, (U_DATA__U_SYSCALL_SP) - ; check if error condition to be signalled on return - ld hl, (U_DATA__U_ERROR) - ld a, h - or l ; set NZ flag if we are to return error - jr z, not_error + ld hl, (U_DATA__U_ERROR) + ld de, (U_DATA__U_RETVAL) - scf ; set carry flag - ; note error code remains in HL - jr unix_return + ld a, (U_DATA__U_CURSIG) + or a -not_error: - ; no error to signal! return syscall return value instead of error code - ld hl, (U_DATA__U_RETVAL) - ; fall through to return code + ; Fast path the normal case + jr nz, via_signal + ; Restore stacks and go + ; + ; Should we change the ABI and just return in DE/HL ? + ; unix_return: + ld a, h + or l + jr z, not_error + scf ; carry flag on return state for errors + jr unix_pop + +not_error: + ex de, hl ; return the retval instead + ; + ; Undo the stacking and go back to user space + ; +unix_pop: ; restore machine state pop iy pop ix @@ -170,76 +230,35 @@ unix_return: ei ret ; must immediately follow EI -dispatch_process_signal: - ; check if any signal outstanding - ld a, (U_DATA__U_CURSIG) - or a - ret z - - ; put system call number on the stack as the argument for the signal handler - ld l, a - ld h, #0 - push hl - - ; load the address of signal handler function - add hl, hl - ld de, #U_DATA__U_SIGVEC - add hl, de - ld e, (hl) - inc hl - ld d, (hl) ; now DE = udata.u_sigvec[cursig] - - ; udata.u_cursig = 0; - xor a - ld (U_DATA__U_CURSIG), a - ; restore signal handler to the default. - ; udata.u_sigvec[cursig] = SIG_DFL; - ; SIG_DFL = 0, A is still 0 from above, HL points at second byte of the signal vector. - ld (hl), a - dec hl - ld (hl), a +via_signal: + ; Get off the kernel syscall stack before we start signal + ; handling. Our signal handlers may themselves elect to make system + ; calls. This means we must also save the error/return code + ld hl, (U_DATA__U_ERROR) + push hl + ld hl, (U_DATA__U_RETVAL) + push hl - ld hl, #signal_return - push hl ; push return address + ; Signal processing. This may longjmp back into userland + call deliver_signals_2 - ex de,hl - ei - jp (hl) ; call signal handler in userspace, with interrupts enabled + ; If not then we recover the syscall return values and + ; exit via the syscall return path + pop de ; retval + pop hl ; errno + jr unix_return -signal_return: - pop hl ; remove arg from stack - pop hl ; we won't be using the return address either - di ; So we don't screw up in mapping and stack games - ; save process stack pointer - ld (U_DATA__U_SYSCALL_SP), sp - ; switch to kernel stack - ld sp, #kstack_top - call map_kernel - call _chksigs - ; Loop back around, switch stacks, check if there is a signal - ; and if so process it. - ; - ; If we do restartable signals we can just check the restartable - ; info and jmp back further up the syscall path *providing* that - ; on signal exit paths we write back any needed parameters with - ; their new info - jr unix_sig_exit +; +; Final component of execve() +; _doexec: di - ; this is a funny extra path out of syscall so we must also cover - ; the exit from kernel here - xor a - ld (_kernel_flag), a - ; map task into address space (kernel_flag is no longer mapped, don't - ; re-order this) call map_process_always pop bc ; return address pop de ; start address - ;; push de ; restore stack ... but we're about to discard SP anyway! - ;; push bc ld hl, (U_DATA__U_ISP) ld sp, hl ; Initialize user stack, below main() parameters and the environment @@ -256,179 +275,40 @@ _doexec: jp (hl) ; -; Very simple IRQ handler, we get interrupts at 50Hz and we have to -; poll ttys from it. The more logic we could move to common here the -; better. +; Trap handlers +; +; Enter with HL being the signal to send ourself ; -interrupt_handler: - ; store machine state - ; we arrive here via the trampoline at 0038h with interrupts disabled - ; save CPU registers (restored in _IRET) - ex af,af' - push af - ex af,af' - exx - push bc - push de - push hl - exx - push af - push bc - push de - push hl - push ix - push iy - - ; Some platforms (MSX for example) have devices we *must* - ; service irrespective of kernel state in order to shut them - ; up. This code must be in common and use small amounts of stack - call platform_interrupt_all - ; FIXME: add profil support here (need to keep profil ptrs - ; unbanked if so ?) - - ; don't allow us to run re-entrant, we've only got one interrupt stack - ld a, (U_DATA__U_ININTERRUPT) - or a - jp nz, interrupt_return - inc a - ld (U_DATA__U_ININTERRUPT), a - -.ifeq CPU_Z180 - ; On Z180 we have more than one IRQ, so we need to track of which one - ; we arrived through. The IRQ handler sets irqvector_hw when each - ; interrupt arrives. If we are not already handling an interrupt then - ; we copy this into _irqvector which is the value the kernel code - ; examines (and will not change even if reentrant interrupts arrive). - ; Generally the only place that irqvector_hw should be used is in - ; the platform_interrupt_all routine. - .globl hw_irqvector - .globl _irqvector - ld a, (hw_irqvector) - ld (_irqvector), a -.endif - - ; switch stacks - ld (istack_switched_sp), sp - ; the istack is not banked (very important!) - ld sp, #istack_top - - call map_save - - ld a, (0) ; save address 0 contents for checking - ld b, a - - call map_kernel - ; - ; kernel_flag is in the kernel map so we need to map early, we - ; need to map anyway for trap_signal - ; - ld a, (_kernel_flag) - or a - push af - jr nz, in_kernel - - ; we're not in kernel mode, check for signals and fault - ld a, #0xC3 - cp b ; should be a jump - jr z, nofault - call map_process_always; map the process - ld a, #0xC3 ; put it back - ld (0), a ; write - call map_kernel ; restore the map - ld hl, #11 ; SIGSEGV - call trap_signal ; signal the user with a fault - -nofault: -in_kernel: - ; set inint to true - ld a, #1 - ld (_inint), a - - ; this may task switch if not within a system call - ; if we switch then istack_switched_sp is out of date. - ; When this occurs we will exit via the resume path - call _platform_interrupt - - xor a - ld (_inint), a - - pop af ; Z = in kernel - jr nz, in_kernel_2 - - ; On a return to user space always do a new map, we may have - ; changed process - call map_process_always - jr int_switch - ; On a return from an interrupt in kernel mode restore the old - ; mapping as it will vary during kernel activity and the kernel - ; wants it put back as it was before -in_kernel_2: - call map_restore -int_switch: - ld sp, (istack_switched_sp) ; stack back - - xor a - ld (U_DATA__U_ININTERRUPT), a - - ld a, (U_DATA__U_INSYS) - or a - jr nz, interrupt_return - - ; FIXME: check kernel mode flag ? - ; we're not in kernel mode, check for signals - call dispatch_process_signal - ; FIXME: we should loop for multiple signals probably - -interrupt_return: - pop iy - pop ix - pop hl - pop de - pop bc - pop af - exx - pop hl - pop de - pop bc - exx - ex af, af' - pop af - ex af, af' - ei -.ifeq CPU_Z180 - ; WRS - we could examine hw_irqvector and return with ret/reti as appropriate? - ret -.else - reti -.endif - -; Enter with HL being the signal to send ourself trap_signal: - push hl - ld hl, (U_DATA__U_PTAB); - push hl - call _ssig - pop hl - pop hl - ret + push hl + ld hl, (U_DATA__U_PTAB); + push hl + call _ssig + pop hl + pop hl + ret +; ; Called from process context (hopefully) +; +; FIXME: hardcoded RST38 won't work on all boxes +; null_handler: - ; kernel jump to NULL is bad - ld a, (U_DATA__U_INSYS) - or a - jp z, trap_illegal - ; user is merely not good - ld hl, #7 - push hl - ld hl, (U_DATA__U_PTAB) - push hl - ld hl, #10 ; signal (getpid(), SIGBUS) - rst #0x30 ; syscall - pop hl - pop hl - ld hl, #0 - rst #0x30 ; exit + ; kernel jump to NULL is bad + ld a, (U_DATA__U_INSYS) + or a + jp z, trap_illegal + ; user is merely not good + ld hl, #7 + push hl + ld hl, (U_DATA__U_PTAB) + push hl + ld hl, #10 ; signal (getpid(), SIGBUS) + rst #0x30 ; syscall + pop hl + pop hl + ld hl, #0 + rst #0x30 ; exit @@ -446,12 +326,197 @@ dpsmsg: .ascii "[dispsig]" nmimsg: .ascii "[NMI]" .db 13,10,0 + nmi_handler: call map_kernel ld hl, #nmimsg call outstring jp _trap_monitor +; +; Interrupt handler. Not quite the same as syscalls, we need to +; stack everything and we must get off the IRQ stack and then +; process need_resched and signals +; +interrupt_handler: + ; store machine state + ex af,af' + push af + ex af,af' + exx + push bc + push de + push hl + exx + push af + push bc + push de + push hl + push ix + push iy + di + + ; Some platforms (MSX for example) have devices we *must* + ; service irrespective of kernel state in order to shut them + ; up. This code must be in common and use small amounts of stack + call platform_interrupt_all + ; FIXME: add profil support here (need to keep profil ptrs + ; unbanked if so ?) +.ifeq CPU_Z180 + ; On Z180 we have more than one IRQ, so we need to track of which one + ; we arrived through. The IRQ handler sets irqvector_hw when each + ; interrupt arrives. If we are not already handling an interrupt then + ; we copy this into _irqvector which is the value the kernel code + ; examines (and will not change even if reentrant interrupts arrive). + ; Generally the only place that irqvector_hw should be used is in + ; the platform_interrupt_all routine. + .globl hw_irqvector + .globl _irqvector + ld a, (hw_irqvector) + ld (_irqvector), a +.endif + + ; Get onto the IRQ stack + ld (istack_switched_sp), sp + ld sp, #istack_top + + call map_save + ; + ; FIXME: re-implement sanity checks and add a stack one + ; + + ; We need the kernel mapped for the IRQ handling + call map_kernel + + ; So the kernel can check rapidly for interrupt status + ; FIXME: move to the C code + ld a, #1 + ld (_inint), a + ; So we know that this task should resume with IRQs off + ld (U_DATA__U_ININTERRUPT), a + + call _platform_interrupt + + xor a + ld (_inint), a + + ld a, (_need_resched) + or a + jr nz, preemption + + ; Back to the old memory map + call map_restore + + ; + ; Back on user stack + ; + ld sp, (istack_switched_sp) + +intout: + xor a + ld (U_DATA__U_ININTERRUPT), a + + ld hl, #intret + push hl + reti ; We have now 'left' the interrupt + ; and the controllers have seen the + ; reti M1 cycle. However we still + ; have DI set +intret: + di + ld a, (U_DATA__U_INSYS) + or a + jr nz, interrupt_pop + + ; Loop through any pending signals. These could longjmp out + ; of the handler so ensure everything is fixed before this ! + + call deliver_signals + + ; Then unstack and go. +interrupt_pop: + pop iy + pop ix + pop hl + pop de + pop bc + pop af + exx + pop hl + pop de + pop bc + exx + ex af, af' + pop af + ex af, af' + ei ; Must be instruction before ret + ret ; runs in the ei interrupt shadow + +; +; Pre-emption. We need to get off the interrupt stack, switch task +; and clean up the IRQ state carefully +; + + .globl preemption + +preemption: + xor a + ld (_need_resched), a ; Task done + + ; Back to the old memory map + call map_restore + + ld hl, (istack_switched_sp) + ld (U_DATA__U_SYSCALL_SP), hl + + ld sp, #kstack_top ; We don't pre-empt in a syscall + ; so this is fine + ld hl, #intret2 + push hl + reti ; We have now 'left' the interrupt + ; and the controllers have seen the + ; reti M1 cycle. However we still + ; have DI set + + ; + ; We are now on the syscall stack (which is fine, we don't + ; pre-empt mid syscall so therefore it is free. We will now + ; task switch. The process being pre-empted will disappear into + ; switchout() and whoever is next will come out of the same - + ; hence the need to reti + + ; +intret2:call map_kernel + ; + ; Process status is offset 0 + ; + ld hl, (U_DATA__U_PTAB) + ld (hl), #P_READY + call _switchout + ; + ; We have been rescheduled, remap ourself and go back to user + ; space via signal handling + ; + call map_process_always ; Get our user mapping back + + ; We are no longer in an interrupt + xor a + ld (U_DATA__U_ININTERRUPT), a + + ; We were pre-empted but have now been rescheduled + ; User stack + ld sp, (U_DATA__U_SYSCALL_SP) + ld a, (U_DATA__U_CURSIG) + or a + call nz, deliver_signals_2 + ; + ; pop the stack and go + ; + jr interrupt_pop + +; +; Debugging helpers +; .area _COMMONMEM @@ -565,30 +630,3 @@ _in: .else .include "lowlevel-z80-cmos.s" .endif - -; -; These are needed by the experimental SDCC size optimisations -; - - .globl __enter, __enter_s - -__enter: - pop hl ; return address - push ix ; save frame pointer - ld ix, #0 - add ix, sp ; set ix to the stack frame - jp (hl) ; and return - -__enter_s: - pop hl ; return address - push ix ; save frame pointer - ld ix, #0 - add ix, sp ; frame pointer - ld e, (hl) ; size byte - ld d, #0xFF ; always minus something.. - inc hl - ex de, hl - add hl, sp - ld sp, hl - push de - ret diff --git a/Kernel/process.c b/Kernel/process.c index 6b779a17..25df8fb1 100644 --- a/Kernel/process.c +++ b/Kernel/process.c @@ -1,7 +1,7 @@ #undef DEBUG /* turn this on to enable syscall tracing */ #undef DEBUGHARDER /* report calls to wakeup() that lead nowhere */ #undef DEBUGREALLYHARD /* turn on getproc dumping */ -#define DEBUG_PREEMPT /* debug pre-emption */ +#undef DEBUG_PREEMPT /* debug pre-emption */ #include #include @@ -365,20 +365,16 @@ void timer_interrupt(void) } #ifndef CONFIG_SINGLETASK /* Check run time of current process */ + /* Time to switch out? */ if ((++runticks >= udata.u_ptab->p_priority) - && !udata.u_insys && inint && nready > 1) { /* Time to switch out? */ + && !udata.u_insys && inint && nready > 1) { + need_resched = 1; #ifdef DEBUG_PREEMPT - kprintf("[preempt %x %d]", udata.u_ptab, - udata.u_ptab->p_priority); + kprintf("[preempt %x %d %x]", udata.u_ptab, + udata.u_ptab->p_priority, + *((uint16_t *)0xEAFE)); #endif - udata.u_insys = true; - udata.u_ptab->p_status = P_READY; - switchout(); -#ifdef DEBUG_PREEMPT - kprintf("[preempt return %x]", udata.u_ptab); -#endif - udata.u_insys = false; /* We have switched back in */ - } + } #endif } @@ -386,9 +382,6 @@ void timer_interrupt(void) #include "syscall_name.h" #endif -extern int16_t kernel_flag; /* true when in a syscall etc, maintained by the - asm interfaces but visible in C */ - // Fuzix system call handler // we arrive here from syscall.s with the kernel paged in, using the kernel stack, interrupts enabled. void unix_syscall(void)