Gos -- implementation thread

Written in front: self-made operating system Gos Chapter 3 chapter 2: main content thread implementation and management
For the basic knowledge of threads or processes, see the following blog:

Gos complete code: Github

Use of threads in Gos

You should use pthread under Linux at ordinary times:

	pthread_t new_pthread;
	pthread_create(&new_pthread,NULL,function,NULL);

This creates a thread. The essence of a thread is actually an execution sequence, which is responsible for executing the functions we pass in. In Gos, I also implement threads, but we can use one function:

//Function prototype:
/*
 * @brief Create a thread with priority and name, and specify its execution function and function parameters
 * @param name Thread name
 * @param priority thread priority 
 * @param function Address of function to be executed
 * @param func_arg Parameters of function
 */
//struct task_struct *thread_start(char *name, int priority, thread_func function, void *func_arg)
thread_start("test_thread", 10, function, NULL);

The main work of this function is as follows:

  1. Allocate memory space for storing thread PCB entities
    //Request one page of kernel space
    struct task_struct *thread = get_kernel_pages(1);
  1. Call function init_thread initialization thread basic information
    init_thread(thread, name, priority);
  1. Call thread_start initialization thread stack information
    thread_create(thread, function, func_arg);
  1. This thread is then added to the ready queue and all threads queue
    list_append(&thread_ready_list, &thread->general_tag);

    list_append(&thread_all_list, &thread->all_list_tag);

Now let's explain these two queues. In order for a thread to be scheduled, we must have a pointer to the next thread to be scheduled. Many such pointers form a queue. Of course, we can also use other containers, but for convenience of implementation, I choose queue to implement. In this way, the following two conceptual queues are formed in Gos:

PCB structure

The following elements are required for a thread:

  • pid: this process and thread have both
  • name: this process and thread have both
  • Thread priority information: this element represents the time that it can occupy the time slice in Gos
  • Page table and bitmap information: This is owned by the process, and the thread only shares the state. Because the design thread and the process share the task_struct structure, so now list it first
  • File descriptor array: This is owned by the process. Threads only share the state. Because design threads and processes share tasks_ Struct structure, so now list it first
  • Kernel stack information of thread
//pcb of process or thread
struct task_struct
{
    uint32_t *self_kstack; //Each kernel thread has its own kernel stack
    pid_t pid;
    enum task_status task_status; //Thread state
    char name[TASK_NAME_LEN];     //Thread name

    uint8_t priority;       //thread priority 
    uint8_t ticks;          //CPU time consumed by each thread
    uint32_t elapsed_ticks; //The total number of CPU s executed by the thread since its birth


    struct list_elem general_tag;  //Indicates the node identity of the thread in the general queue
    struct list_elem all_list_tag; //Thread acting on thread queue_ all_ Nodes in list

    uint32_t *pgdir;                                  //Virtual address of the process page table
    struct virtual_addr userprog_vaddr;               //Virtual address of the user process
    struct mem_block_desc u_block_desc[MEM_DESC_CNT]; //Memory management module of process

    int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; //File descriptor array

    uint32_t cwd_inode_no; //inode number of the working directory where the process is located
    uint16_t parent_pid;   //pid of parent process
    uint32_t stack_magic;  //The boundary mark of the stack, which is used to detect stack overflow
};

You can see that there are no variables representing the function to be executed, that is, its parameters, and these things are all in the process stack. The thread stack stores the current working environment information of the thread, which will be introduced below.

Thread basic information initialization

The initialization of thread basic information is actually to assign values to these parameters according to our input:

/*
 * @brief Basic information of initialization thread
 * @param pthread Thread address to be initialized
 * @param name Thread name
 * @param pthread_priority thread priority 
 */
