Fundamental concepts
Fundamental principles of how the operating system interacts with the hardware.
Fundamental principles of how the operating system interacts with the hardware.
The central processing unit (CPU) is the electronic circuitry within a computer that carries out the instructions of a computer program by performing the basic arithmetic, logical, control and input/output (I/O) operations specified by the instructions. 1
A processor register is a quickly accessible location available to a computer’s central processing unit (CPU). Registers usually consist of a small amount of fast storage. 2 A CPU only has a small number of registers.
Memory refers to the computer hardware integrated circuits that store information for immediate use in a computer; it is synonymous with the term “primary storage”. 3 The memory is much slower than the CPU register but much larger in size.
A typical computer system consists of a CPU, memory and external devices such as hard drives (disks), keyboard, screens, mouse and printers. The CPU and the device controllers can execute in parallel, competing for memory cycles. To ensure orderly access to the shared memory, a memory controller is provided whose function is to synchronize the access to the memory.
In computing, input/output or I/O (or, informally, io or IO) is the communication between an information processing system, such as a computer, and the outside world, possibly a human or another information processing system. Inputs are the signals or data received by the system and outputs are the signals or data sent from it. The term can also be used as part of an action; to “perform I/O” is to perform an input or output operation. 4
For our purposes, any transfer of information between the CPU/Memory and any of the external devices is considered I/O.
Usually I/O operations does not make use of the CPU but are handled by the external devices.
An operating system (OS) is system software that manages computer hardware and software resources and provides common services for computer programs. 5 The OS controls the hardware and coordinates its use among the various application programs for the various user.
In order to execute a program, the operating system must first create a process and make the process execute the program.
At any point in time, the values of all the registers in the CPU defines the CPU context. Sometimes CPU state is used instead of CPU context.
More generally, a task context is the minimal set of data used by a task (which may be a process or thread) that must be saved to allow a task to be interrupted, and later continued from the same point. 6
The kernel is a computer program that is the core of a computer’s operating system, with complete control over everything in the system. 7
In order to protect the operating system from user processes two modes are provided by the hardware: user mode and kernel mode.
Dual mode operation place restrictions on the type and scope of operations that can be executed by the CPU. This design allows the operating system kernel to execute with more privileges than user application processes.
Synchronous means happening, existing, or arising at precisely the same time. 8 Asynchronous simply means “not synchronous”.
If an event occurs at the same instruction every time the program is executed with the same data and memory allocation, the event is synchronous. An synchronous event is directly related to the instruction currently being executed by the CPU. On the other hand, an asynchronous event is not directly related to the instruction currently being executed by the CPU.
Interrupts and exceptions are used to notify the CPU of events that needs immediate attention during program execution.
Exceptions and interrupts are events that alters the normal sequence of instructions executed by a processor. Such events correspond to electrical signals generated by hardware circuits both inside and outside the CPU chip.
When an exception or interrupt occurs, execution transition from user mode to kernel mode where the exception or interrupt is handled. When the exception or interrupt has been handled execution resumes in user space.
A user program requests service from the operating system using system calls. System calls are implemented using a special system call exception. Another name for exception is trap.
What should be done when an exception or interrupt occurs?
When an exception or interrupt occurs, execution transition from user mode to kernel mode where the exception or interrupt is handled. When the exception or interrupt has been handled execution resumes in user space.
When an exception or interrupt occurs, execution transition from user mode to kernel mode where the exception or interrupt is handled. In detail, the following steps must be taken to handle an exception or interrupts.
While entering the kernel, the context (values of all CPU registers) of the currently executing process must first be saved to memory.
The kernel is now ready to handle the exception/interrupt.
When the exception/interrupt have been handled the kernel performs the following steps:
At any point in time, the values of all the registers in the CPU defines the context of the CPU. Another name used for CPU context is CPU state.
The exception/interrupt handler uses the same CPU as the currently executing process. When entering the exception/interrupt handler, the values in all CPU registers to be used by the exception/interrupt handler must be saved to memory. The saved register values can later restored before resuming execution of the process.
The handler may have been invoked for a number of reasons. The handler thus needs to determine the cause of the exception or interrupt. Information about what caused the exception or interrupt can be stored in dedicated registers or at predefined addresses in memory.
Next, the exception or interrupt needs to be serviced. For instance, if it was a keyboard interrupt, then the key code of the keypress is obtained and stored some where or some other appropriate action is taken. If it was an arithmetic overflow exception, an error message may be printed or the program may be terminated.
The exception/interrupt have now been handled and the kernel. The kernel may choose to resume the same process that was executing prior to handling the exception/interrupt or resume execution of any other process currently in memory.
The context of the CPU can now be restored for the chosen process by reading and restoring all register values from memory.
The process selected to be resumed must be resumed at the same point it was stopped. The address of this instruction was saved by the machine when the interrupt occurred, so it is simply a matter of getting this address and make the CPU continue to execute at this address.
Humans are very slow compared to the CPU. No matter how fast you are, the CPU will be able to execute a huge number of instructions between every key-press you make.
To detect a key-press, one option is to repeatedly check the state of the input device until the device has detected a key press. Usually such a device will use one bit in a special device register to signal whether no input has occurred (bit = 0) or if new input is available (bit = 1).
Polling refers to actively sampling the status of an external device at regular intervals. Polling requires the use of the CPU to check the status of an external device. Polling also refers to the situation where a device is repeatedly checked for readiness, and if it is not the computer returns to a different task between checking the status.
Polling is most often used in terms of input/output (I/O), and is also referred to as polled I/O or software driven I/O.
More generally, polling can be summarized as follows.
If the program continuously polls the device without doing anything in between checks, it’s called a busy-waiting.
Repeatedly checking the status of the input device require the use of the CPU. Using a conditional loop the CPU can repeatedly load the value of the status register to check if new input is available. While waiting for input the CPU doesn’t do any other work, the CPU is busy-waiting.
The term polling is sometimes used synonymously with busy-waiting but you should be aware of the differences between polling and busy-waiting.
Interrupts are used to signal events external to the program (timers, serial ports, keyboard input etc). Interrupts are generated by other hardware devices outside the CPU at arbitrary times with respect to the CPU clock signals and are therefore considered to be asynchronous.
A user program executes in user mode (text segment). When an interrupt happens, control is automatically transferred to the interrupt handler executing in kernel mode (ktext segment).
An alternative to both polling and busy-waiting is to make the input device generate an interrupt every time new input is available. Using interrupts makes it possible for the CPU to do something useful, for example execute another program, while waiting for user input.
In general, I/O operations does not make use of the CPU but are handled by external devices. Compared to the CPU, I/O operations are very slow and while waiting for I/O the CPU is idle doing nothing. To overcome this problem multiprogramming was invented.
In systems using multiprogramming a program loaded to memory and ready to execute is called a job.
The simple idea is to always have one job execute on the CPU by changing job when an I/O request is made.
In a multiprogramming system, a job can be in one of three states.
In a multiprogramming system, a the following state transitions are possible.
From | To | Description |
---|---|---|
Running | Waiting | When a running job requests I/O, the job changes state from running to waiting. |
Running | Ready | When an I/O requests completes, the running job changes state from running to ready. |
Waiting | Ready | When an I/O requests completes, the job waiting for the request to complete changes state from waiting to ready. |
Ready | Running | When an I/O requests completes, one of the ready jobs are selected to run on the CPU and changes state from ready to running. |
The problem is, how will the system know when an I/O request is completed?
To implement multiprogramming interrupts are used to notify the system of important events such as the completion of an I/O request.
In a multiprogramming system, several jobs are kept in memory at the same time. Initially, all jobs are int the ready state.
One of the ready jobs is selected to execute on the CPU and changes state from ready to running. In this example, job 1 is selected to execute.
Eventually, the running job makes a request for I/O and the state changes from running to waiting.
Instead of idle waiting for the I/O request to complete, one of the ready jobs is selected to execute on the CPU and have its state change from ready to running. In this example job 3 is selected to execute.
Eventually the the I/O request job 1 is waiting for will complete and the CPU will be notified by an interrupt. In this example, job 1 was waiting for a keypress on the keyboard.
The state of the waiting job (job 1) will change from waiting to ready.
As an example of how multiprogramming handles I/O requests we will study how to wait for the user to input data from the keyboard.
A user pressing a key on a keyboard is not an internal event, nor is a key-press synchronous, a keypress can happen at any time independent of the executing program.
Normally the operating system is responsible for handling user input and output. When a user program requests service from the operating system this is implemented as system calls.
System calls forms an interface between user programs and the operating system.
We will start by to study how to implement a system call that lets the user input a single character from the keyboard. Next we will study how to implement a system call that lets the user input a string from the keyboard.
Let’s sketch the design for a system call similar to the C
library function getc
that allows a program to read a single single character typed by a human user on
the keyboard.
The below figure shows an example where job 1 calls the custom getc
system
call and job 2 executes while job 1 waits for the user to press a key on
the keyboard.
In the above figure important events are marked with a number inside a yellow circle.
getc
system call to read a character from the
keyboard.getc
system call to complete.The result of the getc
system call can not be obtained immediately, the kernel
must wait for the user to press a key on the keyboard. When handling the getc
system call, the kernel blocks the caller and make another job run until the
user presses a key on the keyboard.
A null-terminated string is a character string stored as an array containing
the characters and terminated with a null character \0
.
1
We have already studied how interrupts can be used to wait for a single keypress from a human user. How can we design a system call for inputting a string?
Where should the input buffer be allocated? In user space or in kernel space?
To enforce memory safety user programs are not allowed to access (read or write) kernel data but the kernel is allowed to read and write data in user space.
The kernel must know the address (pointer) to the input buffer in user space.
The kernel must know the size of the input buffer and decide what to do when the buffer is full.
To better understand the details of how the read string system call can be implemented we will study a small example with two jobs. In this example, job 1 allocates a 3 element input buffer and uses a system call to request the operating system to wait (using interrupts) for the user to fill this buffer with two characters and terminate the buffer with null. Until the input buffer is full the operating system (kernel) let job 2 execute between keyboard interrupts.
A common pattern for system calls and other library functions is for the caller to allocate a struct or array in user space. The caller pass a pointer to the struct or array as an argument to the system call or library function call. The system call or library function writes result data to the struct or array in user space using the provided pointer.
An alternative to the above patterns would be to return data (structure or array elements) on the stack. So why are pointers (call by reference) used instead of returning on the stack?
A MIPS processor consists of an integer processing unit (the CPU) and a collection of coprocessors that perform ancillary tasks or operate on other types of data such as floating-point numbers.
Integer arithmetic and logical operations are executed directly by the CPU. Floating point operations are executed by Coprocessor 1. Coprocessor 0 is used do manage exceptions and interrupts.
Normal user level code doesn’t have access Coprocessor 0, but interrupt and exception aware code has to use it. Coprocessor 0 has several registers which controls exceptions and interrupts.
The coprocess 0 status register is a read-write register used to enable or disable various types of interrupts.
If the interrupt enable bit is 1, interrupts are allowed. If it is 0, they are disabled.
The exception level bit is normally 0, but is set to 1 after an exception occurs. When this bit is 1, interrupts are disabled and the EPC is not updated if another exception occurs. This bit prevents an exception handler from being disturbed by an interrupt or exception, but it should be reset when the handler finishes.
The user mode bit is 0 if the processor is running in kernel mode and 1 if it is running in user mode.
Neither Mars nor Spim implements kernel mode and fixes the user mode bit to 1.
The 8 bit interrupt mask 8 field contains one bit for each of the 6 hardware level interrupts and the 2 software level interrupts. A mask bit set to 1 allows interrupts at that level to interrupt the processor. A mask bit set to 0 disables interrupts at that level.
Hardware interrupt level | Used for | Mars | Spim | ||
---|---|---|---|---|---|
Bit nr. | Bit mask | Bit nr. | Bit mask | ||
0 | Transmitter (terminal output) | 9 | 0x00000200 | 10 | 0x00000400 |
1 | Receiver (keyboard interrupt) | 8 | 0x00000100 | 11 | 0x00000800 |
5 | Timer | Not supported | 15 | 0x00008000 |
On startup the status register has the following value.
Register | MARS | SPIM |
---|---|---|
Status | 0x0000ff11 | 0x3000ff10 |
To see which bits are set in the status register on startup we must translate the hexadecimal startup value to binary.
When translating from hexadecimal to binary we do this by translating each hexadecimal digit to a four bit binary value.
0x0
= [binary] = 0000
0x1
= [binary] = 0001
0xf
= [binary] = 1111
Next, we replace each hexadecimal digit with the corresponding four bit binary number.
Mars initializes the status register in coprocessor 0 to 0x0000ff11
= [binary] = 0000 0000 0000 0000 1111 1111 0001 0001
.
On startup:
The Cause register, is a mostly read-only register whose value is set by the system when an interrupt or exception occurs. It specifies what kind of interrupt or exception just happened.
When an exception or interrupt occur, a code is stored in the cause register as a 5 bit value (bits 2-6). This field is commonly referred to as the exception code although it is used for both exceptions and interrupts.
Exception code | Name | Cause of exception |
---|---|---|
0 | Int | Interrupt (hardware) |
4 | AdEL | Address Error exception (Load or instruction fetch) |
5 | AdES | Address Error exception (Store) |
6 | IBE | Instruction fetch Buss Error |
7 | DBE | Data load or store Buss Error |
8 | Sys | Syscall exception |
9 | Bp | Breakpoint exception |
10 | RI | Reversed Instruction exception |
11 | CpU | Coprocessor Unimplemented |
12 | Ov | Arithmetic Overflow exception |
13 | Tr | Trap |
14 | FPE | Floating Point Exception |
Exception Program Counter (EPC) register. When an interrupt or exception occurs, the address of the currently executing instruction is copied from the Program Counter (PC) to EPC. This is the address that your handler jumps back to when it finishes.
In SPIM, a timer is simulated with two more coprocessor registers: Count (register 9), whose value is continuously incremented by the hardware, and Compare (register 11), whose value can be set. When Count and Compare are equal, an interrupt is raised, at Cause register bit 15.
To schedule a timer interrupt, a fixed amount called the time slice (quantum) is stored in the Compare register. The smaller the time slice, the greater the frequency of timer interrupts.
Timer interrupts are yet not implemented in MARS.
To access a register in Coprocessor the register value must be transferred from Coprocessor 0 to one of the regular CPU registers. To update one of the registers in Coprocessor 0 a value in one of the of the regular CPU register in the CPU must be transferred to to one of the Coprocessor 0 registers.
If you want to modify a value in a Coprocessor 0 register, you need to move the
register’s value to a general- purpose register with mfc0
, modify the value
there, and move the changed value back with mtc0
.
The mfc0
(Move From Coprocessor 0) instruction moves a value from a Coprocessor 0 register to a general-purpose register.
Example:
mfc0 $t5, $13 # Copy $13 (cause) from coprocessor 0 to $t5.
The mtc0
(Move To Coprocessor 0) instruction moves a value from a
general-purpose register to a Coprocessor 0 register.
Example:
mtc0 $v0, $12 # Copy $v0 to $12 (status) in coprocessor 0.
In order to study exceptions and interrupts we will use the Mips 32 simulator Mars. All instructions will be written for and tested using Mars but you should be able to use the Spim simulator if you prefer.
A Mips processor consists of an integer processing unit (the CPU) and a collection of coprocessors that perform ancillary tasks or operate on other types of data such as floating-point numbers.
Both Mars and Spim simulates two coprocessors:
Integer arithmetic and logical operations are executed directly by the CPU. Floating point operations are executed by Coprocessor 1. Coprocessor 0 are used do manage exceptions and interrupts.
Mars simulates one I/O device: a memory-mapped console on which a program can read and write characters. The terminal device consists of two independent units: a receiver and a transmitter.
The receiver and transmitter are completely independent. This means, for example, that characters typed at the keyboard are not automatically echoed on the display. Instead, a program must explicit echo a character by reading it from the receiver and writing it to the transmitter.
To execute a Mips program memory must be allocated. The Mips computer can
address 4 Gbyte of memory, from address 0x0000 0000
to 0xffff ffff
. User memory
is limited to locations below 0x7fff ffff
. In the below figure the layout of the
memory allocated to a Mips program is shown.
The purpose of the various memory segments:
A program controls the terminal with four memory-mapped device registers. Memory-mapped means that each register appears as a special memory location. In this sense, these are not real registers but simply words (32 bit) values stored in memory.
The Receiver Control register is at memory location 0xffff0000
. Only two of its
bits are actually used.
Bit 0 is called ready:
Bit 1 is the keyboard interrupt enable bit:
The second terminal device register is the Receiver Data register at memory
address 0xffff0004
.
The third terminal device register is the Transmitter Control register at memory
address 0xffff0008
. Only the low-order 2 bits of this register are used. They
behave much like the corresponding bits of the Receiver Control register.
Bit 0 is called ready:
Bit 1 is the interrupt enable bit:
The final device register is the Transmitter Data register at memory address
0xffff000c
.
Only the least 8 significant bits (the least significant byte) are used:
In a physical computer sending a character to the terminal doesn’t happen instantly, the operation takes some time to complete.
The simulators Mars and SPIM measures time by counting the number executed instructions , not in real clock time. After writing a character to the receiver data register, this means that the transmitter does not become ready again until the processor executes a fixed number of instructions. If you stop the simulation and look at the ready bit, it will not change. However, if you let the simulation continue, the bit eventually changes back to 1.
Before you continue, you must clone the fundamental-os-concepts repository.
From the terminal, navigate to a directory where you want the cloned directory to be created and execute the following command.
git clone https://github.com/os-assignments/fundamental-os-concepts.git
Now you should see something similar to this in the terminal.
Cloning into 'fundamental-os-concepts1'...
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 7 (delta 0), reused 7 (delta 0), pack-reused 0
Unpacking objects: 100% (7/7), done.
To get an overview of the cloned repository, use the tree
command.
tree fundamental-os-concepts
Now you should see a tree view of all files and directories in the
fundamental-os-concepts
directory.
fundamental-os-concepts
├── README.md
├── higher-grade
│ └── multiprogramming.s
└── mandatory
└── exceptions-and-interrupts.s
2 directories, 3 files
If you run macOS and tree
is not installed, use Homebrew to install tree
.
brew install tree
In this assignment you will study the differences between exceptions and interrupts and how to implement a simple exception and interrupt handler. You will also study how both exceptions and interrupts causes a transfer of control from user mode to kernel mode and back to user mode after the exception or interrupt have been handled by the kernel.
To study exception and interrupt handling you will load a small Mips assembly program into the Mips simulator Mars. The program will deliberately trigger the following exceptions:
By single-stepping the program you will examine in detail what actions are taken in order to handle each exception. You will also study keyboard interrupts and how this can be used make the CPU do something different while waiting for user input. To get a fully working system you must add or change the provided code at a few places.
Before you continue you must perform the following preparations.
If you haven’t done so already, go through the introduction to Mips and Mars.
Mars will run on any system (including Windows) as long as you have [Java installed][java-install]. If you prefer, you may download and install Mars on your private computer.
If you haven’t done so already, you must clone the fundamental-os-concepts repository.
If you don’t have Mars installed on your private computer, you can log in to the department Linux system and start Mars from the Applications menu under Programming.
Mars should now start and you should see something similar to this.
Open the file mandatory/exceptions-and-interrupts.s
in Mars.
Study the assembly source code of the loaded program in the built in editor pane.
Read the code with the intention of
getting an overview of the overall structure. Focus on labels and jumps to
labels. Focus on the difference between the user text segment (.text
) and
kernel text segment (.ktext
). You will study the details later.
You should not edit the source code at this stage.
After an introductory comment you find the .text
assembler directive followed by
the label main
which is the entry point of the user mode program. In main:
1
to the
largest positive 32 bit two’s complement value 0x7fffffff
.teqi
(Trap EQual Immediate) instruction.At the end of main
the program enters an infinite loop incrementing a counter
(register $s0
).
The assembler directive .ktext 0x80000180
instructs the assembler to place the
generated machine instructions in the kernel text segment starting at memory
address 0x80000180
. Here the label __kernel_entry_point
marks the entry point
of the exception handler (kernel). Note that this label is not needed but simply
included to make it obvious to a human reader where the exception handler
starts.
When an exception or interrupt occurs, the address of the program counter of the currently executing program is automatically saved to the EPC register in coprocessor 0 and control is transferred from user mode to kernel mode.
When entering the kernel, the kernel must determine whether this due to an an exception or an interrupt.
__interrupt
for an interrupt and at label __exception
for an exception.For an exception, the exception code must be further examined to distinguish
between different exceptions. For interrupts the pending interrupt bits in the
cause register is used to distinguish between different interrupts. At the end
of the kernel text segment at label __resume
, execution is resumed in user
mode at the address saved in the EPC register in coprocessor 0.
Adjust the simulated run speed to 25 inst/sec or lower.
Before you continue, clear both the Mars Messages and Run I/O.
You should see something similar to the following in the Mars Messages display pane.
Assemble: assembling /path/to/file/exceptions-and-interrupts.s
Assemble: operation completed successfully.
In the Run I/O display window you should see the following output.
===> Arithmetic overflow <===
===> Arithmetic overflow <===
===> Arithmetic overflow <===
===> Arithmetic overflow <===
The same error message is printed several times.
Spend some time to see if you can come up with an explanation as to why the same error message printed over and over again.
Before you continue, clear both the Mars Messages and Run I/O.
In the Execute pane the source instructions are shown in the Source column and
the actual instructions produced by the assembler are shown in the Basic column.
Note that the first source instruction li $s0, 0x7fffffff
is a pseudo
instruction that is translated to one lui
instruction and one ori
instruction,
both using the $at
(Assembler Temporary) register, i.e., register $1
.
It is now time to study the execution in more detail by execute one instruction at the time using the single step button .
The program starts by storing the largest 32 bit
positive two’s complement
integer in register $s0
using the li
(Load Immediate) instruction. This instruction is a pseudo
instruction and translates to one lui
instruction and one ori
instruction.
li $s0, 0x7fffffff
instruction.Now, look at the register pane.
Note that register $at
(register number 1) have been highlighted and that the value stored in $at
changed from the initial value 0x00000000
to 0x7fff0000
, i.e., the upper
half of the 32 bit value 0x7fffffff
is now stored in $at
.
ori $16, $1, 0x000ffff
instruction.Now $s0
(register number 4) will be highlighted in the register pane.
Register $s0
now holds the value value 0x7fffffff
= [32 bit binary] = 0111 1111 1111 1111 1111 1111 1111 1111
, i.e., the largest positive 32 bit
two’s two’s complement
integer.
The program counter stores the address of the next instruction to execute.
In the register pane, look at the value of the program counter pc
. We see that
pc
= 0x00400008
. Also note that in the execute pane the instruction at this
address is now highlighted.
In the Coproc 0 tab in the register pane, look at the cause register (register 13).
The value in the cause register is currently 0x00000000
.
We will now try to add the value one (1) to the integer stored in $s0
.
addi $s1, $s0, 1
instruction.The program counter have now jumped from 0x00400008
to 0x80000180
, i.e.,
execution has transition to the kernel entry point. Now the status
register (register 12) is highlighted in the register pane.
Note that the cause register changed from 0x00000000
to 0x00000030
and
that EPC have been set to
0x00400008
, i.e., been set to the address of the addi
instruction causing
the overflow exception.
One great feature of the Mars simulator is the possibility to execute the program backwards.
addi $s1, $s0, 1
instruction by clicking on the undo
button.Study the values of the program counter, the cause register and the EPC register.
addi $s1, $s0, 1
instruction.Keep undo and redo the execution of the addi
instruction and make sure you
understand that this addition causes a transfer of control from user mode to
kernel mode due to an overflow exception and that information about the
exception is stored in the cause register. When the exception happens, the
address of the faulty instruction is automatically saved in the EPC register.
As a consequence of the overflow exception, execution has now transition from
user mode to kernel mode. Execution now continues in the .ktext
segment at the
label __kernel_entry_point
at address 0x80000180
.
To investigate what caused the transfer from user mode to kernel mode, the kernel must fetch the value of the cause register from coprocessor 0.
mfc0 $k0, $13
instruction.In the register pane register $k0
should now be highlighted with value
0x00000030
= [binary] = 0000 0000 0000 0000 0000 0000 0011 0000
, i.e, a copy of the cause register.
The bits of the cause registers have different meaning.
In general, we can’t be sure if other bits than the exception code bits (bits
2 - 6) are set in the cause register. To set all bits but the exception code
bits (bits 2 -6) to zero, the bitmask 0x0000007C
= [binary] = 0000 0000 0000 0000 0000 0000 0111 1100
is used together with bitwise and (andi).
andi $k1, $k0, 0x00007c
instruction.In the register pane, register $k1
should now have value 0x00000030
=
[binary] = 0000 0000 0000 0000 0000 0000 0011 0000
.
To get the value of the exception code we need to shift the value in $k1
two
steps to the right.
srl $k1, $k1, 2
instruction.Register $k1
now hold the exception code = 0x0000000c
=
[binary] = 0000 0000 0000 0000 0000 0000 0000 1100
= [decimal] = 12
.
The exception code is zero for an interrupt and none zero for all exceptions.
The beqz
instruction is now used to jump to the label __interrupt
if the
exception code in $k1
is zero, otherwise execution continues on the next instruction.
beqz $k1, __interrupt
instruction.The exception code is non zero and the branch is not taken.
Next the beq
instruction is used to make a conditional jump to the label
__overflow_exception
if the exception code in $k1
is equal to 12
.
beq $k1, 12, __overflow_exception
by clicking twice on step the step forward button.The branch is taken and execution continues at label __overflow_exception
.
Next a “magic” Mars builtin system call is used to print the error message
"===> Arithmetic overflow <===\n\n"
stored as a null terminated string in
the .kdata
segment at label OVERFLOW_EXCEPTION
.
Unfortunately the built-in system calls in Mars are implemented as part of the underlying Mips emulator. You cannot single-step the built-in system calls to see how they are implemented. Hence they provide us with a little magic that we cannot study or modify.
In the Run I/O pane you should now see the following message.
===> Arithmetic overflow <===
Next an unconditional jump to label __resume_from_exception
is done.
j __resume_from_exception
instruction.Execution now continues at the label __resume_from_exception
. Next the value
of the EPC register is fetched from the coprocessor 0 register $14
to the
CPU register $k0
.
mfc0 $k0, $14
instruction.Register $k0
now have the value 0x00400008
. This is the address that was
automatically stored in EPC when the overflow exception occurred, i.e., the
address of the instruction causing the overflow exception.
Next, the value in the $k0
register is stored back to the EPC register in
coprocessor 0.
mtc0 $k0, $14
instruction.The exception have now been handled by the kernel. The last thing to be done is
to transfer control back to user mode using the eret
instruction which makes
and unconditional jump to the address currently stored in EPC.
eret
instruction.Execution now continues in user mode at the same instruction that caused the overflow exception in the first place.
In the Run I/O display window you should see the following output.
===> Arithmetic overflow <===
===> Arithmetic overflow <===
===> Arithmetic overflow <===
===> Arithmetic overflow <===
The same message is printed repeatedly in the Run I/O display.
When resuming execution after an exception, we want to resume at the instruction
following the instruction at the address saved in EPC.
Each instruction is four bytes, hence we need to add four to EPC before
executing eret
.
# addi $k0, $k0, 4 # TODO: Uncomment this instruction
Before you continue, clear both the Mars Messages and Run I/O.
In the Run I/O display window you should see the following output.
===> Arithmetic overflow <===
===> Unhandled exception <===
===> Unhandled exception <===
This time there is exactly one arithmetic overflow error message followed by two messages about unhandled exceptions.
The program is now stuck in the infinite loop at label infinite_loop
. In the
register pane you should be able to see how the value of register $s0
is
constantly increasing.
At label todo_2
you must add code to:
__bad_address_exception
for exception code 4.__trap__exception
for exception code 13.Before you continue, clear both the Mars Messages and Run I/O.
In the Run I/O display window you should see the following output.
===> Arithmetic overflow <===
===> Bad data address exception <===
===> Trap exception <===
This time you should not halt the simulation, instead you should pause the simulation.
We will now make the keyboard generate an interrupt for each keypress. In order to do this we must first setup the Mars MMIO simulator.
To make MARS simulate the memory mapped keyboard receiver (and display transmitter) you must enable this feature.
From the Tools menu, select Keyboard and Display MMIO Simulator.
A new window should now open.
The lower white area of this window is the simulated keyboard.
To make MARS aware of the simulated memory mapped receiver (keyboard), press the Connect to MIPS button in the lower left corner of the Keyboard and Display MMIO Simulator window.
Now you can resume the simulation.
The program is now stuck in the infinite loop at label infinite_loop
. In the
register pane you should be able to see how the value of register $s0
is
constantly increasing.
Click inside the lower white area of the MMIO simulator window and type a few characters.
Nothing happens, the program is still stuck in the infinite loop.
Before you continue, make sure to stop the simulation.
At label todo_3
you must add code to enable keyboard interrupts. The simulated
keyboard is configured by setting bits in the memory mapped transmitter control
register which appears at address 0xffff0000
.
To make the keyboard generate interrupts on keypresses, the bit 1 of receiver control must be set to 1.
Click inside the lower white area of the MMIO simulator window and type a single character.
When you type a character on the simulated keyboard a keyboard interrupt is generated. The interrupt is handled by the kernel.
Adjust the run speed to a slower speed in order to see how the asynchronous keyboard interrupt causes control to be transferred from the user level infinite loop to the kernel where the interrupt is handles and then back to the user level infinite loop.
There is a conflict between the built-in system call 12 read_char
and the
Keyboard and Display MMIO Simulator.
If the Keyboard and Display MMIO Simulator is open and connected, Mars
hangs if the user types a character on the simulated keyboard while Mars
waits for input using the built-in system call 12 read_char
.
The ASCII value of the pressed key is stored in the memory mapped receiver data register.
At label todo_4
you must uncomment a number of instructions to load the
ASCII value from receiver control and print it to Run I/O using the Mars builtin
system cal.
Click on the label __todo_4
in the labels window.
The instruction at label __todo_4
is now highlighted in the Execute pane. Add
a breakpoint at this address by checking the checkbox in the leftmost
Bkpt column.
Once again, execution is stuck in the infinite loop incrementing the $s0
register.
Click inside the lower white area of the MMIO simulator window and type a single character.
When you type a character on the simulated keyboard a keyboard interrupt is generated. The interrupt is handled by the kernel and execution is paused at the breakpoint.
Continue by single stepping and try to understand how the keyboard interrupt is handled by the kernel and execution resumes in the user mode infinite loop after the keyboard interrupt has been handled.
Once the keyboard interrupt have been handled you should see the pressed character printed to Run I/O display.
Now you can press play again, press a key on the simulated keyboard and single step after the breakpoint. Repeat a few times to make sure you understand how the keyboard interrupt is handled.
Interrupts and exceptions are used to notify the CPU of events that needs immediate attention during program execution. Exceptions and interrupts are events that alters the normal sequence of instructions executed by a processor.
This assignments is constructed more or less as a step-by-step tutorial. The purpose of the code grading is not for you (the student) to show the teaching staff that you got something to work as required. The focus is for you to be able to explain and discuss what you’ve done and what you learnt. In preparation for the code grading:
Previously you have used a number
of system calls built in to MARS, for example to print
strings and integers. Before calling any of the Mars built-in system calls, the system call code is placed
in $v0
. Next, the syscall
instruction is used to invoke the system call.
In a real Mips system, the syscall
instruction triggers a system call
exception (exception code 8) that causes control to be transferred from user
space to kernel space where the system call is handled. The kernel investigates
the value in $v0
to determine which specific system call the user requested.
In Mars the built-in system calls are handled by the simulator and not by the
kernel (exception and interrupt handler) code that you can study and modify. Unfortunately
this makes it impossible to provide custom implementations of system calls using
the syscall
instruction.
After all, this is a course about operating systems and we cannot be satisfied with the magic provided by the system calls built in to Mars. It is now time to implement our own custom system calls from scratch.
Unfortunately, Mars does not support timer interrupts. This makes it impossible for the kernel to to use a timer to swap between running jobs at regular intervals.
For the higher grade assignment you should implement a simplified version of multiprogramming.
An overview of the system is shown in the below figure.
For simplicity there will be exactly two jobs in the system. The kernel will implement three custom system calls: getjid
, getc
and
gets
. The kernel will use multiprogramming to switch job only when a job
requests I/O using the getc
or gets
system call.
To make the system as simple as possible, the kernel only need to support two non-terminating jobs. Only one job can execute on the CPU at the time.
The two jobs are identified by job id (jid
) numbers 0
and 1
respectively.
To keep the system simple the jobs cannot use the stack nor can they make subroutine calls.
Mars does not support any form of memory protection. As a consequence there will be no memory protection between user space and kernel, nor will there be any memory protection between jobs. Thetwo jobs:
The whole purpose of multiprogramming is to maximize the usage of the CPU. A job executes on the CPU until requesting I/O. When requesting I/O the job is blocked waiting for the I/O request to complete. While waiting the other job executes on the CPU.
In this simplified system, only one of the two non-terminating jobs can request I/O. While a job is waiting for the I/O request to complete the other job executes on the CPU.
At any time, each of the two non-terminating jobs job can be in one of the following three states.
getc
or gets
and s blocked
from executing until the the request have been completed.On startup of the system (boot) one job is selected to execute (running) and the other is marked as ready to run.
In this simplified version of a a multiprogramming system with only two jobs, the following state transitions are possible.
From | To | Description |
---|---|---|
Running | Waiting | When the running job requests I/O, the job changes state from running to waiting. |
Ready | Running | When the running job requests I/O, the other job changes state from ready to running. |
Running | Ready | When an I/O requests completes, the running job changes state from running to ready. |
Waiting | Running | When an I/O requests completes, the job waiting for the request to complete changes state from waiting to running. |
In the below diagram a sequence of five events shows how the two jobs change states.
In this system the kernel is responsible for handling all exceptions and interrupts. The kernel will also be responsible for managing the execution of the two jobs.
In order for the kernel to be able to use subroutines, the the kernel will allocate a stack.
When a job executes it uses the CPU registers. At any time, the values of all the registers is called the context of the running job.
In this simplified system each job is only allowed to use the following six
registers: $pc
(program counter), $v0
, $a0
, $a1
, $s0
and $at
(should only
be used by pseudo instructions).
Each register is four byte, hence 6*4 = 24 bytes of storage is needed to store the context. The contents of the registers are saved in the following order in each job context.
If the address to a context is in register $t0
, the address to each of the
fields in the context is shown in the column Assembly notation in the
following table.
Field | Offset | Address | Assembly notation |
---|---|---|---|
$pc | 0 | $t0 + 0 | 0($t0) |
$v0 | 4 | $t0 + 4 | 4($t0) |
$a0 | 8 | $t0 + 8 | 8($t0) |
$a1 | 12 | $t0 + 12 | 12($t0) |
$s0 | 16 | $t0 + 16 | 16($t0) |
In the following examples, the address to a context is in register $t0
.
sw $t1, 8($t0) # Store $t1 to the $a0 field in the context.
lw $t2, 16($t0) # Load the $s0 field from the context into $t2.
To store the contexts of the two jobs, a two element array is used. The array is indexed by job id (0 or 1). Each element of the array is a pointer to storage allocated for the context.
The two element context array is indexed by job id (0
or 1
). Each element of
the context array is a pointer to a job context, i.e., a four byte address to a
job context.
If ca
is the address of the context array and jid
the job id, the following table shows
how to calculate the address to the context pointer the job with id jid
.
Job id (jid ) | Offset to context pointer | Address of context pointer |
---|---|---|
0 | 0 == jid * 4 | ca + jid * 4 |
1 | 4 == jid * 4 | ca + jid * 4 |
Multiplying with 4 is equivalent to shift left two bits. In Mips assembly the
sll
(Shif Left Logical) instruction is used to bit shift to the left.
If the context array is stored at label __context_array
and the job id (jid
)
the of the running job is stored at label __running
the following example shows how to
get the address to the context of the running job.
# Get job id (jid) of the running job.
lw $t0, __running
# Offset to context pointer (0 or 4) = jid * 4.
sll $t1, $t0, 2 # jid * 4
# Pointer to context.
lw $t3, __context_array($t1)
Now $3
contains the address of the running jobs context and can be used to
access fields within the context as described in the
section addressing the context.
When entering the kernel, the context of the running job must be saved. i.e., the
contents of $pc
, $v0
, $a0
, $a1
, $s0
and $at
must be saved to memory.
When the kernel resume the execution of a job, the job context is first
restored, i.e., the values of $pc
, $v0
, $a0
, $a1
, $s0
and $at
that
previously been saved to memory are read from memory and restored.
The kernel will implement the following system calls.
System call code | Name | Description | Arguments | Return value |
---|---|---|---|---|
0 | getjid | Get job id of the caller | $a0 - job id of the caller | |
12 | getc | Read a single character from the keyboard | $v0 - ASCII code of pressed key | |
8 | gets | Read a string of characters from the keyboard | $a0 = buffer, $a1 = buffer size |
The teqi
(Trap EQual Immediate) instruction is used to conditionally trigger a
trap exception (exception code 13) that automatically transfers control to the kernel.
teqi rs, imm
The teqi
instruction will cause a trap exception (exception code 13) if the value in
register rs
is equal to the immediate constant imm
.
**Example usage: **To trigger a trap exception we can compare $zero
(allways
zero) with zero.
teqi $zero, 0
Instead of using the syscall
instruction we use teqi $zero, 0
when we
want to perform one of the custom system calls.
An example showing how to use the custom getjid
system call to get the job id
of the caller:
li $v0, 0
teqi $zero, 0
# On success $a0 will now contain the job id (jid) of the caller.
The below figure shows an example where job 1 calls the custom getjid
system call.
In the above figure important events are marked with a number inside a yellow circle.
getc
system call to read a character from the
keyboard.$k0
an $k1
the kernel can now safely use any of the registers saved to
the job 1 context.$a0
.An example showing how to use the custom getc
system call to read a character
from the keyboard.
li $v0, 12
teqi $zero, 0
# On succes $v0 will now contain the ASCII code of the pressed key.
The below figure shows an example where job 1 calls the custom getc
system
call and job 2 executes while job 1 waits for the user to press a key on
the keyboard.
In the above figure important events are marked with a number inside a yellow circle.
getc
system call to read a character from the
keyboard.getc
system call to complete.$v0
.$v0
.There is a conflict between the built-in system call 12 read_char
and the
Keyboard and Display MMIO Simulator.
If the Keyboard and Display MMIO Simulator is open and connected, Mars
hangs if the user types a character on the simulated keyboard while Mars
waits for input using the built-in system call 12 read_char
.
If you haven’t done so already, you must clone the fundamental-os-concepts repository.
You don’t have to start from scratch but can use the provided
higher-grade/multiprogramming.s
as a starting point.
For 1 point your system must have working implementations of the custom getjid
and
getc
system calls. Follow the labels TODO_1
,
TODO_2
, …, TODO_8
in the provided multiprogramming.s
file to guide you
step by step.
For 3 points, in addition to the 1 point requirements above, your system must have a working implementation of the custom gets
system call. You must also provide a user level job with a few test cases
showing that gets
works for different buffer sizes.