Scenario Analysis of Go Scheduler Source Code Nine: Operating System Threads and Thread Scheduling

The following content is reproduced from  https://mp.weixin.qq.com/s/OvGlI5VvvRdMRuJegNrOMg

Awa love to write original programs Zhang  source Travels  2019-04-25

This article is the ninth section of the preliminary knowledge of the first chapter of the "Go Scheduler Source Code Scenario Analysis" series.

To understand goroutine's scheduler in depth, you need to have a general understanding of operating system threads, because go's scheduling system is built on operating system threads, so let's make a brief introduction to it.

It is difficult to give an accurate and easy-to-understand definition of thread, especially for readers who have never been exposed to multi-threaded programming. It may not be easy to understand what a thread is, so let’s put aside the definition and start directly from a C The language program begins to visually see what a thread is. The reason for using the C language is that we generally use the pthread thread library in the C language, and the user mode threads created using the thread library are actually the threads supported by the Linux operating system kernel, which are the same as the worker threads in the go language. , These threads are managed and scheduled by the Linux kernel, and then the go language has made a goroutine on top of the operating system threads to implement a two-level thread model.

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

#define N (1000 * 1000 * 1000)

volatile int g = 0;

void *start(void *arg)
{
        int i;

        for (i = 0; i < N; i++) {
                g++;
        }

        return NULL;
}

int main(int argc, char *argv[])
{
        pthread_t tid;

        // 使用pthread_create函数创建一个新线程执行start函数
        pthread_create(&tid, NULL, start, NULL);

        for (;;) {
                usleep(1000 * 100 * 5);
                printf("loop g: %d\n", g);
                if (g == N) {
                        break;
                } 
        } 

        pthread_join(tid, NULL); // Wait for the child thread to finish running 

        return 0; 
}

After the program runs, there will be two threads. One is the main thread created when the operating system loads the program and the other is the start sub-thread created by the main thread calling pthread_create. The main thread is created every other thread after the sub-thread is created. Print the value of the global variable g in 500 milliseconds until g is equal to 1 billion, and after the start thread starts, it starts to execute a 1 billion cycle of incrementing g by 1. These two threads run concurrently in the system, and the operating system is responsible By scheduling them, we cannot predict exactly when a thread will run.

Regarding the operating system's scheduling of threads, there are two issues that need to be clarified:

  • When will scheduling occur?

  • What will be done during scheduling?

Let's first look at the first question. When will the operating system initiate scheduling? Generally speaking, the operating system must obtain control of the CPU before it can initiate scheduling. So how can the CPU execute the operating system code when the user program is running on the CPU so that the kernel can gain control? Generally speaking, in two cases, the user program code will be executed to execute the operating system code:

  1. The user program uses the system call to enter the operating system kernel;

  2. Hardware interrupt. The hardware interrupt handler is provided by the operating system, so when the hardware is interrupted, the operating system code will be executed. The hardware interrupt has a particularly important clock interrupt, which is the basis for the operating system to initiate preemptive scheduling.

The operating system will check whether scheduling is required at certain points on the path of executing the operating system code, so the operating system's scheduling of threads will also occur in the above two situations accordingly.

Let's take a look at the output of running the program on the author's single-core computer:

bobo@ubuntu:~/study/c$ gcc thread.c -o thread -lpthread
bobo@ubuntu:~/study/c$ ./thread
loop g: 98938361
loop g: 198264794
loop g: 297862478
loop g: 396750048
loop g: 489684941
loop g: 584723988
loop g: 679293257
loop g: 777715939
loop g: 876083765
loop g: 974378774
loop g: 1000000000

It can be seen from the output that the main thread and the start thread are running in turn. This is the result of the operating system scheduling them. The operating system schedules the start thread to run for a while, and then schedules the main thread to run again.

From the output of the program, you can see the figure of preemptive scheduling, because the main thread is running during the start thread, and the start function executed by the start thread has no system call at all, and this program is running in a single-core system. Other CPUs run the main thread, so if there is no preemptive scheduling that occurs during interruption, the operating system cannot obtain control of the CPU, and thread scheduling cannot occur.

Next, let's take a look at what the operating system will do when scheduling threads.

As mentioned above, the operating system will schedule different threads to run on the same CPU, and each thread will use the CPU registers when running, but each CPU has only one set of registers, so the operating system is scheduling thread B When running on the CPU, you need to first save all the register values ​​used by thread A that was just running in the memory, and then put all the register values ​​of thread B saved in the memory back into the CPU registers. In this way, thread B can be restored to the state it was running before and then run.

In addition to the general registers, the registers that the operating system needs to save and restore during thread scheduling also include the instruction pointer register rip, the stack top register rsp and the stack base address register rbp related to the stack. The rip register determines the next instruction that needs to be executed by the thread. , 2 stack registers determine the stack memory that needs to be used when the thread executes. Therefore, restoring the value of the CPU register is equivalent to changing the next instruction to be executed by the CPU, and at the same time switching the function call stack. Therefore, from the perspective of the scheduler, the thread contains at least the following three important contents:

  • The value of a set of general registers

  • The address of the next instruction to be executed

  • Stack

Therefore, the scheduling of threads by the operating system can be simply understood as the switching of registers and stacks used by different threads by the kernel scheduler.

Finally, we have a simple and inaccurate definition of an operating system thread : an operating system thread is an execution flow that is scheduled by the kernel and has its own private set of register values ​​and stacks.


Finally, if you think this article is helpful to you, please help me click on the “Looking” at the bottom right corner of the article or forward it to the circle of friends, thank you very much!

image

Guess you like

Origin blog.csdn.net/pyf09/article/details/115238594