COMP1521 2,632 words·14 min read

09. Operating Systems

At the lowest level, a computer is a collection of processors, memory and devices. Those components do not inherently provide files, processes, windows, users or even a safe way for two programs to run at once. An operating system creates those higher-level concepts and manages access to the underlying hardware.

The operating system therefore sits between user programs and the machine:

This is related to the abstraction layers described in Preamble. A C program can request “write these bytes to this file” without knowing the voltage levels of the storage device, the structure of its controller or whether the data ultimately lands on an SSD, hard disk or network filesystem.

A World Without an Operating System#

Without an operating system, every program would need to manage the physical machine itself. Among other things, a program would need to:

  • initialise the computer and decide how to load code;
  • understand the exact model and protocol of every connected device;
  • decide where its data may live in physical memory;
  • invent a method for locating persistent data on raw storage;
  • coordinate the CPU with every other program;
  • prevent other code from reading or overwriting its state;
  • implement input, output and networking directly against hardware.

A program written for one hardware configuration would be deeply tied to it. Changing the storage controller, network device or even memory layout could require a rewrite.

The operating system centralises this difficult machinery. Programs target a stable interface supplied by the OS, whilst the OS and its device drivers deal with the hardware-specific details.

What an Operating System Provides#

Abstraction#

The OS presents simpler virtual objects in place of raw hardware:

An abstraction is a useful simplified view which hides machinery you should not need to manage every time. A file, for example, lets a program think in terms of names and ordered bytes. The operating system translates that request into filesystem lookups, device commands and physical storage operations. The hidden machinery still exists; the abstraction gives it a stable interface.

Hardware reality OS abstraction
physical CPU cores and timers processes and threads
physical RAM chips virtual address spaces
storage blocks files and directories
network controller buffers sockets
keyboard/display devices terminal input and output streams

These abstractions are not merely convenient names. They impose useful behaviour and rules. A file has a pathname, permissions and a current offset when opened; none of those ideas exist in a raw sequence of disk sectors.

Resource management#

Many programs may want the CPU, memory or a device simultaneously. The OS decides how these resources are shared:

  • the scheduler decides which process runs on a CPU;
  • the virtual-memory system gives each process its own address space;
  • filesystems coordinate persistent storage;
  • drivers coordinate device access;
  • permissions determine which operations a user or process may perform.

Protection#

One process should not be able to read another process's passwords or overwrite the kernel merely because it calculated a bad pointer. The operating system works with hardware protection mechanisms to isolate programs and enforce permissions.

Portability#

Two computers can contain different hardware whilst presenting the same operating-system interface. A program using the POSIX read interface does not need a separate implementation for every model of storage device.

This does not make every compiled program universally portable. The executable must still target the CPU architecture, executable format and operating-system ABI described in Running MIPS. The OS interface nevertheless removes a large amount of hardware-specific work from ordinary applications.

Kernel Mode and User Mode#

Protection depends on the CPU providing at least two privilege levels.

Kernel mode#

The operating-system kernel is the trusted core which remains in control of the machine. It runs in privileged mode, meaning the CPU permits it to perform operations which ordinary applications are deliberately forbidden from performing. It can:

  • configure memory protection;
  • access device-control registers;
  • execute privileged CPU instructions;
  • respond to interrupts—events which temporarily pause the current instruction stream so the CPU can handle something requiring attention, such as a timer or completed device operation;
  • inspect or modify the state of user processes;
  • schedule which program runs next.

Because kernel code has authority over the whole machine, a kernel bug can crash or compromise the entire system.

User mode#

Ordinary applications run in a non-privileged mode. They cannot directly access arbitrary physical memory or devices. Their instructions are constrained by protection state configured by the kernel.

A device driver is kernel code which knows how to operate a particular class of hardware. Applications ask for a high-level operation such as reading bytes; the driver translates that request into commands understood by the storage, network or display device.

If user code could simply switch itself into kernel mode, the separation would be useless. Instead, the CPU provides carefully controlled entry points through exceptions, interrupts and system-call instructions.

System Calls#

A system call is a request from a user program for the kernel to perform an operation which the program cannot or should not perform directly.

Typical system calls include:

  • reading and writing bytes;
  • opening or closing a file;
  • creating, replacing or terminating a process;
  • allocating or mapping memory;
  • communicating over a network;
  • obtaining time or filesystem metadata.

A system call is not an ordinary function call, even though a C wrapper may make it look like one. The request crosses a privilege boundary, changes the CPU into kernel mode at a controlled address, validates untrusted arguments and eventually returns to user mode.

Important

