Memory Management and Task Switching in a Multiprogram Environment

Engineering

Posted by Bruce Lee on 2024-09-30

About Me

Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.

Main Topics

  • Engineering Projects: Exploring implementation details and how technical systems work.
  • C/C++: Notes on language features and programming techniques.
  • The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.

For more, visit the categories page.

Contact

If you have questions or would like to discuss something, please get in touch through the About page.

Thank you for reading and for your support. I hope these notes help you on your own technical journey!


Overview

Running several programs requires a plan for their memory regions and execution state. Position-independent code and relocation can make load addresses flexible, while virtual memory provides a stronger way to separate address spaces. Task switching must preserve the context, including the next PC and the stack state, so each program can resume correctly.

Loading Several Programs

With fixed physical load addresses, the second program must not overwrite the first program’s memory. That requires knowing both the existing program’s location and its occupied range. Independently compiling the next program does not inherently provide that information.

One approach is to build relocatable or position-independent images and let the loader choose and record nonoverlapping regions. The operating environment must also support switching execution between the loaded programs. In the simple physical-address setup, the PC identifies an actual guest address as seen by the emulator.

Virtual-memory translation provides another approach: programs can use independently managed virtual address spaces while the OS controls their physical mappings.

Deciding How Execution Switches

Why Separate Stacks?

Could several execution contexts share one ordinary stack? A suspended context can be stored at one position, but a different task’s subsequent pushes may overwrite it as the stack grows downward.

Separate stack regions prevent those live frames from overlapping. They need not require separate physical allocations in every implementation, but their usable ranges must be managed independently.

The trap-return path restores a selected context using a stack pointer. Selecting a different context therefore means selecting a different saved register frame and, ultimately, the appropriate task stack.

Finding the Initial Stack Pointer

Earlier examples had not required me to initialize sp explicitly. The tiny built-in NEMU image could run while sp stayed zero because it did not need a normal C call stack.

A program loaded through the larger runtime was different. Debugging its startup showed an assignment to sp. The AM startup assembly obtains the value from _stack_pointer, which is defined by the linker script.

For additional execution contexts, the context-initialization code should supply a stack pointer referring to the stack region allocated to that task.

Constructing an Initial Context

A task that has never run still needs a saved-context shape that the restore path can consume. It needs an initial stack pointer and an entry address in the field restored through mepc, plus appropriate status and privilege information.

The PCB uses a union containing an array that provides stack storage. kcontext uses the supplied Area to locate space for the Context. Its layout must match the save/restore order in trap.S.

The entry is usually a function, and its argument must be placed in the saved a0 field so it appears in the correct register when the function begins.

The Switching Sequence

kcontext prepares an execution context in advance, much as a user-level context facility prepares a function to run on a selected stack.

In this cooperative design, the running task calls yield, deliberately trapping into the runtime to give up execution. The registered event handler invokes scheduling logic, and schedule returns the context to restore. Preemptive systems can also switch tasks on interrupts; voluntary yielding is the particular model examined here.

Back in trap.S, the returned pointer is in a0. The assembly selects that saved frame and loads its registers, eventually transferring control to the chosen task.

The scheduling handler is connected through cte_init. Later work can place scheduling decisions behind the appropriate syscall and event handling.

Saving and Restoring sp

The initial saved sp is established by kcontext. On subsequent traps, the current task’s stack state must be preserved just as carefully as the other registers.

In my implementation, I found that the existing save sequence did not preserve the stack-pointer value in the way my restore path expected. I supplemented it with the corresponding save operation. Otherwise, repeated switches caused stack-pointer confusion.

Some trap layouts reconstruct the original stack pointer from the frame location rather than store it as an ordinary register slot. Whichever convention is used, initialization, save, and restore must agree.

When Should mepc Advance by Four?

For ecall, mepc identifies the trapping instruction. The handler must decide whether execution should retry that instruction or resume after it.

At this stage, the design did not support retrying the call, so I advanced the saved exception PC by four in the trap path. That is appropriate for the handled ecall case, but should not become an unconditional rule for all exceptions and interrupts.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !