Introduction to Operating Systems
OS Structure and Strategies
-
Definition: An operating system is the software that manages hardware resources and provides services for applications.
-
Core Components:
- Kernel: The central component that manages memory, processes, and hardware
- Device drivers: Interface between hardware and the OS
- File system: Organizes and manages data storage
- Shell/User interface: Allows users to interact with the system
-
Common OS Structures:
- Monolithic: All OS services operate in kernel space
- Example: Traditional Unix, Linux kernel
- Advantage: Efficient direct communication between components
- Disadvantage: Poor isolation, bugs can crash entire system
- Microkernel: Minimal kernel with services in user space
- Example: MINIX, QNX
- Advantage: Better reliability and security through isolation
- Disadvantage: Performance overhead from inter-process communication
- Layered: System divided into hierarchical layers
- Example: THE operating system (historically)
- Advantage: Well-defined interfaces between layers
- Disadvantage: Rigid structure can limit performance
- Hybrid: Combines aspects of multiple approaches
- Example: Windows NT kernel, macOS (XNU kernel)
- Advantage: Balances performance and modularity
- Disadvantage: Added complexity
- Monolithic: All OS services operate in kernel space
-
Protection Rings: Hierarchical privilege levels
- Ring 0: Kernel mode (highest privilege)
- Ring 3: User mode (limited privileges)
- Example: x86 architecture supports rings 0-3, though most OSes only use rings 0 and 3
Process Concepts
-
Definition: A process is a program in execution, representing the dynamic state of a running program.
-
Process Components:
- Program code: The executable instructions
- Process Control Block (PCB): Data structure containing process details
- Process ID, state, program counter, CPU registers
- Memory limits, list of open files, scheduling information
- Stack: Contains temporary data (function parameters, return addresses, local variables)
- Heap: Dynamically allocated memory during runtime
- Data section: Global and static variables
-
Process States:
- New: Process is being created
- Ready: Process is waiting to be assigned to a processor
- Running: Instructions are being executed
- Blocked/Waiting: Process is waiting for some event (I/O completion, resource availability)
- Terminated: Process has finished execution
Example state transition: A process calculating large dataset (Running) → Needs to read from disk (Blocked) → Data available (Ready) → Gets CPU again (Running)
-
Process Creation Mechanisms:
-
fork(): Creates child process (UNIX/Linux)
pid_t pid = fork();if (pid == 0) { // Child process code} else if (pid > 0) { // Parent process code} else { // Error handling}
-
exec(): Replaces current process image with new one
-
CreateProcess(): Windows API for process creation
-
-
Process Termination:
- Normal completion
- Critical error
- User termination
- Parent termination (may cascade to children)
- Resource limits exceeded
Threads
-
Definition: A thread is a basic unit of CPU utilization; lightweight processes within a process.
-
Thread Components:
- Thread ID
- Program counter
- Register set
- Stack
-
Shared Resources Between Threads:
- Code section
- Data section
- OS resources (open files, signals)
- Heap memory
-
Thread Types:
- User-level threads: Managed by user-level thread library
- Examples: POSIX Threads (Pthreads), Java threads
- Advantage: Fast thread switching, customizable scheduling
- Disadvantage: Entire process blocks if one thread makes a blocking system call
- Kernel-level threads: Managed by the operating system
- Examples: Windows threads, Linux native threads
- Advantage: Can take advantage of multiprocessing, one thread blocking doesn’t block others
- Disadvantage: Higher overhead for thread operations
- User-level threads: Managed by user-level thread library
-
Threading Models:
- Many-to-One: Many user-level threads mapped to one kernel thread
- One-to-One: Each user thread mapped to a kernel thread
- Many-to-Many: Multiplexes many user-level threads to a smaller or equal number of kernel threads
-
Thread Implementation Example (POSIX Threads):
#include <pthread.h> void *thread_function(void *arg) { // Thread code here return NULL; } int main() { pthread_t thread_id; pthread_create(&thread_id, NULL, thread_function, NULL); pthread_join(thread_id, NULL); return 0; }
-
Threading Benefits:
- Responsiveness: Application remains responsive while processing
- Resource sharing: Threads share process resources
- Economy: Less overhead than creating new processes
- Scalability: Threads can run in parallel on multiprocessor systems
Inter-Process Communication (IPC)
-
Definition: Mechanisms that allow processes to communicate and synchronize their actions.
-
Shared Memory:
-
Fastest IPC method
-
Region of memory shared between processes
-
Example (POSIX):
// Process 1: Create shared memoryint shm_id = shmget(KEY, SIZE, IPC_CREAT | 0666);char *shared_memory = shmat(shm_id, NULL, 0);strcpy(shared_memory, "Hello from Process 1");// Process 2: Access shared memoryint shm_id = shmget(KEY, SIZE, 0666);char *shared_memory = shmat(shm_id, NULL, 0);printf("Message: %s\n", shared_memory);
-
Challenge: Need synchronization to prevent race conditions
-
-
Message Passing:
-
Processes exchange messages through OS kernel
-
Direct or indirect communication
-
Operations: send(message), receive(message)
-
Example (Message Queues):
// Process 1: Send messageint msgid = msgget(KEY, IPC_CREAT | 0666);msgsnd(msgid, &message, sizeof(message), 0);// Process 2: Receive messageint msgid = msgget(KEY, 0666);msgrcv(msgid, &message, sizeof(message), 0, 0);
-
-
Pipes:
-
Unidirectional communication channel
-
Example (Unnamed pipes between parent-child):
int pipefd[2];pipe(pipefd);if (fork() == 0) { // Child process: Write to pipe close(pipefd[0]); // Close read end write(pipefd[1], "message", 7);} else { // Parent process: Read from pipe close(pipefd[1]); // Close write end char buffer[100]; read(pipefd[0], buffer, sizeof(buffer));}
-
-
Named Pipes (FIFOs):
-
Exist as files in the filesystem
-
Allow unrelated processes to communicate
-
Example:
// Process 1: Write to FIFOmkfifo("/tmp/myfifo", 0666);int fd = open("/tmp/myfifo", O_WRONLY);write(fd, "Hello", 5);// Process 2: Read from FIFOint fd = open("/tmp/myfifo", O_RDONLY);char buffer[100];read(fd, buffer, sizeof(buffer));
-
-
Sockets:
-
Used for network and interprocess communication
-
Example (UNIX domain sockets):
// Serverint server_fd = socket(AF_UNIX, SOCK_STREAM, 0);bind(server_fd, &address, sizeof(address));listen(server_fd, 5);accept(server_fd, NULL, NULL);// Clientint client_fd = socket(AF_UNIX, SOCK_STREAM, 0);connect(client_fd, &address, sizeof(address));
-
-
Remote Procedure Calls (RPC):
- Extends procedure calls across networks
- Example frameworks: gRPC, XML-RPC, SOAP
- Handles marshalling/unmarshalling of parameters
CPU Scheduling Algorithms
-
Definition: Algorithms that determine which process runs when and for how long.
-
Scheduling Criteria:
- CPU utilization: Keep CPU as busy as possible
- Throughput: Number of processes completed per time unit
- Turnaround time: Time from submission to completion
- Waiting time: Total time spent in ready queue
- Response time: Time from submission until first response
-
First-Come, First-Served (FCFS):
- Simplest scheduling algorithm
- Non-preemptive
- Example:
- Process P1 (burst time: 24 ms)
- Process P2 (burst time: 3 ms)
- Process P3 (burst time: 3 ms)
- Execution order: P1 → P2 → P3
- Average waiting time: (0 + 24 + 27)/3 = 17 ms
- Disadvantage: “Convoy effect” - short processes wait for long ones
-
Shortest Job First (SJF):
- Schedules process with shortest execution time first
- Can be preemptive or non-preemptive
- Example (non-preemptive):
- Process P1 (arrival: 0, burst: 7)
- Process P2 (arrival: 2, burst: 4)
- Process P3 (arrival: 4, burst: 1)
- Process P4 (arrival: 5, burst: 4)
- Execution order: P1 → P3 → P2 → P4
- Average waiting time: (0 + 4 + 7 + 8)/4 = 4.75 ms
- Challenge: Difficult to predict execution time
-
Priority Scheduling:
- Assigns priority to each process
- Higher priority processes run first
- Example:
- Process P1 (priority: 3, burst: 10)
- Process P2 (priority: 1, burst: 1)
- Process P3 (priority: 4, burst: 2)
- Process P4 (priority: 2, burst: 1)
- Execution order (highest priority first): P2 → P4 → P1 → P3
- Issue: Starvation of low-priority processes
- Solution: Aging (gradually increase priority of waiting processes)
-
Round Robin (RR):
- Time-slicing approach
- Preemptive
- Example (time quantum = 4ms):
- Process P1 (burst: 20)
- Process P2 (burst: 3)
- Process P3 (burst: 4)
- Execution order: P1(4ms) → P2(3ms) → P3(4ms) → P1(4ms) → P1(4ms) → P1(4ms) → P1(4ms)
- Average waiting time: ((10-4) + (4-3) + (7-4))/3 = 3.33 ms
- Performance depends on time quantum size
-
Multilevel Queue:
- Processes assigned to different queues based on properties
- Each queue has its own scheduling algorithm
- Example:
- Foreground queue (interactive): Round Robin
- Background queue (batch): FCFS
- Usually with priority between queues (e.g., foreground > background)
-
Multilevel Feedback Queue:
- Processes can move between queues
- Example:
- New processes enter highest priority queue
- If a process uses its full time quantum, it moves to a lower priority queue
- If a process doesn’t use its full time quantum, it stays at same level
- Adapts to process behavior
- Used in many modern systems including Windows and Unix variants
Process Synchronization
-
Definition: Mechanisms to coordinate the execution of cooperating processes to maintain data consistency.
-
Race Condition:
-
Multiple processes access and manipulate shared data concurrently
-
Final value depends on order of execution
-
Example:
// Initial value: counter = 5// Process 1counter = counter + 1; // Read 5, add 1, write 6// Process 2 (concurrent)counter = counter + 1; // Read 5, add 1, write 6// Final value: counter = 6 (instead of expected 7)
-
-
Atomic Operations:
-
Operations that complete in a single step relative to other processes
-
Example: Test-and-Set instruction
boolean TestAndSet(boolean *target) { boolean rv = *target; *target = true; return rv;}
-
Critical Section Problem
-
Definition: Problem of designing protocols for processes to cooperate without interfering with each other.
-
Critical Section: Part of a program where shared resources are accessed.
-
Requirements for Solution:
- Mutual Exclusion: Only one process can execute in the critical section at a time
- Progress: If no process is in the critical section, a process wanting to enter must be able to (eventually)
- Bounded Waiting: There must be a limit on how many times other processes can enter their critical sections after a process has requested entry
-
Peterson’s Solution (for two processes):
// Shared variables int turn; boolean flag[2] = {false, false}; // Process i flag[i] = true; turn = j; // j is the other process while (flag[j] && turn == j); // Critical section flag[i] = false;
-
Hardware-Based Solutions:
-
Test-and-Set Lock:
do { while (TestAndSet(&lock)); // Critical section lock = false; // Remainder section } while (true);
-
Compare-and-Swap:
do { while (CompareAndSwap(&lock, 0, 1) != 0); // Critical section lock = 0; // Remainder section } while (true);
-
Semaphores
-
Definition: A synchronization tool that uses a counter to control access to shared resources.
-
Types of Semaphores:
- Binary Semaphore: Can only have values 0 or 1
- Counting Semaphore: Can have arbitrary non-negative values
-
Operations:
-
wait(S) or P(S):
while (S <= 0); // busy waiting S--;
-
signal(S) or V(S):
S++;
-
-
Implementation:
typedef struct { int value; struct process *list; } semaphore; void wait(semaphore *S) { S->value--; if (S->value < 0) { // Add this process to S->list block(); } } void signal(semaphore *S) { S->value++; if (S->value <= 0) { // Remove a process P from S->list wakeup(P); } }
-
Semaphore Applications:
-
Mutual Exclusion:
semaphore mutex = 1; wait(mutex); // Critical section signal(mutex);
-
Producer-Consumer Problem:
semaphore mutex = 1; semaphore empty = n; // Initially all slots are empty semaphore full = 0; // Initially no slots are full // Producer wait(empty); wait(mutex); // Add item to buffer signal(mutex); signal(full); // Consumer wait(full); wait(mutex); // Remove item from buffer signal(mutex); signal(empty);
-
Readers-Writers Problem:
semaphore mutex = 1; // Controls access to readcount semaphore wrt = 1; // Controls access to the resource int readcount = 0; // Number of readers accessing resource // Writer wait(wrt); // Write data signal(wrt); // Reader wait(mutex); readcount++; if (readcount == 1) wait(wrt); // First reader blocks writers signal(mutex); // Read data wait(mutex); readcount--; if (readcount == 0) signal(wrt); // Last reader allows writers signal(mutex);
-
-
Deadlock and Starvation:
- Deadlock: Two or more processes are waiting indefinitely for an event that can only be caused by one of the waiting processes
-
Example:
Process 1: wait(S), wait(Q)Process 2: wait(Q), wait(S)
-
- Starvation: Indefinite blocking of a process from accessing a resource
- Example: In readers-writers, writers may starve if readers keep arriving
- Deadlock: Two or more processes are waiting indefinitely for an event that can only be caused by one of the waiting processes
Monitors
-
Definition: A high-level synchronization construct that encapsulates both data and procedures in a module.
-
Components:
- Mutex (automatic, implicit): Only one process can be active within the monitor at a time
- Condition variables: Allow processes to wait for certain conditions
-
Operations on Condition Variables:
- wait(): Process is suspended until another process signals
- signal(): Resumes one waiting process
- broadcast(): Resumes all waiting processes
-
Monitor Example (Java-like syntax):
monitor BoundedBuffer { final int N = 10; int count = 0; Object buffer[N]; int in = 0, out = 0; condition not_full, not_empty; void deposit(Object item) { if (count == N) not_full.wait(); buffer[in] = item; in = (in + 1) % N; count++; not_empty.signal(); } Object fetch() { if (count == 0) not_empty.wait(); Object item = buffer[out]; out = (out + 1) % N; count--; not_full.signal(); return item; } }
-
Implementation Considerations:
- Signal and Continue: After signaling, the signaling process continues execution
- Signal and Wait: After signaling, the signaling process waits and the signaled process executes
- Different languages implement monitors differently (e.g., Java synchronized methods/blocks, C# lock statement)
-
Differences from Semaphores:
- Monitors provide mutual exclusion automatically
- Monitors are higher-level abstractions, easier to use correctly
- Condition variables in monitors are more structured than semaphores for complex synchronization
-
Monitor Usage in Modern Languages:
-
Java:
public class SharedResource { private int count = 0; public synchronized void increment() { count++; notifyAll(); // Similar to condition.signal() } public synchronized void waitForCount(int value) throws InterruptedException { while (count < value) wait(); // Similar to condition.wait() } }
-
C#:
class SharedResource { private int count = 0; private object lockObj = new object(); public void Increment() { lock (lockObj) { count++; Monitor.PulseAll(lockObj); // Similar to condition.signal() } } public void WaitForCount(int value) { lock (lockObj) { while (count < value) Monitor.Wait(lockObj); // Similar to condition.wait() } } }
-