PintOS

Extended the PintOS educational OS by implementing core kernel features spanning user program support, system calls, file I/O, floating-point context switching, and thread scheduling. Some highlights include user-memory validation for syscalls, process creation/exec/wait semantics, a full file-descriptor interface, saving/restoring FPU state across context switches, and kernel-level scheduling improvements (efficient sleeping, strict priority scheduling, and priority donation).

C

Overview

PintOS is an instructional operating system used in UC Berkeley’s CS 162 class to help lear how real kernels support user programs, system calls, processes/threads, scheduling, and synchronization. In my Operating Systems coursework, I worked with Dhruv Agarwal, Anshul Jambula, and Nemerjit Singh to implement the major functionality across the kernel/user boundary, including:

Project Breakdown

1) User Program Support

The first step was to make user programs receive arguments exactly like a normal process.

2) System calls

Next, we built out the syscall path so user programs could ask the kernel for services—process control and I/O.

Syscall support was added for:

A challenge here was that syscalls are a trust boundary. User code can pass garbage pointers or malformed arguments, so the kernel has to make sure the inputs are valid, and we added user-memory validation to prevent kernel crashes, such as:

3) Process coordination and correctness guarantees

Process creation and waiting sematnics require careful coordination internally. In particular they require the parent and child to agree on what happened and when.

We implemented conceptual guarantees like:

4) File I/O model

After process control, we implemented a usable file I/O interface. The goal was to give each process a file descriptor table model and support both console I/O and file-backed I/O.

Features added:

At this point, the underlying filesytem layer wasn’t thread-safe, so we ensured that file syscalls were synchronized to prevent races/corruption. We also made sure to enforce the common OS behavior that prevents modifying an executable while it is running.

5) Floating-point support

PintOS starts out focused on integer CPU state. We added FPU support so that floating-point code can run correctly even with interrupts, syscalls, and thread preemption.

This required:

This validated by using a floating-point computation task (approximating e) to confirm preemption doesn’t corrupt floating-point state.

6) Threading and scheduling improvements

After this initial setup for enabling the system to run programs, our next set of tasks was making it work well under load and contention.

Efficient sleeping (alarm clock)

This replaced inefficient sleeping (looping/yielding while waiting) with real sleeping where a blocked thread consumes no CPU until it’s time to wake up. The wakeups are driven by timer ticks.

Strict priority scheduling + priority donation

We implemented strict priority scheduling so the highest-priority runnable thread runs first, with round-robin among threads of equal priority.

To make synchronization behave correctly under priorities, we ensured:

7) User-level threads + user synchronization (pthread-like model)

To support multithreaded user programs, we added user threads with a 1:1 mapping to kernel threads (so each user thread is scheduled by the kernel).

That included:

We also added user-accessible locks and semaphores through syscalls. Each operation can cross into the kernel, but the behavior is correct and usable for coordination in user programs. Since each user thread needs its own stack, we also implemented the conceptual memory strategy for placing multiple stacks within a process’s address space without collisions.

8) File system internals

In the earlier portions of the project, we mostly used the file system through syscalls. In the third phase, we went a step further and implement core file system behavior such as caching, file growth, and directory trees.

Buffer cache

By default, many file operations would hit the disk device repeatedly. We added a buffer cache so the OS can keep recently-used disk blocks in memory and avoid redundant disk I/O.

What we built:

A big part of this work was making it correct under concurrency:

We also removed extra “bounce buffer” copying so reads/writes can operate cleanly through the cache and the disk interface.

Extensible files

The starter file system allocates files as one contiguous extent, which prevents file growth. We modified the on-disk representation so files can grow over time without requiring a single contiguous region of free blocks.

This enabled:

This work also removed a limitation of the basic root directory implementation (which starts out with a small fixed capacity), because directories are files too and now can expand as needed.

Subdirectories + path resolution (hierarchical directories)

The starter system technically has directories, but user programs can’t really use them. We added real support for a hierarchical directory tree and made the OS understand paths similar to what you would expect from a Unix-like system.

Features added:

On the interface side, we added directory-focused syscalls so programs can navigate and inspect directories (change directory, make directory, read directory entries, check whether an FD is a directory, and retrieve inode numbers).

We also updated existing behaviors to make directories first-class:

Synchronization (removing the “global lock” approach)

As the file system became more complex (buffer cache + growing inodes + directories), correctness under concurrency mattered more. Instead of relying on a single global lock, we switched to a more careful synchronization approach across the file system components.

Testing and validation strategy

We used the provided test suite as the baseline and added additional tests for behaviors such as: