In-depth understanding of process creation and scheduling

  • The initial public number is Rand_cs

This article followed the above in-depth understanding of the data structure of the process to tell about some of the process operations, mainly is to create, scheduling switch, loading procedures, sleep wake up, wait to exit and so on, this article first to tell about the process of creation and scheduling switch two aspects, nonsense not to say, look.

Scheduling switch

About the first part of the thought for a long time, decided or process scheduling and switching at the beginning, according to the truth should be about the creation of the process, but the creation of the process is closely related to the switch, only the process of switching clear, to understand the creation of the process, so the process will be switched to say it.

To cut to the chase, there are two main events that trigger process scheduling and switching:

  • A process timer is up, time to CPUCPUCPU
  • A process actively cedes CPUCPUCPU due to some event blocking

Xv6xv6xv6 process switching is divided into three steps:

  • The AAA process switches to the scheduler
  • The scheduler picks a process BBB
  • The scheduler switches to process BBB

The first and second steps are switchover operations, and the middle step is scheduling operations. In fact, the switching process is switched twice. The first time, the AAA process is switched to the scheduler. The scheduler selects a process BBB according to the scheduling algorithm, and then switches to the process BBB.

The process switching is the context saving and recovery, two switching operations, there will be two kernel-state context saving and recovery. The switch must be carried out in the kernel, and the process will exit the kernel after the switch, so it will also involve the saving and recovery of user-level context, so the process switch is as shown in the figure below:

Switching and scheduling are two kernel functions, so to figure out the process scheduling and switching, mainly is to figure out switching and scheduling two functions, first look at switching function

Switching function

Function prototype:

void swtch(struct context **old, struct context *new);
Copy the code

Function definition:

.globl swtch
swtch:
  movl 4(%esp), %eax
  movl 8(%esp), %edx

  # Save old callee-saved registers
  pushl %ebp
  pushl %ebx
  pushl %esi
  pushl %edi

  # Switch stacks
  movl %esp, (%eax)
  movl %edx, %esp

  # Load new callee-saved registers
  popl %edi
  popl %esi
  popl %ebx
  popl %ebp
  ret
Copy the code

The switch function is short and symmetric, with the upper and lower sections representing the context in which oldoldold is saved and the context in which newNewNew is restored

Process A switches to the scheduler

This function to talk about is not to say, with practical examples to illustrate, such as from now switch to the scheduler schedulerschedulerscheduler AAA process, process the task of AAA structure pointer to AAA, the current CPUCPUCPU pointer for CCC, The switch function is called like this:

Scheduler (&a->context, c->scheduler);

The process AAA kernel stack is shown in the second box, and I also draw the CPUCPUCPU stack and structure. The various references should be very clear, that is, there are more than 100 million points, note two points:

  • The first argument to SWTCHSWTCHSWTCH is a second-level pointer, which in our case is &a→context\&a\rightarrow context&a→context, This secondary pointer is the address value of the ContextContextContext property field in the process AAA structure.
  • The solid line in the diagram is the one that has the actual pointing relationship, and the dotted line is no, no, no, no, I’ve made a mistake here, just to remind myself three times. A pointer in a structure is a variable that points to a certain location only when it is assigned a value, and will always point to a certain location unless its value is changed. The main thing I want to do here is to show you what the variables are pointing to in the various data structures, and you shouldn’t actually draw them. But if because system calls into the kernel, trapframetrapframetrapframe that thread is solid line, because there is a change in the process of handling a system call trapframetrapframetrapframe assignment statements. The kstackkstackkstack pointer always points to the top of the stack (not the top of the stack), as discussed later in the TSSTSSTSS section.

After the parameters are ready, the SWTCHSWTCHSWTCH function is executed. The first two MOV statements are very simple. Take the parameters and put them in a register. &a→ Context \&a\ Rightarrow context&A → Context in eaxeaxeAx, c→ SchedulerC \ Rightarrow schedulerc→ EdxedxedX

Then the four register values are pressed to save the kernel part of the AAA process context, and the stack layout does not change much:

Note in this section that contextContextContext defines 5 registers, but only 4 registers are actually shown to be pushed, and one eipeipeIP (return address) is implicitly pushed when SWTCHSWTCHSWTCH is called

Then there are two more mov statements:

movl %esp, (%eax)
movl %edx, %esp
Copy the code

These two MOV statements are the core because this is a stack swap:

  • Save the top of the kernel stack of process A to the context field of the task structure
  • will
    C P U CPU
    The top of the kernel stack is assigned to ESP to complete the stack swap

Two more points:

  • The top stack address of process A is stored in the context field of the task structure, not in the Kstack field. Although it seems that kstack is named as the kernel stack address, in fact kstack is not very useful. See TSS below for details.
  • The value of CPUCPUCPU’s context field is the top of the stack. For the first reason, as we will see later when we switch from the scheduler to B, CPUCPUCPU’s stack top address is saved into the context field of the CPUCPUCPU structure, which preserves consistency.

Current stack status:

It is clear that the ESPESPESP register now points to the CPUCPUCPU stack instead of the kernel stack of process AAA. The top of the stack of process AAA is saved in the contextContextContext field of the task structure. The ContextContextContext field points to the top of the kernel stack for process AAA.

Then four registers pop up, and ret returns:

The popstack doesn’t change anything except the value of ESPESPESP, so the pointing relationships don’t change at all. But in fact the right that together schedulerschedulerscheduler pointer have failed, for example, such as switching to update its value will only be effective next time.

When ret, ESPESPESP should point to the return address. This address is the return address of the dispatcher, and the dispatcher is executed after ret pops it into the EIPEIPEIP register

The scheduler

The scheduler does two things:

  • According to the scheduling algorithm to pick a process, let’s call this process BBB
  • Call the SWTCHSWTCHSWTCH function above to switch to process BBB

Scheduling algorithm

I in the multi-processor scheduling algorithm article @@@@@@@@@@@ summarized several common scheduling algorithms, you can take a look, including xv6XV6xv6 scheduling. The scheduling algorithm of XV6XV6Xv6 is simple polling. The polling described in various books and online is the case of a single processor, but it is slightly different under multiple processors. See the schematic diagram:

Only a global “ready queue” is maintained in the kernel, shared by all CPucpucpus. Each CPUCPUCPU has its own scheduler, which picks the appropriate process from the global queue and assigns CPUCPUCPU to it.

The single-queue form is relatively simple to implement and is fair to all CPucpucPus. The queue is shared globally, so a lock is required when one CPUCPUCPU selects a process, otherwise multiple CpucpucPus may select the same process. However, the locking mechanism inevitably introduces additional overhead and reduces performance.

The specific code is as follows:

void scheduler(void)
{
  struct proc *p;
  struct $CPU$*c = my$CPU$();
  c->proc = 0;
  
  for(;;) { sti();// Allow interrupts
    acquire(&ptable.lock);  / / lock
      
    for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){  // Loop to find a RUNNABLE process
      if(p->state ! = RUNNABLE)continue;

      c->proc = p;   // This CPU is ready to run p
      switchuvm(p);  // Switch the p process page table
      p->state = RUNNING;  // Set the status to RUNNING

      swtch(&(c->scheduler), p->context);  // Switch the process
      / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
      switchkvm();   // Switch back to the kernel page table
      c->proc = 0;   // No process is running on the CPU
    }
    release(&ptable.lock);   / / releases the lock}}Copy the code

The partial scheduling algorithm on the split line selects a process BBB to switch to the part of BBB, and then switches from the process BBB to the scheduler for a new round of scheduling after the split line. The whole process should feel very clear and simple, but the code is actually quite complex if you look at it closely

The question of why interrupts should be allowed periodically, and why locks should be fetched before SWTCHSWTCHSWTCH, comes down to locks and deadlocks, as detailed in a later article.

In addition, the SWTCHSWTCHSWTCH function in the scheduler should be aware that it does not return. In the middle of execution, the process context is restored to execute the process, and when it returns to the scheduler again, no process is running on CPUCPUCPU. I’m getting ahead of myself here, but let’s continue with the scheduler switch to process BBB

The scheduler switches to process B

