|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Simple OS demo for 6.004 Beta processor -- 4/19/94 Steve Ward |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| ||| This file contains a primitive but complete timesharing kernel ||| sufficient to run three processes, plus handlers for a small ||| selection of supervisor calls (SVCs) to perform OS services. ||| The latter include simple console I/O and semaphores. ||| ||| All kernel code is executed with the Kernel-mode bit of the ||| program counter -- its high-order bit --- set. This causes ||| new interrupt requests to be deferred until the kernel returns ||| to user mode. ||| Interrupt vectors: . = VEC_RESET BR(I_Reset) | on Reset (start-up) . = VEC_II BR(I_IllOp) | on Illegal Instruction (eg SVC) . = VEC_CLK BR(I_Clk) | On clock interrupt . = VEC_KBD BR(I_Kbd) | on Keyboard interrupt ||| The following macro is the first instruction to be entered for each ||| asynchronous I/O interrupt handler. It adjusts XP (the interrupted ||| PC) to account for the instruction skipped due to the pipeline bubble. .macro ENTER_INTERRUPT SUBC(XP,4,XP) |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Kernel Interrupt support code ||| We use a slightly simpler (and less efficient) scheme here from ||| that in the text. On kernel entry, the ENTIRE state -- 31 ||| registers -- of the interrupted program is saved in a designated ||| region of kernel memory ("UserMState", below). This entire state ||| is then restored on return to the interrupted program. |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| | Here's the SAVED STATE of the interrupted process, while we're | processing an interrupt. UserMState: STORAGE(32) | R0-R31... (PC is in XP!) | Here are macros to SAVE and RESTORE state -- 31 registers -- from | the above storage. | N.B. - The following macro assumes that R0 is a macro for | the integer 0, R1 is a macro for the integer 1, etc. .macro SS(R) ST(R, UserMState+(4*R)) | (Auxiliary macro) .macro SAVESTATE() { SS(0) SS(1) SS(2) SS(3) SS(4) SS(5) SS(6) SS(7) SS(8) SS(9) SS(10) SS(11) SS(12) SS(13) SS(14) SS(15) SS(16) SS(17) SS(18) SS(19) SS(20) SS(21) SS(22) SS(23) SS(24) SS(25) SS(26) SS(27) SS(28) SS(29) SS(30) } | See comment for SS(R), above .macro RS(R) LD(UserMState+(4*R), R) | (Auxiliary macro) .macro RESTORESTATE() { RS(0) RS(1) RS(2) RS(3) RS(4) RS(5) RS(6) RS(7) RS(8) RS(9) RS(10) RS(11) RS(12) RS(13) RS(14) RS(15) RS(16) RS(17) RS(18) RS(19) RS(20) RS(21) RS(22) RS(23) RS(24) RS(25) RS(26) RS(27) RS(28) RS(29) RS(30) } KStack: LONG(.+4) | Pointer to ... STORAGE(256) | ... the kernel stack. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Handler for Illegal Instructions ||| (including SVCs) ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| I_IllOp: SAVESTATE() | Save the machine state. LD(KStack, SP) | Install kernel stack pointer. LD(XP, -4, r0) | Fetch the illegal instruction SHRC(r0, 26, r0) | Extract the 6-bit OPCODE SHLC(r0, 2, r0) | Make it a WORD (4-byte) index LD(r0, UUOTbl, r0) | Fetch UUOTbl[OPCODE] JMP(r0) | and dispatch to the UUO handler. .macro UUO(ADR) LONG(ADR+PC_SUPERVISOR) | Auxiliary Macros .macro BAD() UUO(UUOError) UUOTbl: BAD() UUO(SVC_UUO) BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() BAD() ||| Here's the handler for truly unused opcodes (not SVCs): UUOError: CALL(KWrMsg) | Type out an error msg, .text "Illegal instruction " LD(xp, -4, r0) | giving hex instr and location; CALL(KHexPrt) CALL(KWrMsg) .text " at location 0x" MOVE(xp,r0) CALL(KHexPrt) CALL(KWrMsg) .text "! ....." HALT() | Then crash system. ||| Here's the common exit sequence from Kernel interrupt handlers: ||| Restore registers, and jump back to the interrupted user-mode ||| program. I_Rtn: RESTORESTATE() kexit: JMP(XP) | Good place for debugging breakpoint! ||| Alternate return from interrupt handler which BACKS UP PC, ||| and calls the scheduler prior to returning. This causes ||| the trapped SVC to be re-executed when the process is ||| eventually rescheduled... I_Wait: LD(UserMState+(4*30), r0) | Grab XP from saved MState, SUBC(r0, 4, r0) | back it up to point to ST(r0, UserMState+(4*30)) | SVC instruction CALL(Scheduler) | Switch current process, BR(I_Rtn) | and return to (some) user. ||| Sub-handler for SVCs, called from I_IllOp on SVC opcode: SVC_UUO: LD(XP, -4, r0) | The faulting instruction. ANDC(r0,0x7,r0) | Pick out low bits, SHLC(r0,2,r0) | make a word index, LD(r0,SVCTbl,r0) | and fetch the table entry. JMP(r0) SVCTbl: UUO(HaltH) | SVC(0): User-mode HALT instruction UUO(WrMsgH) | SVC(1): Write message UUO(WrChH) | SVC(2): Write Character UUO(GetKeyH) | SVC(3): Get Key UUO(HexPrtH) | SVC(4): Hex Print UUO(WaitH) | SVC(5): Wait(S) ,,, S in R3 UUO(SignalH) | SVC(6): Signal(S), S in R3 UUO(YieldH) | SVC(7): Yield() |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Keyboard handling |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| Key_State: LONG(0) | 1-char keyboard buffer. GetKeyH: | return key code in r0, or block LD(Key_State, r0) BEQ(r0, I_Wait) | on 0, just wait a while | key ready, return it and clear the key buffer LD(Key_State, r0) | Fetch character to return ST(r0,UserMState) | return it in R0. ST(r31, Key_State) | Clear kbd buffer BR(I_Rtn) | and return to user. ||| Interrupt side: read key, store it into buffer. ||| NB: This is a LIGHTWEIGHT interrupt handler, which doesn't ||| do a full state save. It doesn't have to, since (1) it ||| only uses R0, and (2) it always returns to the same process ||| it interrupts. By not saving all state, it manages ||| to save a LOT of time: 20 STs on entry, 30 LDs on exit: I_Kbd: ENTER_INTERRUPT() | Adjust the PC! ST(r0, UserMState) | Save ONLY r0... RDCHAR() | Read the character, ST(r0,Key_State) | save its code. LD(UserMState, r0) | restore r0, and JMP(xp) | and return to the user. WrChH: LD(UserMState,r0) | The user's WRCHAR() | Write out the character, BR(I_Rtn) | then return WrMsgH: LD(UserMState+(4*30), r0) | Fetch interrupted XP, then CALL(KMsgAux) | print text following SVC. ST(r0,UserMState+(4*30)) | Store updated XP. BR(I_Rtn) ||| Handler for HexPrt(): print hex value from R0 HexPrtH: LD(UserMState,r0) | Load user R0 CALL(KHexPrt) | Print it out BR(I_Rtn) | And return to user. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Timesharing: 3-process round-robin scheduler ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| ProcTbl contains a 31-word data structure for each process, ||| including R0-R30. R31, which always contains 0, is omitted. ||| The XP (R30) value stored for each process is the PC, ||| and points to the next instruction to be executed. ||| The kernel variable CurProc always points to the ProcTbl entry ||| corresponding to the "swapped in" process. ProcTbl: STORAGE(29) | Process 0: R0-R28 LONG(P0Stack) | Process 0: SP LONG(P0Start) | Process 0: XP (= PC) STORAGE(29) | Process 1: R0-R28 LONG(P1Stack) | Process 1: SP LONG(P1Start) | Process 1: XP (= PC) STORAGE(29) | Process 2: R0-R28 LONG(P2Stack) | Process 2: SP LONG(P2Start) | Process 2: XP (= PC) CurProc: LONG(ProcTbl) ||| Schedule a new process. ||| Swaps current process out of UserMState, swaps in a new one. Scheduler: PUSH(LP) CMOVE(UserMState, r0) LD(CurProc, r1) CALL(CopyMState) | Copy UserMState -> CurProc LD(CurProc, r0) ADDC(r0, 4*31, r0) | Increment to next process.. CMPLTC(r0,CurProc, r1) | End of ProcTbl? BT(r1, Sched1) | Nope, its OK. CMOVE(ProcTbl, r0) | yup, back to Process 0. Sched1: ST(r0, CurProc) | Here's the new process; ADDC(r31, UserMState, r1) | Swap new process in. CALL(CopyMState) LD(Tics, r0) | Reset TicsLeft counter ST(r0, TicsLeft) | to Tics. POP(LP) JMP(LP) | and return to caller. | Copy a 31-word MState structure from the address in to that in | Trashes r2, leaves r0-r1 unchanged. .macro CM(N) LD(r0, N*4, r2) ST(r2, N*4, r1) | Auxiliary macro CopyMState: CM(0) CM(1) CM(2) CM(3) CM(4) CM(5) CM(6) CM(7) CM(8) CM(9) CM(10) CM(11) CM(12) CM(13) CM(14) CM(15) CM(16) CM(17) CM(18) CM(19) CM(20) CM(21) CM(22) CM(23) CM(24) CM(25) CM(26) CM(27) CM(28) CM(29) CM(30) JMP(LP) ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Clock interrupt handler: Invoke the scheduler. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Here's the deal: ||| Each compute-bound process gets a quantum consisting of TICS clock ||| interrupts, where TICS is the number stored in the variable Tics ||| below. To avoid overhead, we do a full state save only when the ||| clock interrupt will cause a process swap, using the TicsLeft ||| variable as a counter. ||| We do a LIMITED state save (r0 only) in order to free up a register, ||| then count down TicsLeft stored below. When it becomes negative, ||| we do a FULL state save and call the scheduler; otherwise we just ||| return, having burned only a few clock cycles on the interrupt. ||| RECALL that the call to Scheduler sets TicsLeft to Tics, giving ||| the newly-swapped-in process a full quantum. Tics: LONG(2) | Number of clock interrupts/quantum. TicsLeft: LONG(0) | Number of tics left in this quantum I_Clk: ENTER_INTERRUPT() | Adjust the PC! ST(r0, UserMState) | Save R0 ONLY, for now. LD(TicsLeft, r0) | Count down TicsLeft SUBC(r0,1,r0) ST(r0, TicsLeft) | Now there's one left. CMPLTC(r0, 0, r0) | If new value is negative, then BT(r0, DoSwap) | swap processes. LD(UserMState, r0) | Else restore r0, and JMP(XP) | return to same user. DoSwap: LD(UserMState, r0) | Restore r0, so we can do a SAVESTATE() | FULL State save. LD(KStack, SP) | Install kernel stack pointer. CALL(Scheduler) | Swap it out! BR(I_Rtn) | and return to next process. |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| yield() SVC: voluntarily give up rest of time quantum. |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| YieldH: CALL(Scheduler) | Schedule next process, and BR(I_Rtn) | and return to user. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Here on start-up (reset): Begin executing process 0. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| I_Reset: CMOVE(P0Stack, SP) CMOVE(P0Start, XP) JMP(XP) ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| SVC Sub-handler for user-mode HALTs ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| HaltH: BR(I_Wait) | SVC(0): User-mode HALT SVC ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Kernel support for User-mode Semaphores ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| User-mode access: macrodefinitions. Semaphore adr passed in r3, ||| which is saved & restored appropriately by macros: ||| NB: Wait() and Signal() SVCs each pass the address of a semaphore ||| in R3. Since the Illegal Opcode handler code doesn't change any ||| registers except R0, the R3 semaphore address is still intact ||| when we enter these handlers: ||| Kernel handler: wait(s): ||| ADDRESS of semaphore s in r3. WaitH: LD(r3,0,r0) | Fetch semaphore value. BEQ(r0,I_Wait) | If zero, block.. SUBC(r0,1,r0) | else, decrement and return. ST(r0,0,r3) | Store back into semaphore BR(I_Rtn) | and return to user. ||| Kernel handler: signal(s): ||| ADDRESS of semaphore s in r3. SignalH:LD(r3,0,r0) | Fetch semaphore value. ADDC(r0,1,r0) | increment it, ST(r0,0,r3) | Store new semaphore value. BR(I_Rtn) | and return to user. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Kernel-callable Utility Routines ||| NB: These routines use PRIVILEDGED instructions; hence they can be ||| called directly only from kernel code (ie, with the high-PC-bit ||| set). Use SVC traps to accomplish the same functions from user- ||| level code. ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Hex print procedure: prints longword in R0 ||| HexDig: LONG('0') LONG('1') LONG('2') LONG('3') LONG('4') LONG('5') LONG('6') LONG('7') LONG('8') LONG('9') LONG('A') LONG('B') LONG('C') LONG('D') LONG('E') LONG('F') KHexPrt: PUSH(r0) | Saves all regs, incl r0 PUSH(r1) PUSH(r2) PUSH(lp) CMOVE(8, r2) MOVE(r0,r1) KHexPr1: SRAC(r1,28,r0) | Extract digit into r0. MULC(r1, 16, r1) | Next loop, next nybble... ANDC(r0, 0xF, r0) MULC(r0, 4, r0) LD(r0, HexDig, r0) WRCHAR () SUBC(r2,1,r2) BNE(r2,KHexPr1) POP(lp) POP(r2) POP(r1) POP(r0) RTN() |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| Procedure to print out a zero-terminated message, packed one ||| ||| char/byte. Char data follows branch; returns to next 4-byte ||| ||| aligned location. Saves all regs. ||| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| KWrMsg: PUSH (R0) MOVE(LP, R0) CALL(KMsgAux) MOVE(R0, LP) POP (R0) RTN() | Auxiliary routine for sending a message to the console. | On entry, R0 should point to data; on return, R0 holds next | longword aligned location after data. | Note: Must be called while in supervisor mode. KMsgAux: PUSH(r1) PUSH(r2) PUSH(r3) PUSH(r4) MOVE (R0, R1) WrWord: LD (R1, 0, R2) | Fetch a 4-byte word into R2 ADDC (R1, 4, R1) | Increment word pointer CMOVE(4,r3) | Byte/word counter WrByte: ANDC(r2, 0x7F, r0) | Grab next byte -- LOW end first! BEQ(r0, WrEnd) | Zero byte means end of text. WRCHAR() | Print it. SRAC(r2,8,r2) | Shift out this byte SUBC(r3,1,r3) | Count down... done with this word? BNE(r3,WrByte) | Nope, continue. BR(WrWord) | Yup, on to next. WrEnd: MOVE (R1, R0) POP(r4) POP(r3) POP(r2) POP(r1) RTN() ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ||| END OF kernel.uasm |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||