Hello and welcome to Part 3 of the Into-the-OS series! This part aims to cover Chapter 6 of OSTEP. We will primarily dive into processes, Limited Direct Execution (LDE) alongside its problems and solutions (plus a few of my own opinions), and the dynamic between user space and kernel space π.
Direct Execution -> Limited Direct Execution
Okay, let's think about it this way: what is the absolute fastest way to run a process on the CPU? Simple: just run the process.
No fluff, just raw, direct performance. Hence the name Direct Execution. Simple enough, right? (Well, at least until I/O calls start staring at us from the background.)
So where is the problem?
Imagine you own a factory with a machine and a single worker. When an order comes in, the worker is responsible for doing everything needed to get the job done. Their tasks might include:
- Fetching things from the market
- Running the machine
- Writing the report to you and telling how much time might be needed
So, can you guess the problem?
When the worker is out at the market, the machine sits idle. Assuming we are incredibly greedy for efficiency, we don't want that machine to stop for anything other than a mandatory cooldown.
So here is what will happen:
- The time the worker takes to fetch materials from the market is wasted β the machine sits idle.
This, my friend, is exactly the problem with I/O calls. If a process waits for I/O, the CPU wastes precious cycles sitting idle.
Now, to offload this work, you hire a manager to oversee the worker. But this introduces a second problem: what if the worker tries to access confidential information, or attempts to take factory resources home?
We need two things to fix this:
- Control: We don't want to give the resources to the worker (we are greedy, remember π€).
- Performance: We also want to make as much profit as we can.
That's where the word LIMITED comes into play. We give control to the Manager (the OS) to maintain security, while offloading the responsibility of performance management.
Limited Direct Execution means the process still executes directly on the CPU, but we limit its ability to interact with the machine's hardware and resources.
A Practical Example
If you are familiar and can distinguish the difference between a Synchronous program and an Asynchronous program, you understand me, don't you?
There are various concepts we can pull out of this, like:
- Synchronous and Asynchronous Programming
- Decoupling in System Design (this includes various methods)
But all of these are based on one single concept: separating execution from fetching/uploading.
Addressing the Performance
Okay, now if sitting idle is the problem β what if, when this worker gets to work, we send another worker to the market to get materials? This would fix the issue, and the machine would not be idle.
Here's a program that shows this in action:
This is accessible in the Into-the-OS GitHub repository:
c0d3l0v3r-HeHe
/
Into-the-OS
A hands-on exploration of Operating System fundamentals. This repo contains the C/C++ code, calculations, and practical experiments accompanying the "Into the OS" deep-dive series.
Into-the-OS
These functions run as threads. Quick refresher if you're not familiar with threads: a thread isn't a process β it's a lightweight unit of execution within a process, and all the threads belonging to a process share that process's address space. Think of them as the individual workers for one process. In our hypothetical Factory (the process), we have 2 threads (workers) doing the work.
A quick caveat: this demo uses threads purely to illustrate the general idea β "don't let the CPU sit idle." It's programmer-level concurrency happening inside a single process. The actual mechanism OSTEP is describing in this chapter is different: the OS scheduler context-switching between separate, independent processes when one of them blocks on I/O. Threads are a great visual for the underlying problem, just don't read this code as "this is literally how the OS scheduler works."
As you can see in the code, as soon as the first worker goes to the market, the 2nd worker starts working on the machine. (Code is partially generated using AI.)
Addressing the Control
Computers enforce this by splitting execution into two modes:
- Kernel Mode: The privileged mode (the Manager). It can execute all instructions and access all resources.
- User Mode: The unprivileged mode (the Worker). It executes a restricted subset of instructions.
If any worker steps into the wrong room, it's the OS's responsibility to fire that worker β or the OS gets fired.
Unfortunately, we can't demonstrate this mode-switching in code the way we did with the threads example above β kernel/user mode transitions happen below the level our program can observe, so we can only walk through it conceptually here.
To execute the process, the CPU has to be handed over to it. So how does the manager get control back?
Earlier versions of operating systems relied on the phrase "TRUST THE PROCESS." Not very controlling, are they?
Earlier OSes typically used cooperative policies: it was the programmer's responsibility to hand control back to the OS after a certain amount of time. And when a developer asked, "what if I don't?" β the OS was practically silent. The answer was: you need to restart your machineβ¦.
To fix this, we moved to a timer-based approach. There's a timer running in hardware from the moment the computer boots, and after a certain threshold, a timer interrupt fires, handing control back to the OS. The OS then decides whether to keep running the current process or swap in another one.
How do we execute an I/O call?
Now, we want the CPU to know exactly what a worker (process) can and can't access, so it doesn't touch something it shouldn't. So how are these privileged instructions executed by the CPU? Can't we just tell the CPU directly β "Hey CPU, please read or open this file"?
No β because the process doesn't have the right to open the file. It's the OS's job to check whether the worker has that right. So what actually happens when we call open?
What Happens?
When we call open() in our program, we're not calling the syscall directly β we're calling a wrapper function (provided by libc) around it. What that wrapper does is save some information into a few registers:
So when we call open(args), each argument gets saved into a register, and then a special instruction called syscall is executed.
That instruction is what hands control to the OS to actually execute the I/O. But how does the OS know which syscall to run?
See the number next to open β that 2? That doesn't identify which syscall to run; it actually refers to Section 2 of the Linux manual pages, the section dedicated entirely to System Calls.
When a program wants to open a file, it places a specific syscall number into a register (like rax on 64-bit architecture) and executes a trap instruction (like syscall or int 0x80). This switches the CPU into kernel mode and triggers the kernel's main system call handler. The OS then reads that number and uses it as an index into the System Call Table β a list, set up by the OS, containing the memory addresses of every kernel function (like sys_open). When the syscall instruction runs, the system saves the current register values onto the program's kernel stack and switches to kernel mode, where it decides whether to keep running the same process or perform a context switch (swapping out the registers and stack of the running process for those of the next scheduled process).
Here are the images that show the earlier and later control mechanisms:
Credits : The two images above are from OSTEP Book.
We've now covered how the OS handles Control and tries to optimize for Performance, and we've seen how it juggles between processes. But how does it actually decide which process runs next?
I'd planned to go further, but I think this is already pushing past the 10-minute read that's the whole point of this format β so I'll pick this up next time. See ya!!!!!!!
β c0d3l0v3r





Top comments (0)