SWTCH (&(C -> Context), B -> Context); SWTCH (&(C ->context); The process is almost exactly the same, and I don’t want to repeat it, just look at the picture at the end. Here mainly shows that the process of switching to BBB need to do two important things: before updating TSSTSSTSS and switch a page table, these two things are done in switchuvmswitchuvmswitchuvm function, respectively

update
T S S TSS

Let’s focus on the role of TSSTSSTSS in the data structure can be simply considered to provide the address of the kernel stack, switching process must be written to the address of the kernel stack in the TSSTSSTSS structure, so there are the following operations.

my$CPU$()->gdt[SEG_TSS] = SEG16(STS_T32A, &my$CPU$()->ts, sizeof(my$CPU$()->ts)- 1.0);
my$CPU$()->gdt[SEG_TSS].s = 0;

my$CPU$()->ts.ss0 = SEG_KDATA << 3;   // Change SS to new stack
my$CPU$()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;

my$CPU$()->ts.iomb = (ushort) 0xFFFF;   // Disable IO commands in user mode

ltr(SEG_TSS << 3);      // load the TSS segment selector into TR register
Copy the code

TSSTSSTSS’s only function now is to provide the address of the kernel stack, so the TSSTSSTSS esp0ESP0 field should be updated only when switching processes. Even SS0SS0SS0 doesn’t need to be updated because flat mode shares selectors.

I made the following changes to the xv6xv6xv6 code, except for the ESP0ESP0ESP0 update, which I put together in the initialization code at startup:

static void tssinit(void){
  struct $CPU$*c;
  c = &$CPU$s[$CPU$id()];
  c->gdt[SEG_TSS] = SEG16(STS_T32A, &my$CPU$()->ts, sizeof(my$CPU$()->ts)- 1.0); // Register the TSS descriptor in GDT
  c->gdt[SEG_TSS].s = 0;  // Change the S bit to indicate that this is a system segment

  c->ts.ss0 = SEG_KDATA << 3;  // Selectors use kernel data segment selectors

  c->ts.iomb = (ushort) 0xFFFF;  // Disable IO commands in user mode
  ltr(SEG_TSS << 3);  // Load selectors into TR
}
Copy the code

Add the TSSTSSTSS initialization function to the initialization code:

/*******main.c********/
int main(void){      // Initialize the TSS of BSP
    / * * * * * * * * * /
    tssinit();
    / * * * * * * * * * /
}

static void mpenter(void){  // Initialize TSS for AP
    / * * * * * * * * * /
    tssinit();
    / * * * * * * * * * /
}
Copy the code

Since xv6XV6xv6 supports multiple processors, the startup code for BSPBSP and APAPAP is slightly different. Both need to be initialized by calling tssinittssinittssinit. Here is the initialization code:

According to the previous process switching method, each process should have a separate TSSTSSTSS, but it is too inefficient to use this. Xv6xv6xv6 This is one for each CPUCPUCPU, shared by all processes. TSSTSSTSS is a piece of memory data, which needs to be registered in GDTGDTGDT. The so-called registration is to add a TSSTSSTSS descriptor in GDTGDTGDT, and fill in the base address, bounds, and type of TSSTSSTSS. TSSTSSTSS is a data structure supported by the hardware, and the hardware operation must have this structure. The memory data segment with such characteristics (generalized data) is called the system segment, and the descriptor SSS bit of the system segment needs to be set to 0. The process code segment data segment (narrow sense of data) is not the system segment, they are SSS bit 1.

The kernel stack segment uses the kernel data segment selector. The base address of the IOIOIO bitmap is set to 0xFFFF0xFFFF0xFFFF. This position exceeds the TSSTSSTSS limit, indicating that the IOIOIO bitmap does not exist. If the IOIOIO bitmap does not exist, only the IOPLIOPLIOPL can determine whether the current privilege can use the IOIOIO instruction. EFLAGSEFLAGSEFLAGS If the IOPLIOPLIOPL bit is always 0, only the kernel can use the IOIOIO instruction.

Finally, the TSSTSSTSS selector is loaded into TRTRTR so that CPUCPUCPU knows where TSSTSSTSS is. This is the initialization part of TSSTSSTSS, and it doesn’t need to change again. Each time the process switches, you just need to update the value of ESP0ESP0ESP0 to:

my$CPU$()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
Copy the code

Kstack +KSTACKSIZE(uint)p\ rightarRow kstack+KSTACKSIZE(uint)p\ kstack+KSTACKSIZE(uint)p\ kstack+KSTACKSIZE, As shown below:

The value of ESP0ESP0ESP0 is the bottom of the kernel stack. The kernel stack is empty when exiting the kernel. This problem is explained at process creation time, instead of talking here and there.

To switch a page table

Update TSSTSSTSS with ESP0ESP0ESP0 as the kernel stack address of the new process. Each process works in its own virtual address space, so it has to switch the page table when switching processes.

lcr3(V2P(p->pgdir));
Copy the code

Switching the page table is a single statement that loads the page directory address of the new process BBB into register CR3CR3CR3. In CR3CR3CR3 page directory address must be a physical address, the address translation is to be obtained from the CR3CR3CR3 page directory address, if the address is a virtual address, it will have to translate this address not “infinite recursion” made a mistake, so CR3CR3CR3 must put the physical address, Therefore, the macro V2PV2PV2P is used to convert virtual addresses into physical addresses

Switching from scheduler to process B is shown as follows:

After the process is switched, let’s talk about the process creation.

Creating a normal process

With the previous process switch in mind, it’s much easier to understand process creation. In xv6xv6xv6 or LinuxLinuxLinux, all processes are created by forkfork except for the first initinitinit process, which requires a kernel. The first process is created in the last section of this article. This section starts with the creation of a normal process, the implementation of the Forkforkfork function

You’ve heard a lot about the forkforkfork function. As I mentioned in my article @@@@@@@@@, forkforkfork is like creating a clone of a child from the parent process. Cloning comes in different forms, from the unpretentious (silly) version to the very clever (copy while writing) version. The forkfork implementation of xv6xv6xv6 is a no-frightly copy of almost everything in the parent process.

The basic idea behind forkforkfork is to give a general idea of what the forkfork function does

  • Assign task structure, initialize task structure
  • Allocate the kernel stack, simulating the context to populate the kernel stack (this step is useless when forkforkfork)
  • Copy the parent process data and create a “new” page table
  • Copy the file descriptor table
  • Modify process structure properties.

Assign and initialize task structures

For the source code, I will only put the core code, some definitions and some “less important” operations such as locking and unlocking will not be displayed to take up space. The use of locks in general functions is relatively simple, and there is a special section behind the difficult key part to tell about it. Let’s start with the assignment of the task structure

static struct proc* allocproc(void){
    / * * * * * * * * * * * * * just * * * * * * * * * * * * * /  
    /* Find the space mission structure from beginning to end */
    for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) 
        if(p->state == UNUSED)
            goto found;
    / * * * * * * * * * * * * * just * * * * * * * * * * * * * / 
}
Copy the code

