Series catalog
- Preface
- Preparation work
- BIOS boot to real mode
- GDT and protected mode
- A Preliminary Study of Virtual Memory
- load and enter the kernel
- Display and print
- Global Descriptor Table GDT
- Interrupt handling
- virtual memory perfection
- implements heap and malloc
- first kernel thread
- Multi-thread switching
- lock and multi-thread synchronization
- enter user mode
- process
- system call
- Simple file system
- Load executable program
- keyboard driver
- Run shell
Interrupt
Interrupts play a very important role in the CPU. The response to hardware, task switching, and exception handling are all inseparable from interrupts. It is not only the source of power that drives everything, but also the source of all evil that brings us all kinds of pain.
Interruption-related issues will run through the development of the entire kernel. Its difficulty lies in its disruption of the code execution flow and its unpredictability. This article is just a preliminary framework for interrupt handling, and it will always follow you in the future, and it is also a touchstone for testing the kernel design and implementation.
Concept preparation
There are often some confusions about the literal concepts of interrupts, exceptions, hard interrupts, and soft interrupts, and the use of these terms in Chinese and English is also somewhat inconsistent. In order to unify the understanding, we make a statement on the use of the following terms:
- interrupt , this term is used as a general concept, that is, it includes various types of interrupts and exceptions;
Then, under the general concept of interruption, the following classifications are made:
- Exception (
exception
): Internal interrupt, which is an error encountered during the internal execution of the CPU. In English, it hasexception
,fault
,trap
and other types. We generallyexception
them as 0611539d99f062; such problems are generally non-maskable and must Processed - Hard interrupt (
interrupt
): External interrupts are generally sent by other hardware devices, such as clocks, hard disks, keyboards, network cards, etc. They can be shielded; - Soft interrupt (
soft int
): Strictly speaking, this is not an interrupt, because it isint
instruction. The most commonly used is system call, which is the way users actively request to enter the kernel state; its processing mechanism is the same as other interrupts. , So it is also included in the interrupt;
Later, we will use the English word exception
to refer to the first category, that is, CPU internal exceptions and errors, which is not ambiguous; and interrupt
used to specifically refer to the second category, namely hard interrupts, which is also the original usage on the Intel documentation; As for the third category, you can ignore it first, because we don’t need to discuss it yet;
As for interrupt , we use it to refer to all the types mentioned above. It is a big concept. Note that we do not equate interrupt
Note that this is purely my personal usage and regulation, just to facilitate the unification of the expression and understanding of the following terms.
Interrupt descriptor table
The reason why interrupt causes ambiguity, I think it may be because the processing functions of all the above things are placed in the interrupt descriptor table IDT ( Interrupt Descriptor Table
) management, which leads to the word interrupt exceeds the interrupt
The category itself includes exception
This is why I want to use the Chinese word interrupt to represent the overall concept, and use English interrupt
and exception
represent the two sub-concepts below it.
IDT table entry
Back to the interrupt descriptor table IDT
, its main function is to define various interrupt handler functions. The structure of each entry is defined as follows:
struct idt_entry_struct {
// the lower 16 bits of the handler address
uint16 handler_addr_low;
// kernel segment selector
uint16 sel;
// this must always be zero
uint8 always0;
// attribute flags
uint8 attrs;
// The upper 16 bits of the handler address
uint16 handler_addr_high;
} __attribute__((packed));
typedef struct idt_entry_struct idt_entry_t;
The code is linked at src/interrupt/interrupt.h , for IDT documentation, please refer to here .
For the specific meaning of each field, please refer to the above document. Here are two of the more important fields:
sel
: Thatselector
, where the provisions of this interrupt handler is locatedsegment
. After entering the interrupt processing, the CPU'scode
segment register,cs
, is replaced with this value, so this selector must point tokernel
ofcode segment
;DPL
: This isattrs
field. It specifies the lowest privilege level of the CPU required to be able to call or enter this handler; for us, it must be set to privilege level 3 or user level, otherwise In user mode, the interrupt cannot be entered;
Of course, we are missing one of the most important parts in the IDT entry above, which is the address of the interrupt handler, which will be discussed later.
Build IDT
Then you can define the IDT structure, the code is src/interrupt/interrupt.c :
static idt_entry_t idt_entries[256];
There are 256 entries reserved here, which is more than enough to meet our needs.
The first 0 to 31 items are reserved for exception
. Starting from item 32, it is used as the interrupt
processing function. Each interrupt will have an interrupt number, which corresponds to the first item in the IDT table. The CPU finds the interrupt handler based on this and jumps to it.
Among them, exception
is the following, I took the picture wiki
Among them, the 14th page faualt
, that is, the page fault exception, we will focus on processing in the next virtual memory improvement. Other exception
we don’t need to pay attention at the moment, because they shouldn’t appear under normal circumstances.
IDT starts with item 32 for interrupt
, for example, item 32 is for clock ( timer
) interrupt
.
Interrupt handler
Let's return to the interrupt handling function mentioned above, or interrupt handler
, whose address is written in each entry of the above IDT. They do not currently exist, so we need to define these functions.
Each interrupt handler is of course different, but before and after entering and leaving these handlers, there are some common tasks that need to be done, that is, saving and restoring the scene before the interruption occurs, or context ( context
), which mainly includes various register
, they will be stored in stack
. Therefore, the interrupt processing process is similar to this:
save_context();
handler();
restore_context();
It is worth noting that context
is done by the CPU and us together, that is, the CPU will automatically push a part of the register into the stack, and we will also push a part of the register and other information into the stack as needed. These two parts together form the interrupted context
.
Interrupt CPU push
stack
's look at the registers that the CPU automatically pushes in 0611539d99f474:
There are two situations here:
- If the interrupt is entered from the kernel state, only the three values on the left will be pressed;
- If it is an interrupt entered from the user mode, the CPU will push the five values in the figure on the right;
The difference between the two is the user ss and user esp at the top.
We can take a look at the internal logic here: the purpose of what the CPU does is very clear, it saves execution flow , which contains two core elements:
- Before the interrupt occurred
instruction
position (cs
andeip
provided); - Before the interrupt occurred
stack
position (ss
andesp
provided);
ss
and esp
only when the privilege level transition occurs (the user mode enters the kernel mode)? Because the user mode code execution and the kernel
code are stack
, the interrupt processing from the user mode needs to be converted to the kernel stack; after the interrupt processing is completed, it will return to the user stack, so the user needs to be The stack information is saved.
The following figure shows the jump of stack
when an interrupt occurs in user mode:
If the interrupt occurs in the kernel state, then the situation will become much simpler, because the interrupt processing is still executed in the same stack in the kernel stack, so its situation is a bit like an ordinary function call (of course Slightly different):
We see that the CPU is only responsible for saving the instruction
and stack
, but not the registers related to data. This mainly includes several general-purpose registers eax
, ecx
, edx
, ebx
, esi
, edi
and ebp
and 0611539d99f656 and 0611539d99f656 and 0611539d99f656. Data segment registers ds
, es
, fs
, gs
etc.
Why does the CPU ignore these registers? In fact, I don't quite understand, I can only say that this is determined by the design of the CPU architecture. My personal understanding is that the CPU is an instruction executor, it only cares about the execution flow of instructions, which includes the instruction
and stack
; as for data, it should be handed over to the upper logic, which is the logic of the code itself. Responsible for management. The design concept here is actually to make a segmentation of the logic responsible for the hardware and software. In fact, this is difficult to define, and it can also be understood as a historical legacy. It was decided from the beginning.
Interrupt handler
context
far, we return to the problem of saving the interrupt 0611539d99f6a8. Since the CPU does not save the data-related registers, it is up to us to save it.
Let's look at the code of the interrupt handler from top to bottom. First of all, each interrupt obviously has its own interrupt handler, or isr (interrupt service routine)
:
isr0
isr1
isr2
...
Here, each isr*
has a common structure, which is defined by the macro syntax in asm:
; exceptions with error code pushed by CPU
%macro DEFINE_ISR_ERRCODE 1
[GLOBAL isr%1]
isr%1:
cli
push byte %1
jmp isr_common_stub
%endmacro
; exceptions/interrupts without error code
%macro DEFINE_ISR_NOERRCODE 1
[GLOBAL isr%1]
isr%1:
cli
push byte 0
push byte %1
jmp isr_common_stub
%endmacro
Then we can define all isr*:
DEFINE_ISR_NOERRCODE 0
DEFINE_ISR_NOERRCODE 1
DEFINE_ISR_NOERRCODE 2
DEFINE_ISR_NOERRCODE 3
DEFINE_ISR_NOERRCODE 4
DEFINE_ISR_NOERRCODE 5
DEFINE_ISR_NOERRCODE 6
DEFINE_ISR_NOERRCODE 7
DEFINE_ISR_ERRCODE 8
DEFINE_ISR_NOERRCODE 9
...
Why are there two definitions of isr
In fact, I forgot to mention a little bit about the CPU automatic instruction
. In addition to the information about 0611539d99f763 and stack
mentioned above, for some exception
, the CPU will also push an error code. As for which exception
will press the error code, you can refer to the form given above.
exception
inconsistency, for the sake of unity, for those 0611539d99f78d that will not be pressed into the error code, we manually add a 0 to it.
So in summary, isr
is the total entry point for interrupt processing. It mainly does the following things:
- Turn off
interrupt
, note that this can only shield hard interrupts, it is invalidexception
- Press the interrupt number;
- Jump to
isr_common_stub
;
Come to isr_common_stub
, the code is at src/interrupt/idt.S , here is the place to save and restore the data related register context, and enter the real interrupt processing.
This is the first half of it:
[EXTERN isr_handler]
isr_common_stub:
; save common registers
pusha
; save original data segment
mov ax, ds
push eax
; load the kernel data segment descriptor
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
call isr_handler
Then comes the second half of it:
interrupt_exit:
; recover the original data segment
pop eax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
popa
; clean up the pushed error code and pushed ISR number
add esp, 8
; make sure interrupt is enabled
sti
; pop cs, eip, eflags, user_ss, and user_esp by processor
iret
There is actually only one function isr_common_stub
(there is no ret
first half). interrupt_exit
is just a mark I added, because it will be used in other places in the future. Of course, in fact, there is no concept of functions in the assembly, and they are all marks in essence.
We saw several things done in the first half:
pusha
saves all general-purpose registers;- Then save the data segment register
ds
; - Modify the data segment register to the kernel, and then call the real interrupt processing logic
isr_handler
(more on this later);
After the interrupt is processed, the following things are done in the second half of the recovery phase, which is essentially the reverse operation of the first half:
- Restore the original
data
segment register; popa
restore all general-purpose registers;- Skip the error code and interrupt number in the stack;
- Resume interruption and return;
In this way, we can draw the complete stack
when the interrupt occurs, in which the green part is the saved interrupt context, including the part that is automatically pressed by the CPU and pressed by ourselves:
The pink part below is the real interrupt processing core function isr_handler
, which is written in C language:
typedef void (*isr_t)(isr_params_t);
void isr_handler(isr_params_t regs);
The isr_params_t
is defined as:
typedef struct isr_params {
uint32 ds;
uint32 edi, esi, ebp, esp, ebx, edx, ecx, eax;
uint32 int_num;
uint32 err_code;
uint32 eip, cs, eflags, user_esp, user_ss;
} isr_params_t;
The reason isr_handler
can use this structure as a parameter is precisely because the green part of the picture is pushed onto the stack, and then through call isr_handler
, the green part corresponds to the isr_params_t
structure. The red arrow points to the address of the isr_params_t
With the parameter isr_params_t
, we can get all the information about the interrupt isr_handler
void isr_handler(isr_params_t params) {
uint32 int_num = params.int_num;
// ...
// handle interrupt
if (interrupt_handlers[int_num] != nullptr) {
isr_t handler = interrupt_handlers[int_num];
handler(params);
} else {
monitor_printf("unknown interrupt: %d\n", int_num);
PANIC();
}
}
The upper part is all about the necessary interaction between the CPU and the interrupt peripheral chip, you can ignore it for now. isr_handler
as a general interrupt processing entry, it actually plays a role of distribution, it will find the real processing function corresponding to the interrupt according to the interrupt number. These functions are defined in the array interrupt_handlers
static isr_t interrupt_handlers[256];
They are set by the function register_interrupt_handler
void register_interrupt_handler(uint8 n, isr_t handler) {
interrupt_handlers[n] = handler;
}
The above codes are all located in src/interrupt/ . The code is not much but rather circumstantial. It should not be difficult to understand if you read it carefully.
Open clock interrupt
The above are all theoretical parts, we need a real interruption to practice the effect. The most ideal interrupt
course the clock ( timer
) interrupt, which is also the core interrupt used to drive multi-task switching later. The code for initializing timer
src/interrupt/timer.c . I won’t go into details here. It is mainly related to hardware port operations, setting the clock frequency, and the most important registration interrupt processing function:
register_interrupt_handler(IRQ0_INT_NUM, &timer_callback);
IRQ0_INT_NUM
is 32, which is the clock interrupt number;timer_callback
we can simply do the printing process:
static uint32 tick = 0;
static void timer_callback(isr_params_t regs) {
monitor_printf("tick = %d\n", tick++);
}
Then you can try to verify:
int main() {
init_gdt();
monitor_clear();
init_idt();
init_timer(TIMER_FREQUENCY);
enable_interrupt();
while (1) {}
}
Run bochs, if you are lucky, you can see this:
Trigger exception
timer
is a hardware interrupt interrupt
, we will look at a exception
examples, such as page fault
, interrupt number 14:
register_interrupt_handler(14, page_fault_handler);
void page_fault_handler(isr_params_t params) {
monitor_printf("page fault!\n");
}
How to trigger page fault
? It's very simple, as long as we visit a page table
that is not mapped.
int main() {
init_gdt();
monitor_clear();
init_idt();
register_interrupt_handler(14, page_fault_handler);
int* ptr = (int*)0xD0000000;
*ptr = 5;
while (1) {}
}
Run bochs, if you are lucky, you can see this:
You can see that it keeps printing. This is because our page fault handler does nothing but print, and it does not really solve the page fault. After the processing is over, the CPU will try to execute the memory access instruction that caused the page fault before, so it will trigger the page fault again. Regarding page fault
, we leave it to the next article, the virtual memory is perfect.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。