void init_thread(struct task_struct *pthread, char *name, int pthread_priority)
{
    memset(pthread, 0, sizeof(*pthread));
    pthread->pid = allocate_pid();
    strcpy(pthread->name, name);

    //Add a judgment. If it is the main function, its running status will always be running
    if (pthread == main_thread)
    {
        pthread->task_status = TASK_RUNNING;
    }
    else
    {
        pthread->task_status = TASK_READY;
    }

    pthread->priority = pthread_priority;
    pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
    pthread->ticks = pthread_priority; //Set the thread running time as the priority of the thread. Undoubtedly, the higher the priority, the higher the running time
    pthread->elapsed_ticks = 0;
    pthread->pgdir = NULL;

    //Initialization file descriptor information
    pthread->fd_table[0] = 0; //Standard input
    pthread->fd_table[1] = 1; //standard output 
    pthread->fd_table[2] = 2; //Standard error
    //The rest are all - 1
    uint8_t fd_idx = 3;
    while (fd_idx < MAX_FILES_OPEN_PER_PROC)
    {
        pthread->fd_table[fd_idx] = -1;
        fd_idx++;
    }

    pthread->cwd_inode_no = 0; //Take the root directory as the default working path
    pthread->parent_pid = -1;
    pthread->stack_magic = 0x20000314; //Custom magic number, here is my birthday
}

In this way, the entity of this thread will be assigned in the physical space of this page allocated by us:

It can be seen that some elements have been initialized, while some have not. In fact, the unused elements do not belong to the basic thread information. They are more auxiliary information. We will call other functions to initialize them.

Thread stack information initialization

To understand the initialization process of thread stack information, we must first understand what elements are in the thread stack: first, several register variables, which follow the ABI called function Convention and are used to save on-site during thread switching; Secondly, eip is actually a very important element. After the first call, this variable will point to the function to be executed. However, after thread switching, this variable will save the return address of the new task after task switching; Finally, the pointer to the function to be executed and the function parameters I mentioned earlier.

/*
 * @brief Thread stack
 * @note Used to store the functions to be executed in the thread
 * @note This structure is not fixed in the kernel stack of the thread itself
 * @note Record in switch_to save the thread environment
 */
struct thread_stack
{
    uint32_t ebp; //The called function is used to save the values of these registers in the main calling function, mainly for fear of damaging the site
    uint32_t ebx;
    uint32_t edi;
    uint32_t esi;

    // *@ brief when the thread executes for the first time, eip points to the function to be called; At other times, eip points to switch_ Return address of to
    void (*eip)(thread_func *func, void *func_arg);

    //The following is only used when the cpu is scheduled for the first time
    void (*unused_retaddr); //Return address. It is reasonable to say that there is no return address after the execution of the thread, so this is only for bit occupation
    thread_func *function; //Save the address of the called function
    void *func_arg;        //Save the required parameters of the called function
};

The next step is to initialize the thread stack information:

  1. Reserve space for interrupt stack and thread stack
  2. Let eip point to the kernel function_ Thread, this function is very simple. In fact, it is through the kernel_ The thread function calls the function we passed in
  3. Initialize the information of the four registers
/*
 * @brief Initialize thread stack information
 * @param pthread The address of the thread to be initialized
 * @param function Thread pending function information
 * @param func_arg Parameters required for function
 */
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg)
{
    //Reserve space for interrupt use stack
    pthread->self_kstack -= sizeof(struct intr_stack);

    //Reserved thread stack space
    pthread->self_kstack -= sizeof(struct thread_stack);

    //Initialize thread stack information
    struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack;
    kthread_stack->eip = kernel_thread;
    kthread_stack->function = function;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}

So far, we know the information of thread stack in memory, as shown in the following figure:

Thread scheduling

After the thread initialization is completed, the thread entity actually exists in the ready thread queue and all thread queues we created, and our next step is to implement thread scheduling. After all, the main thread of the kernel cannot occupy CPU resources all the time. This implementation is mainly implemented in the schedule function of thread.c, and calling this function is mainly triggered by clock interrupt. Each clock interrupt will check whether the time slice of the current thread has expired. If it has expired, the schedule function will be executed.

/*
 * @brief Interrupt processing function of clock
 */