Iterate through the array of task constructs from front to back, looking for idle task constructs, and also looking for UNUSEDUNUSEDUNUSED constructs. After finding them, jump to FoundfoundFound

found:
  p->state = EMBRYO;   // Set the state to embry
  p->pid = nextpid++;  // Set the process number
Copy the code

You find a free task structure and you set it to EMBRYOEMBRYOEMBRYO, which means it’s just been assigned, it’s in the embryonic stage.

Nextpidnextpidnextpid is a global variable with an initial value of 1 that increases with each process created.

The distribution of task structure is very simple, so much, allocprocallocprocallocproc function of subsequent sections to allocate and initialize the kernel stack

Allocates and initializes the kernel stack

if((p->kstack = kalloc()) == 0) {// Allocate the kernel stack
    p->state = UNUSED;    // If the assignment fails, the task structure is recycled and returned
    return 0;
}
sp = p->kstack + KSTACKSIZE;  / / stack
// #define KSTACKSIZE 4096 
Copy the code

The kallockallockalloc function allocates a page in free space as the kernel stack, its location is not fixed, completely dependent on memory usage at the time. If the kernel stack fails to be allocated, the newly allocated task structure is reclaimed (set to UNUSEDUNUSEDUNUSED) and returned.

Using kallockallockalloc to allocate a page of space returns the first address of the page (low address). The newly allocated stack must be empty, so the top of the stack is the first address of the page plus the page size 409640964096

The next step is to initialize the kernel stack, and work on the newly allocated kernel stack, which is related to process switching. From the previous we know that the process switch is essentially a context save and restore, so what does this have to do with the process switch? Let’s think about what happens if you just allocate stack space and do nothing to decorate it.

A new process is created in the kernel, which we call process AAA. When A is created, it needs to be scheduled to execute on CPUCPUCPU, and then switch with the executing process B (I omit the switch to the scheduler here). The switch operation is to save the BBB context to the BBB kernel stack, there is no problem, and to restore the AAA context, the operation of restoring the context is the stack, and the stack has to have something to pop, right, and there seems to be only allocated stack space there is nothing in it?

From this, we need to populate the kernel stack of the new process with context, kernel-level context for switching, user-level context for returning from the kernel to user state. The user-level context is replicated by the parent process, as you’ll see later, while the kernel-level context is simulated to populate.

