2025. 1. 19. 03:18ㆍComputerScience/OperatingSystem
Process
A process is an instance of an application. Each process has its own independent execution context and resources, such as a virtual address space. Practical operating systems provide the execution context as a separate concept called a "thread". For simplicity, in this book we'll treat each process as having a single thread.
Process control block
The following process structure defines a process object. It's also known as "Process Control Block (PCB)".
#define PROCS_MAX 8 // Maximum number of processes
#define PROC_UNUSED 0 // Unused process control structure
#define PROC_RUNNABLE 1 // Runnable process
struct process {
int pid; // Process ID
int state; // Process state: PROC_UNUSED or PROC_RUNNABLE
vaddr_t sp; // Stack pointer
uint8_t stack[8192]; // Kernel stack
};
The kernel stack contains saved CPU registers, return addresses (where it was called from), and local variables. By preparing a kernel stack for each process, we can implement context switching by saving and restoring CPU registers, and switching the stack pointer. There is another approach called "single kernel stack". Instead of having a kernel stack for each process (or thread), there's only single stack per CPU. (seL4 adopts this method.) This "where to store the program's context" issue is also a topic discussed in async runtimes of programming languages like Go and Rust. Try searching for "stackless async" if you're interested.
Context switch
Switching the process execution context is called "context switching". The following switch_context function is the implementation of context switching
__attribute__((naked)) void switch_context(uint32_t *prev_sp,
uint32_t *next_sp) {
__asm__ __volatile__(
// Save callee-saved registers onto the current process's stack.
"addi sp, sp, -13 * 4\n" // Allocate stack space for 13 4-byte registers
"sw ra, 0 * 4(sp)\n" // Save callee-saved registers only
"sw s0, 1 * 4(sp)\n"
"sw s1, 2 * 4(sp)\n"
"sw s2, 3 * 4(sp)\n"
"sw s3, 4 * 4(sp)\n"
"sw s4, 5 * 4(sp)\n"
"sw s5, 6 * 4(sp)\n"
"sw s6, 7 * 4(sp)\n"
"sw s7, 8 * 4(sp)\n"
"sw s8, 9 * 4(sp)\n"
"sw s9, 10 * 4(sp)\n"
"sw s10, 11 * 4(sp)\n"
"sw s11, 12 * 4(sp)\n"
// Switch the stack pointer.
"sw sp, (a0)\n" // *prev_sp = sp;
"lw sp, (a1)\n" // Switch stack pointer (sp) here
// Restore callee-saved registers from the next process's stack.
"lw ra, 0 * 4(sp)\n" // Restore callee-saved registers only
"lw s0, 1 * 4(sp)\n"
"lw s1, 2 * 4(sp)\n"
"lw s2, 3 * 4(sp)\n"
"lw s3, 4 * 4(sp)\n"
"lw s4, 5 * 4(sp)\n"
"lw s5, 6 * 4(sp)\n"
"lw s6, 7 * 4(sp)\n"
"lw s7, 8 * 4(sp)\n"
"lw s8, 9 * 4(sp)\n"
"lw s9, 10 * 4(sp)\n"
"lw s10, 11 * 4(sp)\n"
"lw s11, 12 * 4(sp)\n"
"addi sp, sp, 13 * 4\n" // We've popped 13 4-byte registers from the stack
"ret\n"
);
}
switch_context saves the callee-saved registers onto the stack, switches the stack pointer, and then restores the callee-saved registers from the stack. In other words, the execution context is stored as temporary local variables on the stack. Alternatively, you could save the context in struct process, but this stack-based approach is beautifully simple, isn't it?
Callee-saved registers are registers that a called function must restore before returning. In RISC-V, s0 to s11 are callee-saved registers. Other registers like a0 are caller-saved registers, and already saved on the stack by the caller. This is why switch_contexthandles only part of registers.
The naked attribute tells the compiler not to generate any other code than the inline assembly. It should work without this attribute, but it's a good practice to use it to avoid unintended behavior especially when you modify the stack pointer manually.
Callee/Caller saved registers are defined in Calling Convention. Compilers generate code based on this convention.
Next, let's implement the process initialization function, create_process. It takes the entry point as a parameter, and returns a pointer to the created process struct
struct process procs[PROCS_MAX]; // All process control structures.
struct process *create_process(uint32_t pc) {
// Find an unused process control structure.
struct process *proc = NULL;
int i;
for (i = 0; i < PROCS_MAX; i++) {
if (procs[i].state == PROC_UNUSED) {
proc = &procs[i];
break;
}
}
if (!proc)
PANIC("no free process slots");
// Stack callee-saved registers. These register values will be restored in
// the first context switch in switch_context.
uint32_t *sp = (uint32_t *) &proc->stack[sizeof(proc->stack)];
*--sp = 0; // s11
*--sp = 0; // s10
*--sp = 0; // s9
*--sp = 0; // s8
*--sp = 0; // s7
*--sp = 0; // s6
*--sp = 0; // s5
*--sp = 0; // s4
*--sp = 0; // s3
*--sp = 0; // s2
*--sp = 0; // s1
*--sp = 0; // s0
*--sp = (uint32_t) pc; // ra
// Initialize fields.
proc->pid = i + 1;
proc->state = PROC_RUNNABLE;
proc->sp = (uint32_t) sp;
return proc;
}
Testing context switch
We have implemented the most basic function of processes - concurrent execution of multiple programs. Let's create two processes
void delay(void) {
for (int i = 0; i < 30000000; i++)
__asm__ __volatile__("nop"); // do nothing
}
struct process *proc_a;
struct process *proc_b;
void proc_a_entry(void) {
printf("starting process A\n");
while (1) {
putchar('A');
switch_context(&proc_a->sp, &proc_b->sp);
delay();
}
}
void proc_b_entry(void) {
printf("starting process B\n");
while (1) {
putchar('B');
switch_context(&proc_b->sp, &proc_a->sp);
delay();
}
}
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
WRITE_CSR(stvec, (uint32_t) kernel_entry);
proc_a = create_process((uint32_t) proc_a_entry);
proc_b = create_process((uint32_t) proc_b_entry);
proc_a_entry();
PANIC("unreachable here!");
}
The proc_a_entry function and proc_b_entry function are the entry points for Process A and Process B respectively. After displaying a single character using the putchar function, they switch context to the other process using the switch_context function.
delay function implements a busy wait to prevent the character output from becoming too fast, which would make your terminal unresponsive. nop instruction is a "do nothing" instruction. It is added to prevent compiler optimization from removing the loop. Now, let's try! The startup messages will be displayed once each, and then "ABABAB..." lasts forever.
'ComputerScience > OperatingSystem' 카테고리의 다른 글
[OS Project] Chap9. Memory Allocation (0) | 2025.01.18 |
---|---|
[OS Project] Chap8. Exception (0) | 2025.01.17 |
[OS Project] Chap7. Kernel Panic (0) | 2025.01.17 |
[OS Project] Chap6. C Standard Library (0) | 2025.01.17 |
[OS Project] Chap5. Hello World! (0) | 2025.01.16 |