Series directory

  • introductory
  • The preparatory work
  • BIOS starts in real mode
  • GDT and Protected Mode
  • Exploration of Virtual Memory
  • Load and enter the kernel
  • Display and print
  • The global descriptor table GDT
  • Interrupt handling
  • Virtual memory improvement
  • Implement heap and malloc
  • The first kernel thread
  • Multithreading switch
  • The lock is synchronized with multithreading
  • Implementation of a process
  • Enter user mode
  • A simple file system
  • Load the executable program
  • Implementation of system calls
  • The keyboard driver
  • To run a shell

To prepare

The first half of the segment, virtual memory, interrupts, etc., can actually be seen as preparatory work. Yes, we spent so much time preparing these cornerstones that up to now, the whole so-called kernel seems to be in a “static” state, probably making you feel sleepy. From this point on, the kernel will actually “move” and start building the core capability of an operating system, which is task management.

From the user’s point of view, the operating system’s essential function is to run tasks for them, otherwise it would be hard to call it an operating system. This is a complex project, and the first step is the hardest, so this first step will be as simple as possible, starting with a single thread. In later chapters, we will gradually move into the issues of multithreaded switching and management, synchronization and contention, and eventually to the higher level of process management.

thread

The concepts of Thread and Process do not need to be explained. They are all platitudinums. In the following lines of text and code, I will equate Task with Thread, and mix them up to represent Threads. Process is a process.

The object of operating system scheduling is Thread, which is the core concept that needs to be discussed and implemented next. While Thread may sound abstract, at its core it boils down to two core elements:

Code + stack

The code controls the flow of its temporal dimension and the Stack is its spatial dimension, both of which form the core of Thread’s operation.

So each thread has its own stack, such as a bunch of threads running in kernel mode, which looks something like this:

Each thread runs on its own stack, and the operating system is responsible for scheduling the start and stop of those threads. In essence, since we entered the kernel and ran the main function up to now, it can also be classified as a thread, which is a bootstrap. After that, the operating system will create more threads, and the CPU will jump and switch between these threads under the control of the operating system. In fact, these threads will jump and switch between their respective codes (instructions) and stack.

Create a thread

SRC /task/ Thread.c

Task_struct (tcb_t, task_struct, task_struct, tcb_t)

struct task_struct {
  uint32 kernel_esp;
  uint32 kernel_stack;
  
  uint32 id;
  char name[32];

  // ...
};
typedef struct task_struct tcb_t;

Here are two key fields for stack information about this thread:

  • kernel_esp
  • kernel_stack

Each thread allocates space in the kernel stack on a page basis. Linux seems to have 2 pages, so we also allocate 2 pages, pointing to it with the kernel_stack field, which never changes:

#define KERNEL_STACK_SIZE  8192

tcb_t* init_thread(char* name,
                   thread_func function,
                   uint32 priority,
                   uint8 is_user_thread) {
  // ...
  thread = (tcb_t*)kmalloc(sizeof(struct task_struct));
  // ...
  uint32 kernel_stack = (uint32)kmalloc_aligned(KERNEL_STACK_SIZE);
  for (int32 i = 0; i < KERNEL_STACK_SIZE / PAGE_SIZE; i++) {
    map_page(kernel_stack + i * PAGE_SIZE);
  }
  memset((void*)kernel_stack, 0, KERNEL_STACK_SIZE);
  thread->kernel_stack = kernel_stack;
  
  // ...
}

Note that after allocating 2 pages to kernel_stack, the physical memory map is immediately created for it. This is because the page fault, as an interrupt, is handled on the kernel stack, so the access to the kernel stack itself cannot trigger the page fault, so this problem is solved in advance here.

The other field, KERNEL_ESP, identifies the ESP pointer that this thread is currently running on the kernel stack. At this stage, we need to initialize the ESP, so first we need to initialize the entire stack layout. We define the following structure for stack:

struct switch_stack {
  // Switch context.
  uint32 edi;
  uint32 esi;
  uint32 ebp;
  uint32 ebx;
  uint32 edx;
  uint32 ecx;
  uint32 eax;

  // For thread first run.
  uint32 start_eip;
  void (*unused_retaddr);
  thread_func* function;
};