static void intr_timer_handler(void)
{
    struct task_struct *current_thread = running_thread();

    //Check whether the stack overflows. 0x20000314 is a magic number. For my birthday, you can set other numbers at will
    ASSERT(current_thread->stack_magic == 0x20000314);

    current_thread->elapsed_ticks++; //Record the cpu time consumed by this thread
    ticks++;                         //Kernel time++

    if (current_thread->ticks == 0)
    {
        schedule();
    }
    else
    {
        current_thread->ticks--;
    }
}

In order to realize thread scheduling, we must know the currently running thread information, because we need to save the current execution site for switching back in the future:

    //Get the address of the current thread
    struct task_struct *current = running_thread();

After that, we need to judge the reason why the current thread needs to be replaced. If the time slice arrives, our processing is:

  1. The current thread enters the thread ready queue
    if (current->task_status == TASK_RUNNING)
    {
        //This situation belongs to the time slice. It's time for rotation
        //Just add it to the end of the ready queue
        ASSERT(!elem_find(&thread_ready_list, &current->general_tag));
        list_append(&thread_ready_list, &current->general_tag);

        current->ticks = current->priority; //Reassign time slice
        current->task_status = TASK_READY;
    }
  1. Judge whether there are threads to be run in the ready queue. If there is no default running thread idle_thread, this thread will do nothing
    if (list_empty(&thread_ready_list))
    {
        //If there is no thread to run, wake up idle_thread thread, let it execute
        thread_unblock(idle_thread);
    }
  1. Get the information of the next thread to be run from the thread ready queue
    thread_tag = list_pop(&thread_ready_list);
    struct task_struct *next = elem2entry(struct task_struct, general_tag, thread_tag);
    next->task_status = TASK_RUNNING;
  1. Of course, if we are switching between processes, we also need to change the page table. After all, the running memory space has changed
    process_activate(next);
  1. Call switch_to switch the thread running field
    switch_to(current, next);

Get current thread information

We all know that esp is the stack top pointer of the program. If we get the current value of esp, then align and 0xffff f000 for and operation, we can or the starting address of the program page where the thread is located, that is, the starting address of the thread in memory.

/*
 * @brief Gets the PCB pointer of the current thread
 * @return Returns the address of the current thread
 * @note Each thread occupies one page of space, i.e. 4096 bytes, so you can get the starting address by performing ESP & 0xfffff000 here
 */
struct task_struct *running_thread()
{
    uint32_t esp;
    asm("mov %%esp,%0"
        : "=g"(esp));
    return (struct task_struct *)(esp & 0xfffff000);
}

Thread switching

As I just said, the essence of thread switching is to switch the running site of the site. In fact, it is to save the current thread and switch the register information of the next thread. Function switch_to accepts two parameters. The first parameter is the current thread, and the second function is the next thread to be run. It will save the register image of the current thread, and then load the image of the next thread to the processor.

switch_to:
    ;This is the return address in the stack
    push esi
    push edi
    push ebx
    push ebp

    mov eax,[esp+20]    ;Press in two parameters, the first is current´╝îThe second is next. Because there are four registers+Return address
                        ;therefore current Your address is esp+20
    mov [eax],esp       ;Save the stack top pointer, that is task_struct of self_kstack Field address, i.e. starting address
                        ;In fact, it means backing up the current thread environment

    ;Switch thread
    mov eax,[esp+24]    ;obtain next
    mov esp,[eax]       ;Switch to self_kstack

    ;These registers are next Thread register, non current. Its is next Stored by the replaced processor
    pop ebp
    pop ebx
    pop edi
    pop esi
    ret

The stack at this time is shown in the figure below. Each rectangle represents four bytes. We will first press the current register information on the stack, and then get the thread information of current. The first element in the current thread is self_kstack field, save the esp stack top pointer of current, and then load the self of next_ Kstack's stack top pointer is OK. At this time, the four ABI convention registers out of the stack are saved in the next.

reference

[1] Operating system truth restore

Tags: thread

Posted on Sat, 04 Dec 2021 14:22:44 -0500 by tskweb