Series catalog
- Preface
- Preparation work
- BIOS boot to real mode
- GDT and protection mode
- A preliminary
- loads and enters the kernel
- display and print
- Global Descriptor Table GDT
- Interrupt handling
- virtual memory perfect
- implements heap and malloc
- The 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
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 functionkernel_thread
function
is the work function that thread actually needs to run, which iskernel_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 tostart_eip
, which is initialized to the functionkernel_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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。