6

Series catalog

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 has exception , fault , trap and other types. We generally exception 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 is int 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 : That selector , where the provisions of this interrupt handler is located segment . After entering the interrupt processing, the CPU's code segment register, cs , is replaced with this value, so this selector must point to kernel of code segment ;
  • DPL : This is attrs 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 and eip provided);
  • Before the interrupt occurred stack position ( ss and esp 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 invalid exception
  • 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.


navi
612 声望191 粉丝

naive