Chapter 3: The Programming Interface
Everything in the operating system must consider a tradeoff between
- Flexibility. It's much easier to modify OS code that lives outside the kernel. For example, the syscall interface must be kept as backwards compatible as possible, making it difficult to change significantly.
- Safety. Obviously.
- Reliability. Kernel modules are not protected from each other, so a bug in kernel code can result in system crashes. Some OS's try to keep the majority of the code outside of the kernel--this is known as a microkernel.
- Performance. Mode switches significantly impact performance. Modern kernels are primarily monolithic kernels (essentially the opposite of a microkernel) for this reason.
3.1 Process Management
Early operating systems had the kernel in control of everything. Modern operating systems allow user programs to create and manage their own processes. A shell, for instance, is a job control system, allowing you to run several instructions in sequence.
UNIX manages processes via fork() and exec(). fork() duplicates the parent process, while exec() replaces the current process with an entirely new one. The parent may wait() until the child terminates.
3.2 Input/Output
The basic ideas of UNIX I/O are
- Uniformity. All device I/O, file operations, and interprocess communication use the same set of system calls:
open,close,read,write. - Open before use. Before an application does I/O, it must first call open. This allows for access permission checks and internal bookkeeping for the OS.
- Byte-oriented. All devices transfer data through byte arrays.
- Kernel-buffered reads. Stream data is stored in a kernel buffer and returned to the application on request. This enables uniformity between devices with streaming reads and those with block reads.
- Kernel-buffered writes. Same thing here.
- Explicit close. When an application finishes with a device/file, it must call close.
There's a bit more to discuss with IPC.
- Pipes. A UNIX pipe is a kernel buffer with a file descriptor for writing and another for reading. Since the data is buffered, producer and consumer execution can be decoupled. The pipe terminates when either endpoint closes the pipe or exits.
- Replace file descriptor. A shell can manipulate the file descriptors of a child process to change stdin/stdout/stderr to files, pipes, etc.
- Wait for multiple reads. The UNIX call
select(fd[], n)allows waiting for input from a set of file descriptors, rather than polling each in turn.
3.4 Interprocess Communication
Three common ways:
- Producer-consumer. Communication is a one-way street. Can be multiple producer or multiple consumer, too. Is typically done through a simple pipe.
- Client-server. Bidirectional communication. Client asks server to perform tasks, and the server responds with results. Is typically done through two pipes, one for each direction.
- File system. Here, communication can be separated temporally. Is typically done through files (consume permanent disk space).
3.5 Operating System Structure
Two types of kernels, as mentioned previously.
-
Monolithic Kernel. Most of the operating system runs inside the kernel. This is the most performant, and is what most modern operating systems used. All modern systems have a hardware abstraction layer and dynamically loaded device drivers. This separates OS development from the hardware.
Notably, though, errors in device drivers can corrupt the kernel. Thus, kernel developers must review drivers extensively before allowing incorporation into the kernel. More modern solutions include user-level device drivers or device drivers isolated by VMs and sandboxing.
-
Microkernel. Minimal kernel space code, with most operating system functionality executing in userspace. This is much easier to develop, as it enables easy modularization and debugging. However, the frequency of context switches reduces performance.