How To Build An Operating System: Interrupts and Inputs- Part 05
This is the fifth article of this building an own operating system article series. If you didn’t read the first four articles in this series, I recommend you to read those articles first.
Our operating system can now produce output. So the next step is getting input. To that, the OS must be able to handle interrupts in order to read information from the keyboard.
The idea of the interrupt is at the core of the modern operating system design. By its nature, an operating system is linear and cannot handle more than one computing task at any given time. However, as modern operating systems have become more advanced, engineers and developers have included interrupt functionality to help the operating system handle many different programs by switching in and out of various tasks, with minimal inconvenience to the user. This means modern operating systems can perform many different tasks in ways that do not delay user activity. Now, while a user is viewing or using one program, the operating system can be in the background working on some other task. When the user generates an event that requires the operating system to focus on the primary program that is being used, an interrupt can facilitate a quick response.
A code module characterized as an interrupt handler uses an available queue to prioritize different programs at different times. In some cases, a piece of code called a scheduler is also used. Many different kinds of interrupts enhance the capability of an operating system to provide on-demand services to users while handling more and more additional computing tasks in the background.
What Does Interrupt Do?
An interrupt is a function of an operating system that provides multi-process multi-tasking. The interrupt is a signal that prompts the operating system to stop work on one process and start work on another.
Interrupt Descriptor Table (IDT)
The kernel needs to maintain one IDT (Interrupt Descriptor Table), which actually maps the interrupt line with the interrupt handler routine. This table has 256 entries and each entry has 8 bytes. The first 32 entries of this table are used for exceptions and the remaining are used for hardware interrupts received from the ‘outside world’. This table can contain three different types of entries; these three different types are as follows:
- Task handler
- Interrupt handler
- Trap handler
The difference between an interrupt handler and a trap handler is that the interrupt handler disables interrupts, which means you cannot get an interrupt while at the same time handling an interrupt. In this article series, we will use trap handlers and disable interrupts manually when we need to.
Creating an Entry in the IDT
An entry in the IDT for an interrupt handler consists of 64 bits. The highest 32 bits are shown in the figure below:
Bit: | 31 16 |15| 14 13| 12 | 11| 10 9 8 | 7 6 5| 4 3 2 1 0 |
Content:|offset high|P | DPL | 0 | D | 1 1 0 | 0 0 0| reserved |
The lowest 32 bits are presented in the following figure:
Bit: | 31 16 | 15 0 |
Content: | segment selector | offset low |
A description for each name can be found below:
offset high — The 16 highest bits of the 32-bit address in the segment.
offset low — The 16 lowest bits of the 32 bits address in the segment.
p — If the handler is present in memory or not (1 = present, 0 = not present).
DPL — Descriptor Privilege Level, the privilege level the handler can be called from (0, 1, 2, 3).
D — Size of gate, (1 = 32 bits, 0 = 16 bits).
segment selector — The offset in the GDT.
r — Reserved.
The offset is a pointer to code (preferably an assembly code label). For example, to create an entry for a handler whose code starts at 0xDEADBEEF
and that runs in privilege level 0 (therefore using the same code segment selector as the kernel) the following two bytes would be used:
0xDEAD8E00
0x0008BEEF
If the IDT is represented as an unsigned integer idt[512]
then to register the above example as a handler for interrupt 0 (divide-by-zero), the following code would be used:
idt[0] = 0xDEAD8E00
idt[1] = 0x0008BEEF
We recommend that you instead of using bytes (or unsigned integers) use packed structures to make the code more readable.
Handling an Interrupt
When an interrupt occurs the CPU will push some information about the interrupt onto the stack, then look up the appropriate interrupt handler in the IDT and jump to it. The stack at the time of the interrupt will look like the following:
[esp + 12] eflags
[esp + 8] cs
[esp + 4] eip
[esp] error code?
The reason for the question mark behind the error code is that not all interrupts create an error code. The specific CPU interrupts that put an error code on the stack are 8, 10, 11, 12, 13, 14, and 17. The error code can be used by the interrupt handler to get more information on the interrupt that has taken place. Moreover, Interrupt Number is not pushed onto the stack. So, the interrupt that has occurred can be identified only by knowing the code that is executing. Ex:- if the handler registered for interrupt 14 is executing, then interrupt 14 has occurred.
Once the interrupt handler has done its task, it uses the iret instruction to return. The instruction iret expects the stack to be the same as at the time of the interrupt. Hence, any values pushed onto the stack by the interrupt handler should be popped from the stack. Before returning, iret restores eflags by popping the value from the stack and then finally jumps to cs: eip as specified by the values on the stack.
The interrupt handler has to be written in assembly code since all registers that the interrupt handlers used must be preserved by pushing them onto the stack. This is because the code that was interrupted doesn’t know about the interrupt and will therefore expect that its registers stay the same. Writing all the logic of the interrupt handler in assembly code will be tiresome. Creating a handler in assembly code that saves the registers, calls a C function, restores the registers, and finally executes iret
is a good idea!
The C handler should get the state of the registers, the state of the stack, and the number of the interrupt as arguments. The following definitions can for example be used:
struct cpu_state {
unsigned int eax;
unsigned int ebx;
unsigned int ecx;
.
.
.
unsigned int esp;
} __attribute__((packed)); struct stack_state {
unsigned int error_code;
unsigned int eip;
unsigned int cs;
unsigned int eflags;
} __attribute__((packed));
Now make a file called interrupts.h in your working directory to save the following C function declarations:
Creating a Generic Interrupt Handler
The generic interrupt handling layer is designed to provide a complete abstraction of interrupt handling for device drivers. It is able to handle all the different types of the interrupt controller hardware.
Since the CPU does not push the interrupt number on the stack it is a little tricky to write a generic interrupt handler. This section will use macros to show how it can be done. Writing one version for each interrupt is tedious — it is better to use the macro functionality of NASM. And since not all interrupts produce an error code the value 0 will be added as the “error code” for interrupts without an error code. The following code shows an example of how this can be done:
The common_interrupt_handler
does the following:
- Push the registers on the stack.
- Call the C function
interrupt_handler
. - Pop the registers from the stack.
- Add 8 to
esp
(because of the error code and the interrupt number pushed earlier). - Execute
iret
to return to the interrupted code.
Since the macros declare global labels the addresses of the interrupt handlers can be accessed from C or assembly code when creating the IDT.
Loading the IDT
The IDT is loaded with the lidt
assembly code instruction which takes the address of the first element in the table. It is easiest to wrap this instruction and use it from C:
Programmable Interrupt Controller (PIC)
In computing, a programmable interrupt controller (PIC) is an integrated circuit that helps a microprocessor (or CPU) handle interrupt requests coming from multiple different sources (like external I/O devices) which may occur simultaneously. It helps prioritize IRQs so that the CPU switches execution to the most appropriate interrupt handler (ISR) after the PIC assesses the IRQ’s relative priorities.
The PIC makes it possible to map signals from the hardware to interrupts. The reasons for configuring the PIC are:
- Remap the interrupts. The PIC uses interrupts 0–15 for hardware interrupts by default, which conflicts with the CPU interrupts. Therefore the PIC interrupts must be remapped to another interval.
- Select which interrupts to receive. You probably don’t want to receive interrupts from all devices since you don’t have code that handles these interrupts anyway.
- Set up the correct mode for the PIC.
The following table lists the hardware interrupts:
--------------------------------------------------------------------
PIC 1 | Hardware | PIC 2 | Hardware --------------------------------------------------------------------
0 | Timer | 8 | Real Time Clock
1 | Keyboard | 9 | General I/O
2 | PIC 2 | 10 | General I/O
3 | COM 2 | 11 | General I/O
4 | COM 1 | 12 | General I/O
5 | LPT 2 | 13 | Coprocessor
6 | Floppy disk | 14 | IDE Bus
7 | LPT 1 | 15 | IDE Bus
In the beginning, there was only one PIC (PIC 1) and eight interrupts. As more hardware was added, 8 interrupts were too few. The solution chosen was to chain on another PIC (PIC 2) on the first PIC (see interrupt 2 on PIC 1).
Every interrupts from the PIC has to be acknowledged — that is, sending a message to the PIC confirming that the interrupt has been handled. If this isn’t done the PIC won’t generate any more interrupts.
Acknowledging a PIC interrupt is done by sending the byte 0x20
to the PIC that raised the interrupt. Implementing a pic_acknowledge
function can thus be done as follows:
Reading Input from the Keyboard
The keyboard creates scan codes rather than ASCII characters. A scan code describes a button, both when it is pressed and when it is released. The scan code for the recently pushed button may be retrieved from the data I/O port on the keyboard, which contains the address 0x60. The following example demonstrates how this may be done:
All the things implement by following C code Keyboard. c file:
The next step is to write a function that translates a scan code to the corresponding ASCII character. If you want to map the scan codes to ASCII characters as is done on an American keyboard then Andries Brouwer has a great tutorial.
Remember, since the keyboard interrupt is raised by the PIC, you must call pic_acknowledge
at the end of the keyboard interrupt handler. Also, the keyboard will not send you any more interrupts until you read the scan code from the keyboard.
Hope you understand how to works Interrupts and Input in an operating system. See you in the next article.
Thank You for Reading!
References.