This stack structure may seem strange at first, but we’ll explain it later. It is the initial stack when the thread first starts running, and the stack when the multi-threads context is switched, so it can also be called the context switch stack or switch stack. We will lay it out onto the stack space of the 2 pages we just allocated:

The dotted space above the switch stack is reserved for the interrupt stack to be used later as a return to user space and can be ignored for now. Now you just need to know its structure defined as interrupt_stack_t, namely before the SRC/interrupt/interrupt. H isr_params defined within this structure, you can review the interrupt handling this article, It is the CPU and operating system stack that holds the interrupt context when the interrupt occurs.

So the entire stack is initialized with an interrupt stack + switch stack at the top:

thread->kernel_esp = kernel_stack + KERNEL_STACK_SIZE -
    (sizeof(interrupt_stack_t) + sizeof(switch_stack_t));

Kernel_esp is then initialized to the position shown above, which in effect refers to the switch stack structure.

The next step is to initialize the switch stack:

switch_stack_t* switch_stack = (switch_stack_t*)thread->kernel_esp;

switch_stack->edi = 0;
switch_stack->esi = 0;
switch_stack->ebp = 0;
switch_stack->ebx = 0;
switch_stack->edx = 0;
switch_stack->ecx = 0;
switch_stack->eax = 0;

switch_stack->start_eip = (uint32)kernel_thread;
switch_stack->function = function;
  • All general purpose registers are initialized to 0 because this is the first time Thread runs;
  • start_eipIs the thread entry address, set tokernel_threadThis function;
  • functionIs the actual working function Thread runs, which is composed ofkernel_threadFunction to start running;
static void kernel_thread(thread_func* function) {
  function();
  schedule_thread_exit();
}

If you don’t understand this, you can go ahead and look at Thread and then review it.

Running thread

Create Thread and run Thread:

void test_thread() { monitor_printf("first thread running ... \n"); while (1) {} } int main() { tcb_t* thread = init_thread( "test", test_thread, THREAD_DEFAULT_PRIORITY, false); asm volatile ( "movl %0, %%esp; \ jmp resume_thread": : "g" (thread->kernel_esp) : "memory"); }

The test code is simple, creating a thread that runs the function test_thread, and just printing it out.

Here, we use ASM code inline in C language to trigger Thread to start running. Let’s see how it works. First, assign the ESP register to the KERNEL_ESP of the thread, and then jump to the function Resume_thread:

resume_thread:
  pop edi
  pop esi
  pop ebp
  pop ebx
  pop edx
  pop ecx
  pop eax
  
  sti
  ret

This is the second half of the context_switch function, which is used for multi-threads switching. We’ll cover this in the next article.

To see what Resume_thread does, start with the kernel_esp location in the diagram and run the code:

  • First of all,popIn the multi-threads switch, it is used to restore the thread’s context data, but now the thread is running for the first time, so they are all pop to 0.
  • thenretCommand, cause the program to jump tostart_eipIt is initialized as a functionkernel_threadHere Thread officially starts running, and its running Stack is the light blue part in the right figure.

Notice that the KERNEL_THREAD function, passed in and run with the parameter function, is the actual work function of the thread:

static void kernel_thread(thread_func* function) {
  function();
  schedule_thread_exit();
}

There may be a few questions that need explaining:

Question 1: Why not run function directly instead of nested a layer of kernel_thread function as wrapper?

Because you need an exit mechanism when the thread finishes running, schedule_thread_exit does the finishing and cleaning of the thread. The schedule_thread_exit function doesn’t return; instead, it simply guides the thread to die and then switches to the next thread to run. More on schedule_thread_exit in the next article.

Question 2: What is the grey unused part in the figure?

This is the return value of the kernel_thread function, which does not actually return because schedule_thread_exit does not return. Here unused is just a placeholder.


OK, so now that our first thread is running, you can see its print:

conclusion

Here we run the first Thread. What it does is simple: it takes a chunk of memory to stack it, creates a function environment on it, and then jumps the instruction to the entry of the Thread to run it. There may be a lot of details, I do not know why, this article did not go into details, such as:

  • Why build one like thisstackThe layout?
  • What is the exit mechanism when Thread finishes running?
  • And most importantly, how do you switch between Threads?

I’ll leave all of this for the next post, but when I finish the next post, I should have a complete understanding of how ThreaAd works.