- System call is not the same as context switch
A system call switches privilege mode so the kernel can serve the current process. The scheduler may still return to that same process. A context switch specifically saves one execution context and restores another, as described in Processes.

Why validation matters#

The kernel must assume that every user-space argument is untrusted. Before writing to a file, for example, it may need to check:

  • whether the supplied file descriptor exists—a small integer handle which identifies one of the process's open kernel-managed I/O connections;
  • whether it was opened for writing;
  • whether the source buffer belongs to the calling process;
  • whether the requested byte count is valid;
  • whether the device or filesystem can accept the data;
  • whether a signal or resource limit interrupts the operation.

A syntactically valid system call can therefore fail. The kernel does not grant a request merely because the caller reached the correct entry point.

Note

Checkpoint

  • User programs run with restricted privilege; the kernel controls protected hardware and shared resources.
  • A system call crosses that protection boundary through a defined interface.
  • Crossing into the kernel is not the same thing as switching from one process to another.

Practice: Quick Checks#

  1. What is the difference between entering kernel mode for a system call and performing a context switch?
  2. A program passes a valid file descriptor but an invalid buffer pointer to write(). Why must the kernel reject the request instead of trusting libc?

Note

- Quick Check Answers

  1. A system call changes privilege mode so the kernel can serve the current process. A context switch saves one process or thread's execution state and restores another; a syscall does not necessarily cause one.
  2. The kernel treats every user-space argument as untrusted. Libc runs in user space and cannot make an arbitrary pointer trustworthy, so the kernel must validate the descriptor and check that the requested bytes can safely be read from the process's address space. If that memory is inaccessible, the call must fail instead of letting the kernel itself crash.

Three Layers of I/O in C#

On Linux, an operation such as printing can be requested at several layers.

Calling syscall directly#

The C library exposes a generic syscall function which takes a system-call number and its arguments:

C
#include <sys/syscall.h>
#include <unistd.h>

char message[] = "Hello!\n";
syscall(SYS_write, 1, message, 7);

This is useful for experimenting or accessing a newly added call without a wrapper. It is otherwise difficult to read, easy to misuse and tied to a particular operating-system interface and architecture.

Using a named C library wrapper#

The C library on a Unix-like system provides POSIX functions whose names and types correspond to system calls. These interfaces are supplied by libc, but functions such as write() are not part of the ISO C standard library:

C
#include <unistd.h>

char message[] = "Hello!\n";
write(STDOUT_FILENO, message, 7);

The wrapper supplies a readable name, performs the architecture-specific transition and translates kernel error conventions into the C errno interface.

errno is a thread-local integer used by many library wrappers to record an error code after their return value reports failure. <errno.h> provides readable names for those codes, such as EBADF for a bad file descriptor. Test the function's return value first; an old errno value is not evidence that the latest call failed.

Worked trace: one write request#

Suppose write(1, "hi\n", 3) is called on a terminal:

  1. The libc wrapper places the descriptor, buffer address and byte count where the platform ABI requires them.
  2. A controlled system-call instruction transfers execution to a kernel entry point. The program has not called a terminal device directly.
  3. The kernel looks up descriptor 1, checks that it is open for writing, and safely reads up to three bytes from the process's address space.
  4. The terminal driver accepts the bytes now or arranges for the process to wait until it can do so.
  5. The kernel returns a count or an error through the ABI, and libc presents that as the C return value and errno convention.

If descriptor 1 is closed, step 3 fails. On Linux the wrapper returns -1 and sets errno to EBADF; no terminal write occurs. If it returns 2, only two bytes were accepted and a caller which requires complete output must try the remaining byte. The requested count is a maximum, not a promise.

Using a higher-level library#

The <stdio.h> library provides buffered and formatted I/O:

C
#include <stdio.h>

printf("Hello!\n");

printf eventually causes bytes to be written, but it also parses a format string, converts values to text and normally buffers output. One library call does not necessarily correspond to one system call.

Higher layers are generally more portable and convenient. Lower layers provide greater control and expose the mechanics which COMP1521 is trying to teach. File Systems concentrates on the named open, read, write and close wrappers.

Practice: Tracing and Classification#

  1. Trace the layers crossed by printf("x"). Why might the call return without a write system call occurring immediately?
  2. Classify each operation as normally user-space work or a kernel service: integer addition, formatting %d, opening a pathname, scheduling another process, and copying bytes from a stdio buffer into a user array.

Note

- Tracing Answers
3. printf formats the character through stdio and places it in a user-space buffer. Libc eventually uses a write-style wrapper, crosses the syscall boundary, and lets the kernel validate and deliver the bytes. The immediate syscall may be absent because the stream has not yet reached a flush condition.
4. Integer addition, %d formatting and copying from a stdio buffer are normally user-space work. Opening a pathname and choosing which process runs require kernel-managed state and protection.