With the above understanding, back to allocprocallocprocallocproc function:

sp -= sizeof *p->tf;
p->tf = (struct trapframe*)sp;
Copy the code

Here is to reserve the interrupt stack frame space in the stack, and then record the interrupt stack frame address in PCBPCBPCB. This shows that the allocated empty stack is stored in the interrupt stack frame first. According to the previous process switch, we know that when returning to the user state, we need to restore the user level context, that is, to pop out the things in the interrupt stack frame. The kernel stack is empty after the eject, so no matter how complicated it is in the middle, the bottom part of the kernel stack must be the user-level context, and restoring the user-level context on exit will make the kernel stack empty again.

sp -= 4;
*(uint*)sp = (uint)trapret;
Copy the code

This step puts in the address of the interrupt return program

sp -= sizeof *p->context;
p->context = (struct context*)sp;
memset(p->context, 0.sizeof *p->context);
p->context->eip = (uint)forkret;
Copy the code

This step simulates the contents of the kernel-state context, and eipeipeIP (return address) is filled with the address of the forkretforkretret function

So when the process is scheduled, it executes forkretforkretret, then returns to execute an interrupt return, and then returns to execute the user program. See the interrupt mechanism for multiprocessors @@@@@@@@@ for details

The forkforkfork function allocates the task structure, allocates the kernel stack, and simulates the context.

Copy data to create page tables

if((np->pgdir = copyuvm(curproc->pgdir, curproc->sz)) == 0){
    kfree(np->kstack);
    np->kstack = 0;
    np->state = UNUSED;
    return - 1;
  }
Copy the code

The copyuvmcopyuVMcopyuvm function copies the parent user space data and creates a new page table. If an error occurs during replication, reclaim all resources allocated above and return.

pde_t* copyuvm(pde_t *pgdir, uint sz) {
  / * * * * * * * * * * * just * * * * * * * * * * /
  if((d = setupkvm()) == 0)      // Construct the kernel part of the page table. The kernel part is the same
    return 0;
  for(i = 0; i < sz; i += PGSIZE){     // Loop user part sz
    if((pte = walkpgdir(pgdir, (void *) i, 0)) = =0)  // Return the address of the page entry on which the address is located
      panic("copyuvm: pte should exist");    // If the value is 0,panic does not exist
    if(! (*pte & PTE_P))// Determine the P bit of the page entry
      panic("copyuvm: page not present");  // If the table 0 does not exist, panic
    pa = PTE_ADDR(*pte);     // Get the physical address of the page
    flags = PTE_FLAGS(*pte);  // Get the attributes of the page
      
    if((mem = kalloc()) == 0)  // Assign a page
      goto bad;
    memmove(mem, (char*)P2V(pa), PGSIZE);  // Copy the page data
    if(mappages(d, (void*)i, PGSIZE, V2P(mem), flags) < 0) {  // Map the physical page to a new virtual address
      kfree(mem);   // If there is an error release
      gotobad; }}return d;     // Returns the virtual address of the page directory
bad:
  freevm(d);    // Free all the space indicated by the page directory d
  return 0;
}
Copy the code

The process can use 2GB2GB2GB in user space, but only SZSZSZ is used. This value is recorded in the process structure. The previous data structure section @@@@@@@ explained this value because the program is loaded with 0 as the starting address. So SZSZSZ represents both the size of the current process in user space and the end address of the user part of the process.

Copyuvmcopyuvmcopyuvm is to copy this part to the child process from 0 to the virtual address space, and establish a mapping relationship to create a new page table. This indicates that the virtual address Spaces of the parent and child processes are the same, but mapped to different physical address Spaces. The process should be fairly clear:

  1. The physical address of part of the user’s virtual page is obtained from the page table of the parent process

  2. Assign a physical page to the child process and copy the data

  3. Map virtual pages to newly allocated physical pages

Repeat the above process is to copy the data to the user space and the new process to create a new page table of the process, there is an implied attention points, above a piece of code at first glance is very simple, but consider this problem, was rather then copy the data to a user space data handling to another user space, and each process’s virtual address space is independent, We commonly used memmovememmovememmove, memcpymemcpymemcpy and other functions are in the same virtual address space, is not across the space.

So what’s the solution? Here is using the kernel as a transfer, so carefully look at the use of the above memmovememmovememmove, two address parameters are kernel address, two virtual space address are converted into the kernel address, and then do data handling. I’ll leave it at that. If there’s any confusion, I’ll explain it in detail in the loader section later, because the loader section has special functions, so I’ll go into detail there.

