3

Series catalog

Prepare

This project series is almost halfway here. The segments, virtual memory, interrupts, etc. in the first half can actually be regarded as just preparations. Yes, we spent a lot of time preparing these cornerstones, so that up to now, the entire so-called kernel seems to be still in a "static" state, which may have made you feel sleepy. From the beginning of this article, this kernel will really "move" and begin to build the core capability of an operating system, which is task management.

From the user's point of view, the essential function of an operating system is to run tasks for them, otherwise it would be difficult to call it an operating system. This is a complicated project, and everything is difficult at the beginning, so as the beginning of this article, it will be as simple as possible, starting from running a single thread. In the following chapters, we will gradually enter the issues of multi-thread switching and management, synchronization and competition, and finally to the higher-level process management.

Thread

thread and process should not need to be explained, they are all clichés. In the following text and code, I will task equivalent to thread , the two are mixed, both represent threads; while process is a process.

The object of operating system scheduling is thread , which is also the core concept that needs to be discussed and implemented next. Maybe thread sounds very abstract. In essence, it can be boiled down to the following two core elements:

代码 + stack

The code controls its circulation in the time dimension, and stack is the backing of its spatial dimension. These two constitute the thread operation of 060fecdec26b12.

So each thread has its own stack, such as a bunch of threads running in the kernel state, which is roughly like this:

Each thread runs on its own stack, and the operating system is responsible for scheduling the start and stop of these threads. In essence, since we entered the main function of the kernel to run, it can also be classified as a thread, which is a guide. In the future, the operating system will create more threads, and the CPU will jump back and forth between these threads under the control of the operating system. The essence is to perform on the code (instruction) and stack of these threads. Jump switch.

Create thread

This code is mainly in src/task/thread.c , for reference only.

First create the thread structure task_struct , or tcb_t , that is, task control block :

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

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

There are two key fields here, which are stack information about this thread:

  • kernel_esp
  • kernel_stack

Each thread allocates kernel stack space in units of pages. Linux seems to be 2 pages, so we also allocate 2 pages and kernel_stack field. This field does not change anymore:

#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 assigning 2 pages to kernel_stack, a physical memory mapping is established for it immediately. This is because page fault an interrupt, and its processing is to be completed on the kernel stack, so access to the kernel stack itself can no longer trigger page fault , so this problem is solved in advance.

Another field, kernel_esp , identifies the esp pointer of the current thread running on the kernel stack. At present, it is the thread creation stage. We need to initialize this esp, so first we need to initialize the layout of the entire stack. 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 glance, but it will be explained later. It is not only the initial stack when the thread starts running for the first time, but also the stack when multi-threads context switches later, so it can also be called context switch stack or switch stack . We lay it out on the 2 pages stack space we just allocated:

switch stack interrupt stack that will be used as the user space in the future and can be ignored for the time being. Currently you need to know its structure is defined as interrupt_stack_t , that is, before the src / interrupt / interrupt.h defined in isr_params this structure, you can look back interrupt handling this one, it is the time of interruption The CPU and operating system are pushed onto the stack to save the interrupt context.

So the initialization of the entire stack is to allocate a interrupt stack + switch stack structure at the top:

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

So kernel_esp is initialized to the position shown on the icon, which actually points to the structure switch stack

The next step is to initialize this 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 the thread runs;
  • start_eip is the thread entry address, set to the function kernel_thread
  • function is the work function that thread actually needs to run, which is kernel_thread function;
static void kernel_thread(thread_func* function) {
  function();
  schedule_thread_exit();
}

If you don't understand here, you can continue to look at the operation of thread first, and then come back to review.

Run 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 very simple, a thread is test_thread , and the function to be run is 060fecdec26e83, just print it.

Here we use inline asm code in C language to trigger thread to start running, let's take a look at its principle. Esp register assignment for the first thread of kernel_esp , then jumps to resume_thread function:

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

It is actually context_switch function. This is used for multi-threads switching, and I will talk about it in the next article.

Look at what resume_thread kernel_esp in the figure, and run the code:

  • First, pop has all general-purpose registers. In the multi-threads switch, it is used to restore the context data of thread, but now thread is running for the first time, so they are all popped to 0 here;
  • Then the ret instruction causes the program to jump to start_eip , which is initialized to the function kernel_thread . From here, the thread officially starts to run, and its running stack is the light blue part in the right picture;

Note that the kernel_thread function, passed in and run the parameter function , this is the real working function thread

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

There may be several issues that need to be explained:

[question 1] Why not just run function , and want to nest a layer on the outside kernel_thread function as a wrapper?

Because the thread needs an exit mechanism after the thread runs, the function schedule_thread_exit will complete the thread finishing and cleaning up; the schedule_thread_exit function will not return, but directly guide the thread to die, and then switch to the next thread to run. About schedule_thread_exit will be discussed in detail in the next article.

[Question 2] is the gray part unused

It is kernel_thread function. In fact, kernel_thread will not return because the function schedule_thread_exit will not return. Here unused just a placeholder.


OK, so far our first thread is up and running, you can see its print:

Summarize

This article ran the first thread. What it did was actually relatively simple. It found a piece of memory to do stack , then created a function running environment on it, and then jumped the instruction to the entry of the thread to start running. There may be a lot of details in the fog, but I don’t know why, so I haven’t expanded it in detail in this article. For example:

  • Why build such a stack layout?
  • What is the exit mechanism after thread runs?
  • And most importantly, how do you switch between threads?

These are left to the next article for detailed explanation. After the next article is completed, combined with the content of these two articles, you should have a comprehensive understanding of the operating mechanism of threaad.


navi
612 声望189 粉丝

naive