Mipsy System Calls#

Mipsy emulates a MIPS processor and also supplies a tiny teaching environment for system services. A MIPS program places a service number in $v0, places arguments in designated registers and executes syscall:

MIPS
li $v0, 4          # print_string service
la $a0, message    # address of argument string
syscall

This resembles the structure of a real system call, but the service numbers and behaviours are specific to the mipsy/SPIM-style teaching interface. They are not Linux x86-64 system-call numbers and should not be mixed with them.

Environment Service selector Arguments Transition mechanism
mipsy teaching system $v0 usually $a0 onwards MIPS syscall instruction
Linux through libc wrapper name such as write C function arguments wrapper performs architecture-specific transition
direct Linux call architecture-specific number architecture ABI architecture-specific syscall instruction

The syscall table in Using MIPS is therefore a table for mipsy programs, not a universal MIPS or Linux table.

The ABI Boundary#

An operating-system ABI specifies details such as:

  • system-call numbers;
  • registers or stack locations used for arguments;
  • how errors and return values are represented;
  • data structure layouts;
  • executable and object-file conventions;
  • rules for calling functions across compiled modules.

This is why source code can be recompiled for different systems but an already compiled executable generally cannot simply move between them. Both the processor's instruction set and the OS ABI must match.

Practice: Challenge#

  1. Two machines use the same C source and POSIX function names, but one is x86-64 Linux and the other is MIPS Linux. Why can recompilation work while copying the first machine's executable usually does not?
  2. Explain why “the syscall number is correct” is insufficient evidence that a request will succeed.

Note

- Challenge Answers
5. Source-level interfaces can be implemented for both targets, but the compiled instruction set and ABI details—registers, syscall numbers, object format and data layouts—differ. Recompilation produces code and metadata for the destination target.
6. The kernel must also validate arguments, addresses, descriptor state, permissions and resource availability. A correct selector can still accompany an invalid or unauthorised request.

Inspecting System Calls#

On Linux, strace records the system calls made by a process:

BASH
strace ./program

To focus on selected calls:

BASH
strace -e trace=openat,read,write,close ./program

The output reveals operations performed indirectly by libraries and by program start-up. Even a tiny C program usually makes more system calls than the lines you wrote suggest, because the dynamic loader and runtime must prepare the process.

Reading the Manual#

The Linux manual is divided into numbered sections. The most relevant sections for COMP1521 are:

Section Contains Example
1 executable commands man 1 ls
2 system calls man 2 read
3 library functions man 3 printf
7 conventions and overview pages man 7 man-pages

When names overlap, the section removes ambiguity. man 2 open documents the system-call interface, whilst another section may document a command or unrelated library interface.

Useful habits include:

BASH
man 2 open
man 2 read
man 3 perror
man 7 errno

The manual gives the required headers, function signature, return convention, errors and relevant standards. It will be available in the exam, so learning how to locate information is more valuable than memorising every error code.

Note

Checkpoint

  • Application code usually calls a library wrapper rather than issuing a raw syscall instruction.
  • The ABI defines where arguments, results, and syscall numbers travel across the boundary.
  • Manuals are part of the interface: check parameters, return values, errors, and required headers together.

Practice: Writing and Debugging#

  1. What is wrong with calling syscall(SYS_write, 1, message, 7); and then assuming seven bytes were written? State the checks a robust caller needs.
  2. Which manual sections would you try for the read system-call wrapper, printf, and an overview of errno conventions?

Note

- Writing and Debugging Answers
7. The call can fail or transfer fewer bytes than requested. Check for -1, inspect errno only after that failure indication, and loop over any remaining bytes when complete output is required.
8. Start with man 2 read, man 3 printf, and man 7 errno (or the implementation's relevant errno page).

Practice: Cumulative#

  1. A program executes printf("hello"), but strace shows no immediate write. Give a valid explanation and name one event which could later cause the bytes to be written.
  2. A process enters the kernel to call getpid() and then resumes immediately. Must a context switch to another process have occurred? Explain the two distinct transitions.

Note

- Cumulative Answers
9. stdio can keep the formatted bytes in a user-space buffer. A newline on a line-buffered terminal, an explicit fflush, a full buffer, fclose, or normal process exit can later trigger output.
10. No. The system call crosses from user mode into kernel mode and back for the same process. A context switch occurs only if the kernel saves that execution context and restores another process or thread's context.

The next note applies this abstraction and protection machinery to Processes. Later, File Systems follows another important OS abstraction—files and file descriptors—from opening a pathname through transferring bytes.