Copy file descriptor table and share file table

Returning to the Forkfork function, I have adjusted the order of the source code slightly to make it easier to tell.

for(i = 0; i < NOFILE; i++)
    if(curproc->ofile[i])
      np->ofile[i] = filedup(curproc->ofile[i]);
np->cwd = idup(curproc->cwd);
Copy the code

Both parent and child processes have file descriptors. Forkforkfork copies the parent process’s file descriptors to the child process, but the file descriptors hold Pointers to the file table, so they share the file table.

Finally, change the current working path of the child process to that of the parent process. All these file management needs to call the special copy function dupduPDup, because the file system strictly manages the number of references and links of the file system. For details, see @@@@@@@@@@@@@ in file system Invocation.

Modify the process structure

np->sz = curproc->sz;   // The size of the user section
np->parent = curproc;   // The parent of the child process is the current process
*np->tf = *curproc->tf; // The child's stack frame is the parent's stack frame

// Clear %eax so that fork returns 0 in the child.
np->tf->eax = 0;   // Change the eAX value of interrupt stack frame to 0

safestrcpy(np->name, curproc->name, sizeof(curproc->name));  // Name of the replication process
pid = np->pid;    // Process id, which is used to return information

acquire(&ptable.lock);
np->state = RUNNABLE;    // Child process can run!!
release(&ptable.lock);

return pid;
Copy the code

The previous allocation and initialization of the kernel stack only reserved the interrupt stack frame space, not initialized, here directly to the parent process interrupt stack frame to a copy over. An interrupt stack frame is a user-level context, and a forkforkfork clone is an identical process that copies the user-level context of the parent process after the interrupt exits the recovery context. A parent-child process is one that runs the same program (because it replicates user-space data) and starts execution from the same place (because it replicates user-level context).

Why is the kernel-level context not replicated? The kernel level context is created when the process is switched, and the forkforkfork function is not executed when the process is switched, so it is not related to the process switch, but to create a child process to be scheduled on CPUCPUCPU, it needs to simulate the fill context. There will be a diagram at the end of this section

In addition, the context in the interrupt stack frame is not copied exactly, changing the value of eaxeaxeax, which is the return value of eaxeaxeax, to 0, which is why forkforkfork returns 0 for the child process.

The rest of the code is dealing with the name of the process, the state of the process, very simple, not to say more.

At this point, a simple version of Forkforkfork is created. It simply copies everything from the parent process, except for the context and the process number. Finally, let’s look at a picture

This graph shows what data forkforkfork mainly copies. Let’s look at what happens when the child process is first scheduled after being created and then returns to user state

The child process returned to user state

The kernel stack of the child process contains the context that we simulated to fill when it is scheduled to execute CPUCPUCPU. Specifically, after executing SWTCHSWTCHSWTCH, Forkret (EIP)forkret(EIP) is then loaded into EIPEIPEIP and executes forkretforkret:

void forkret(void){
  static int first = 1;
  release(&ptable.lock);
  if (first) {
    first = 0; iinit(ROOTDEV); initlog(ROOTDEV); }}Copy the code

Forkretforkretforkret this function for normal process is an empty function, general function here is for the first process, the story behind the first process, moreover the function involves releasing ptable. Lockptable. Lockptable. The operation of the lock, The issue of locking is also focused on later. So let’s just say it’s an empty function.

Traprettraprettrapret is the interrupt exit function, the interrupt stack frame inside the context to pop out. Finally, execute iRetiretiret to exit the interrupt and return to user state. See the interrupt base address @@@@@@@@@@@@@@@ under multiprocessors for this section

This concludes the forkforkfork function, which is used to create a normal process. The first creation is more appropriate after the loader and will not be covered in this article.

Well, that’s the end of this article, which focuses on creating and switching processes, although it’s backwards, but it shouldn’t affect much. The creation of a normal process is not tricky. Forkforkfork assigns a copy of “everything” to the parent process, and if it wants to be properly scheduled to perform the switch, it needs to simulate the new process as the old process and fill it with kernel-level context. And the most important thing is to put the address of the function to be executed in the kernel.

And the process switch is mainly the context of the preservation and recovery, the most important step is the stack, grasp these two points there is no problem.

This article is here first, related to the process of the later article to continue, if there is any problem, please comment, also welcome everyone to communicate with me about learning progress.