🖥️ Operating System Notes

Complete Guide for Placement Preparation - PrepCampus

📋 Course Overview

Course Objectives:
  • Understand core OS concepts and architecture
  • Manage processes, memory, storage, and I/O effectively
  • Learn synchronization, concurrency, and deadlock resolution
  • Explore file systems, protection mechanisms, and modern OS case studies

Unit I: OS Overview & Structure

1.1 Definition and Functions

Operating System: An operating system is a comprehensive system software that serves as the fundamental interface between computer hardware and user applications. It is a complex software system that manages all hardware resources, provides essential services to application programs, and creates a user-friendly environment for human-computer interaction. The OS acts as a resource manager, coordinator, and service provider that ensures efficient, secure, and reliable operation of the entire computing system.
Detailed Functions of an Operating System:
  • Resource Management: Efficiently allocates and manages CPU, memory, storage, and I/O devices among competing processes, ensuring optimal utilization and preventing resource conflicts
  • Process Management: Creates, schedules, terminates, and coordinates processes, handling process synchronization, communication, and deadlock prevention
  • Memory Management: Manages primary and secondary memory, implementing virtual memory systems, memory protection, and efficient memory allocation strategies
  • File System Management: Organizes and manages files and directories, providing hierarchical storage, access control, data persistence, and backup mechanisms
  • Device Management: Controls and coordinates hardware devices through device drivers, handling device allocation, buffering, and error recovery
  • Security and Protection: Implements comprehensive access control, user authentication, data encryption, and system protection mechanisms
  • User Interface: Provides both command-line and graphical interfaces for user interaction, enabling efficient human-computer communication
  • Network Management: Handles network communication, protocols, distributed computing support, and remote resource access
  • Error Handling: Detects, reports, and recovers from hardware and software errors, ensuring system stability and reliability
  • Performance Monitoring: Tracks system performance metrics, resource usage patterns, and provides optimization recommendations

Core Functions of OS:

1.2 Types of Operating Systems

Batch Operating System

Definition: A batch operating system is a type of OS that processes jobs in groups or batches without user interaction. Jobs with similar requirements are grouped together and executed sequentially, maximizing system utilization and throughput while minimizing idle time.
Detailed Characteristics:
  • Batch Processing: Jobs are collected and executed in batches without user interaction during execution
  • High Throughput: Optimized for processing large volumes of similar jobs efficiently
  • Low Response Time: No immediate user interaction, resulting in longer turnaround times
  • Job Scheduling: Uses sophisticated algorithms to optimize job ordering and resource allocation
  • Resource Efficiency: Maximizes CPU and memory utilization through batch optimization
  • Error Handling: Comprehensive error detection and recovery mechanisms for batch operations
  • Historical Context: Developed for early mainframe systems where computing resources were expensive and limited
  • Modern Applications: Still used in data processing centers, scientific computing, and large-scale data analysis

Time-Sharing Operating System

Definition: A time-sharing operating system allows multiple users to access and use computer resources simultaneously by rapidly switching between different user programs. It provides each user with the illusion of having exclusive access to the computer while efficiently sharing CPU time and other resources among all active users.
Detailed Characteristics:
  • Multi-User Support: Multiple users can access the system simultaneously through terminals or network connections
  • CPU Time Sharing: CPU time is divided into small time slices and allocated among multiple users in a round-robin fashion
  • Interactive Response: Provides quick response times for interactive applications and user commands
  • Resource Sharing: Efficiently shares memory, storage, and peripheral devices among multiple users
  • Process Scheduling: Implements sophisticated scheduling algorithms to ensure fair resource allocation
  • Memory Management: Uses virtual memory and paging to support multiple concurrent processes
  • Security and Isolation: Provides user authentication, access control, and process isolation for security
  • File System: Hierarchical file system with user-specific directories and access permissions
  • Network Support: Built-in networking capabilities for remote access and distributed computing
  • Modern Examples: Unix, Linux, Windows, macOS, Android, iOS

Real-Time Operating System

Definition: A real-time operating system (RTOS) is designed to process data and respond to events within strict time constraints. It guarantees that critical tasks are completed within specified deadlines, making it essential for applications where timing is critical for system safety and functionality.
Detailed Characteristics:
  • Predictable Timing: Guarantees response times within specified limits for critical operations
  • Deterministic Behavior: Provides consistent and predictable system behavior under all conditions
  • Priority-Based Scheduling: Uses priority-based scheduling to ensure critical tasks meet deadlines
  • Minimal Latency: Minimizes interrupt latency and context switching overhead
  • Resource Management: Efficient resource allocation with predictable timing characteristics
  • Error Handling: Robust error detection and recovery mechanisms for system reliability
  • Two Types:
    • Hard Real-Time: Missing a deadline results in system failure (e.g., medical devices, aircraft control)
    • Soft Real-Time: Missing a deadline degrades performance but doesn't cause failure (e.g., multimedia streaming)
  • Applications: Medical devices, automotive systems, industrial automation, aerospace, telecommunications
  • Examples: VxWorks, QNX, FreeRTOS, RTEMS, Windows CE, Linux with RT patches

1.3 OS Services and System Calls

OS Services:

System Calls:

// Example of system calls in C
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    // Process control system calls
    pid_t pid = fork();  
    // Create new process
    
    if (pid == 0) {
        // Child process
        printf("Child process executing\n");
        exit(0);
    } else if (pid > 0) {
        // Parent process
        wait(NULL);  
        // Wait for child to complete
        printf("Child process completed\n");
    }
    
    // File manipulation system calls
    int fd = open("file.txt", O_RDONLY);  
    // Open file for reading
    
    if (fd == -1) {
        perror("Error opening file");
        return 1;
    }
    
    char buffer[1024];
    int bytes_read = read(fd, buffer, 1024);  
    // Read from file
    
    if (bytes_read > 0) {
        printf("Read %d bytes\n", bytes_read);
    }
    
    close(fd);  
    // Close file descriptor
    
    // Device management system calls
    int device_fd = open("/dev/device", O_RDWR);
    if (device_fd != -1) {
        ioctl(device_fd, command, arg);  
        // Device control operation
        close(device_fd);
    }
    
    return 0;
}

1.4 Architecture Styles

Monolithic Architecture

Definition: A monolithic architecture is an OS design where all operating system services run in kernel mode as a single large program. The entire operating system runs as a single process in kernel space, with all system calls, device drivers, file systems, and memory management functions integrated into one large executable. This architecture provides direct access to hardware and efficient system calls but can be complex to maintain and debug.
Detailed Characteristics:
  • Single Kernel Space: All OS components run in privileged kernel mode with direct hardware access
  • Efficient System Calls: Direct function calls between OS components without inter-process communication overhead
  • Hardware Access: Direct access to hardware resources without abstraction layers
  • Performance: High performance due to minimal overhead and direct memory access
  • Complexity: Large, complex codebase that can be difficult to maintain and debug
  • Reliability: A bug in any component can crash the entire system
  • Security: All components run with full privileges, increasing security risks
  • Examples: Traditional Unix kernels, early Linux kernels, MS-DOS
Advantages:
  • Better performance due to direct communication
  • Efficient resource sharing
  • Simple design and implementation
Disadvantages:
  • Difficult to maintain and modify
  • System crash affects entire OS
  • Large kernel size
Example: Traditional Unix, Linux (partially)

Microkernel Architecture

Definition: A microkernel architecture is an OS design where only essential core services run in kernel mode, while all other operating system services run as separate user processes. The microkernel provides only basic services like process management, memory management, and inter-process communication (IPC), with all other services (file systems, device drivers, networking) implemented as user-space servers that communicate through message passing.
Detailed Characteristics:
  • Minimal Kernel: Kernel contains only essential services: process management, memory management, and IPC
  • User-Space Services: File systems, device drivers, networking, and other services run as user processes
  • Message Passing: Communication between services occurs through well-defined message passing interfaces
  • Modularity: Services can be added, removed, or modified without affecting the core kernel
  • Fault Isolation: A failure in one service doesn't crash the entire system
  • Security: Services run with limited privileges, reducing security risks
  • Portability: Core kernel is hardware-independent and easily portable
  • Performance Overhead: Message passing introduces communication overhead
  • Complexity: More complex design due to distributed service architecture
  • Examples: QNX, MINIX, Mach, L4, seL4, HelenOS

1.5 Generations of Operating Systems

Generation Period Characteristics Examples
First (1945-1955) 1945-1955 Vacuum tubes, no OS, machine language ENIAC, UNIVAC
Second (1955-1965) 1955-1965 Transistors, batch systems, assembly language IBM 1401, FORTRAN
Third (1965-1980) 1965-1980 Integrated circuits, multiprogramming, time-sharing Unix, Multics
Fourth (1980-1990) 1980-1990 Personal computers, GUI, networking MS-DOS, Mac OS
Fifth (1990-Present) 1990-Present Mobile computing, cloud, virtualization Android, iOS, Windows 10

Unit II: Process & Thread Management

2.1 Concept of Processes

Process: A process is a program in execution that represents the basic unit of work in an operating system. It is a dynamic entity that consists of an executable program, associated data, and system resources. A process is created when a program is loaded into memory and begins execution, and it terminates when the program completes or is terminated. Each process has its own address space, system resources, and execution context that are isolated from other processes.
Detailed Process Components:
  • Program Counter (PC): Points to the next instruction to execute in the program
  • Process Stack: Contains temporary data, function parameters, return addresses, and local variables
  • Data Section: Contains global variables and static data that persist throughout program execution
  • Heap: Memory dynamically allocated during runtime using functions like malloc() and free()
  • Text Section: Contains the actual program code and instructions that are executed
  • Process State: Current state of the process (new, ready, running, waiting, terminated)
  • CPU Registers: Current values of all CPU registers including general-purpose and special-purpose registers
  • Process ID (PID): Unique identifier assigned to each process by the operating system
  • Parent Process ID (PPID): Identifier of the process that created this process
  • File Descriptors: List of open files and their associated file descriptors
  • Memory Limits: Information about memory allocation and limits for the process
  • Priority and Scheduling Information: Process priority, scheduling parameters, and CPU usage statistics

Process Control Block (PCB)

// Structure of Process Control Block
struct PCB {
    // Process identification
    int process_id;           
    // Unique process identifier
    int parent_id;            
    // Parent process ID
    
    // Process state information
    char process_state;        
    // New, Ready, Running, Waiting, Terminated
    int priority;              
    // Process priority level
    
    // CPU state information
    int program_counter;       
    // Address of next instruction
    int cpu_registers[16];    
    // General purpose registers
    int stack_pointer;         
    // Current stack pointer
    int base_pointer;          
    // Current base pointer
    
    // Memory management information
    int memory_limits;         
    // Memory allocation limits
    int page_table_ptr;        
    // Pointer to page table
    int memory_usage;          
    // Current memory usage
    
    // File management information
    int list_of_open_files;    
    // List of open file descriptors
    int working_directory;     
    // Current working directory
    
    // CPU scheduling information
    int cpu_usage;             
    // Total CPU time used
    int waiting_time;          
    // Time spent in waiting queue
    int arrival_time;          
    // Time when process arrived
    int burst_time;            
    // Estimated CPU burst time
    
    // Process communication information
    int message_queue;         
    // Message queue for IPC
    int semaphores;            
    // Associated semaphores
    
    // Process hierarchy information
    int child_processes[10]; 
    // Array of child process IDs
    int num_children;          
    // Number of child processes
};

Process Life Cycle

Process States:
  1. New: Process is being created
  2. Ready: Process is waiting to be assigned to a processor
  3. Running: Instructions are being executed
  4. Waiting: Process is waiting for some event to occur
  5. Terminated: Process has finished execution

Context Switching

Context Switch: A context switch is the process of saving the current state (context) of a running process or thread and loading the saved state of another process or thread so that execution can resume from the same point later. This is a fundamental mechanism in multitasking operating systems that allows multiple processes to share a single CPU by rapidly switching between them, creating the illusion of concurrent execution. The context switch involves saving all the information needed to restore the process later, including CPU registers, program counter, memory management information, and other process-specific data.
Detailed Context Switch Process:
  • State Preservation: Saves the current process's CPU registers (general-purpose registers, program counter, stack pointer, base pointer, status registers), memory management information (page table pointers, memory limits), and process-specific data (file descriptors, working directory, process state)
  • Process Control Block Update: Updates the Process Control Block (PCB) of the current process with the saved context information, including CPU usage statistics, waiting time, and current state
  • Scheduler Invocation: Calls the CPU scheduler to determine which process should run next based on scheduling algorithms and process priorities
  • Memory Management Switch: Switches memory management context, including page tables, memory protection settings, and virtual memory mappings for the new process
  • State Restoration: Loads the saved context of the new process from its PCB, restoring all CPU registers, program counter, and process-specific information
  • Execution Transfer: Transfers control to the new process, resuming execution from the exact point where it was previously interrupted
  • Cache Invalidation: May invalidate CPU cache entries to ensure the new process doesn't access stale data from the previous process
  • Performance Overhead: Context switching has significant overhead due to the time required to save and restore process state, making it a critical factor in system performance

2.2 CPU Scheduling

Scheduler Types

Scheduling Algorithms

First Come First Serve (FCFS)
// FCFS Scheduling Algorithm
void fcfs_scheduling(struct Process processes[], int n) {
    int current_time = 0;
    float total_waiting_time = 0;
    
    for(int i = 0; i < n; i++) {
        // Process arrives at current_time
        if(current_time < processes[i].arrival_time) {
            current_time = processes[i].arrival_time;
        }
        
        // Calculate waiting time
        processes[i].waiting_time = current_time - processes[i].arrival_time;
        total_waiting_time += processes[i].waiting_time;
        
        // Execute process
        current_time += processes[i].burst_time;
        processes[i].completion_time = current_time;
    }
    
    float avg_waiting_time = total_waiting_time / n;
    printf("Average Waiting Time: %.2f\n", avg_waiting_time);
}
Shortest Job First (SJF)
// SJF Scheduling Algorithm (Non-preemptive)
void sjf_scheduling(struct Process processes[], int n) {
    int current_time = 0;
    int completed = 0;
    
    while(completed < n) {
        int shortest_job = -1;
        int min_burst = INT_MAX;
        
        // Find shortest job that has arrived
        for(int i = 0; i < n; i++) {
            if(processes[i].arrival_time <= current_time && 
               processes[i].remaining_time > 0 && 
               processes[i].burst_time < min_burst) {
                shortest_job = i;
                min_burst = processes[i].burst_time;
            }
        }
        
        if(shortest_job == -1) {
            current_time++;
            continue;
        }
        
        // Execute the shortest job
        processes[shortest_job].waiting_time = current_time - processes[shortest_job].arrival_time;
        current_time += processes[shortest_job].burst_time;
        processes[shortest_job].completion_time = current_time;
        processes[shortest_job].remaining_time = 0;
        completed++;
    }
}
Round Robin (RR)
// Round Robin Scheduling Algorithm
void round_robin(struct Process processes[], int n, int quantum) {
    int current_time = 0;
    int completed = 0;
    
    while(completed < n) {
        for(int i = 0; i < n; i++) {
            if(processes[i].remaining_time > 0 && 
               processes[i].arrival_time <= current_time) {
                
                if(processes[i].remaining_time > quantum) {
                    current_time += quantum;
                    processes[i].remaining_time -= quantum;
                } else {
                    current_time += processes[i].remaining_time;
                    processes[i].waiting_time = current_time - processes[i].arrival_time - processes[i].burst_time;
                    processes[i].remaining_time = 0;
                    completed++;
                }
            }
        }
    }
}
Priority Scheduling
// Priority Scheduling Algorithm
void priority_scheduling(struct Process processes[], int n) {
    int current_time = 0;
    int completed = 0;
    
    while(completed < n) {
        int highest_priority = -1;
        int min_priority = INT_MAX;
        
        // Find highest priority job that has arrived
        for(int i = 0; i < n; i++) {
            if(processes[i].arrival_time <= current_time && 
               processes[i].remaining_time > 0 && 
               processes[i].priority < min_priority) {
                highest_priority = i;
                min_priority = processes[i].priority;
            }
        }
        
        if(highest_priority == -1) {
            current_time++;
            continue;
        }
        
        // Execute the highest priority job
        processes[highest_priority].waiting_time = current_time - processes[highest_priority].arrival_time;
        current_time += processes[highest_priority].burst_time;
        processes[highest_priority].completion_time = current_time;
        processes[highest_priority].remaining_time = 0;
        completed++;
    }
}

2.3 Threads

Thread vs Process

Characteristic Process Thread
Definition Program in execution Segment of a process
Creation Independent Within process
Memory Separate memory space Shared memory space
Context Switch Expensive Inexpensive
Communication Inter-process communication Shared memory

Thread Implementation

User Threads
// User Thread Implementation in C
#include <pthread.h>

void* thread_function(void* arg) {
    int thread_id = *(int*)arg;
    printf("Thread %d is running\n", thread_id);
    return NULL;
}

int main() {
    pthread_t threads[5];
    int thread_ids[5];
    
    // Create threads
    for(int i = 0; i < 5; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_function, &thread_ids[i]);
    }
    
    // Wait for threads to complete
    for(int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }
    
    printf("All threads completed\n");
    return 0;
}

2.4 Process Synchronization

Critical Section Problem

Critical Section: A critical section is a segment of code in a multi-process or multi-threaded program where shared resources (such as variables, data structures, files, or hardware devices) are accessed or modified. It is called "critical" because if multiple processes or threads execute this code simultaneously, it can lead to race conditions, data corruption, inconsistent states, or other unpredictable behavior. The fundamental requirement is that only one process or thread should execute in its critical section at any given time, ensuring mutual exclusion and data integrity.
Detailed Characteristics of Critical Sections:
  • Mutual Exclusion: Ensures that only one process can execute the critical section at any time, preventing concurrent access to shared resources
  • Progress: If no process is executing in its critical section, then a process that wants to enter its critical section should be allowed to do so without unnecessary delay
  • Bounded Waiting: A process should not wait indefinitely to enter its critical section, preventing starvation of processes
  • Shared Resource Protection: Protects shared variables, data structures, files, databases, or hardware devices from concurrent access
  • Race Condition Prevention: Prevents race conditions where the outcome depends on the relative timing of process execution
  • Data Consistency: Ensures that shared data remains in a consistent and valid state throughout program execution
  • Atomic Operations: Makes operations on shared resources appear atomic (indivisible) to other processes
  • Deadlock Prevention: Must be designed to avoid deadlock situations where processes wait indefinitely for each other

Peterson's Algorithm

// Peterson's Algorithm for Two Processes
int turn = 0;
int flag[2] = {0, 0};

void enter_critical_section(int process_id) {
    int other = 1 - process_id;
    
    flag[process_id] = 1;  
    // Show interest
    turn = other;        
    // Give turn to other process
    
    // Wait until other process is not interested or it's our turn
    while(flag[other] == 1 && turn == other) {
        // Busy wait
    }
}

void exit_critical_section(int process_id) {
    flag[process_id] = 0;  
    // No longer interested
}

Semaphores

// Semaphore Implementation
#include <semaphore.h>

sem_t mutex;  
// Binary semaphore for mutual exclusion
sem_t empty;  
// Counting semaphore for empty slots
sem_t full;   
// Counting semaphore for full slots

void producer() {
    while(1) {
        // Produce item
        sem_wait(&empty);  
        // Wait for empty slot
        sem_wait(&mutex);  
        // Enter critical section
        
        // Add item to buffer
        
        sem_post(&mutex);  
        // Exit critical section
        sem_post(&full);   
        // Signal that buffer is full
    }
}

void consumer() {
    while(1) {
        sem_wait(&full);   
        // Wait for full slot
        sem_wait(&mutex);  
        // Enter critical section
        
        // Remove item from buffer
        
        sem_post(&mutex);  
        // Exit critical section
        sem_post(&empty);  
        // Signal that buffer is empty
    }
}

Mutexes

// Mutex Implementation
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* thread_function(void* arg) {
    pthread_mutex_lock(&mutex);  
    // Acquire lock
    
    // Critical section
    printf("Thread %d in critical section\n", *(int*)arg);
    
    pthread_mutex_unlock(&mutex);  
    // Release lock
    return NULL;
}

Classic Concurrency Problems

Dining Philosophers Problem
// Dining Philosophers Problem Solution
#include <pthread.h>
#include <semaphore.h>

#define N 5  
// Number of philosophers

sem_t chopsticks[N];
sem_t mutex;

void* philosopher(void* arg) {
    int id = *(int*)arg;
    int left = id;
    int right = (id + 1) % N;
    
    while(1) {
        // Think
        printf("Philosopher %d is thinking\n", id);
        
        // Pick up chopsticks
        sem_wait(&mutex);
        sem_wait(&chopsticks[left]);
        sem_wait(&chopsticks[right]);
        sem_post(&mutex);
        
        // Eat
        printf("Philosopher %d is eating\n", id);
        
        // Put down chopsticks
        sem_post(&chopsticks[left]);
        sem_post(&chopsticks[right]);
    }
    
    return NULL;
}
Readers-Writers Problem
// Readers-Writers Problem Solution
#include <pthread.h>
#include <semaphore.h>

sem_t mutex;      
// Protects read_count
sem_t write_lock; 
// Protects the shared resource
int read_count = 0;

void* reader(void* arg) {
    int id = *(int*)arg;
    
    while(1) {
        sem_wait(&mutex);
        read_count++;
        if(read_count == 1) {
            sem_wait(&write_lock);  
            // First reader blocks writers
        }
        sem_post(&mutex);
        
        // Read the shared resource
        printf("Reader %d is reading\n", id);
        
        sem_wait(&mutex);
        read_count--;
        if(read_count == 0) {
            sem_post(&write_lock);  
            // Last reader allows writers
        }
        sem_post(&mutex);
    }
    
    return NULL;
}

void* writer(void* arg) {
    int id = *(int*)arg;
    
    while(1) {
        sem_wait(&write_lock);
        
        // Write to the shared resource
        printf("Writer %d is writing\n", id);
        
        sem_post(&write_lock);
    }
    
    return NULL;
}

Unit III: Deadlocks & Memory Management

3.1 Deadlock

Deadlock Definition

Deadlock: A deadlock is a situation in a multi-process system where two or more processes are unable to proceed because each is waiting for a resource that is held by another process in the set. It is a state of permanent blocking where processes are stuck in a circular wait condition, preventing any of them from making progress. Deadlocks are a fundamental problem in concurrent systems and can cause system-wide performance degradation or complete system failure if not properly managed.
Detailed Characteristics of Deadlocks:
  • Permanent Blocking: Once a deadlock occurs, the involved processes remain blocked indefinitely unless external intervention occurs
  • Resource Contention: Deadlocks arise from competition for limited system resources such as CPU, memory, files, or hardware devices
  • Circular Dependency: Processes form a circular chain where each process holds resources needed by the next process in the chain
  • System-wide Impact: Deadlocks can affect multiple processes and potentially the entire system, not just the directly involved processes
  • Predictable Conditions: Deadlocks occur only when all four necessary conditions are simultaneously satisfied
  • Detection Difficulty: Deadlocks can be difficult to detect in complex systems with many processes and resources
  • Recovery Complexity: Resolving deadlocks often requires process termination or resource preemption, which can be disruptive

Deadlock Conditions

Four necessary conditions for deadlock:
  1. Mutual Exclusion: At least one resource must be non-shareable, meaning only one process can use it at a time. This condition prevents multiple processes from simultaneously accessing the same resource, creating the potential for resource contention.
  2. Hold and Wait: A process must be holding at least one resource and waiting for additional resources that are currently held by other processes. This condition allows processes to hold resources while waiting for others, creating the potential for circular waiting.
  3. No Preemption: Resources cannot be forcibly removed from a process that is holding them. The process must voluntarily release the resource when it completes its task, preventing forced resource reallocation.
  4. Circular Wait: A set of processes must be waiting for each other in a circular manner, where each process holds a resource needed by the next process in the chain, creating a closed loop of resource dependencies.

Deadlock Prevention

Banker's Algorithm

// Banker's Algorithm Implementation
#define MAX_PROCESSES 5
#define MAX_RESOURCES 3

int available[MAX_RESOURCES];
int maximum[MAX_PROCESSES][MAX_RESOURCES];
int allocation[MAX_PROCESSES][MAX_RESOURCES];
int need[MAX_PROCESSES][MAX_RESOURCES];
int finish[MAX_PROCESSES] = {0};

int is_safe_state() {
    int work[MAX_RESOURCES];
    int safe_sequence[MAX_PROCESSES];
    int count = 0;
    
    // Initialize work array
    for(int i = 0; i < MAX_RESOURCES; i++) {
        work[i] = available[i];
    }
    
    // Find a process that can be allocated resources
    while(count < MAX_PROCESSES) {
        int found = 0;
        
        for(int i = 0; i < MAX_PROCESSES; i++) {
            if(!finish[i]) {
                int can_allocate = 1;
                
                for(int j = 0; j < MAX_RESOURCES; j++) {
                    if(need[i][j] > work[j]) {
                        can_allocate = 0;
                        break;
                    }
                }
                
                if(can_allocate) {
                    for(int j = 0; j < MAX_RESOURCES; j++) {
                        work[j] += allocation[i][j];
                    }
                    safe_sequence[count] = i;
                    finish[i] = 1;
                    count++;
                    found = 1;
                }
            }
        }
        
        if(!found) {
            return 0; 
            // Deadlock detected
        }
    }
    
    return 1; 
    // Safe state
}

3.2 Memory Management

Memory Allocation Techniques

Memory Allocation: Memory allocation is the process of assigning portions of computer memory to programs and processes for their execution. The operating system manages memory allocation to ensure efficient use of available memory resources, prevent memory conflicts between processes, and provide optimal performance. Different allocation techniques are used depending on the system requirements, memory constraints, and performance goals.
1. Partitioning Techniques:
  • Fixed Partitioning: Memory is divided into fixed-size partitions of predetermined sizes. Each partition can hold exactly one process, and processes must fit entirely within a partition. This method is simple to implement but suffers from internal fragmentation when processes are smaller than their allocated partitions.
  • Variable Partitioning: Memory is divided dynamically based on the actual size of processes. Partitions are created and destroyed as processes arrive and depart, allowing for more efficient memory utilization. This method eliminates internal fragmentation but may create external fragmentation over time.
  • Best Fit: Allocates the smallest partition that is large enough to hold the process. This minimizes internal fragmentation by finding the closest match between process size and partition size, but requires searching through all available partitions.
  • Worst Fit: Allocates the largest available partition to a process. This approach leaves larger remaining fragments, which may be more useful for future allocations, but can lead to inefficient memory utilization.
  • First Fit: Allocates the first partition that is large enough to hold the process. This is the fastest allocation method as it stops searching at the first suitable partition, but may not be the most efficient in terms of memory utilization.

Paging

// Page Table Entry Structure
struct page_table_entry {
    int frame_number;     
    // Physical frame number
    int valid_bit;        
    // 1 if page is in memory
    int dirty_bit;        
    // 1 if page has been modified
    int reference_bit;    
    // 1 if page has been accessed
    int protection;       
    // Read/write permissions
};

Virtual Memory

Virtual Memory: Virtual memory is a memory management technique that allows a computer to use more memory than is physically available by temporarily transferring data from random access memory (RAM) to disk storage. It creates an illusion of a large, contiguous memory space by combining physical memory and disk storage, enabling programs to run even when they require more memory than is physically available. Virtual memory provides memory isolation between processes, efficient memory utilization, and support for programs larger than physical memory.
Detailed Virtual Memory Characteristics:
  • Memory Abstraction: Provides a uniform address space that is independent of the actual physical memory layout
  • Process Isolation: Each process has its own virtual address space, preventing one process from accessing another's memory
  • Memory Protection: Implements access control mechanisms to prevent unauthorized memory access and ensure system security
  • Demand Paging: Pages are loaded into memory only when needed, reducing initial memory requirements and improving system performance
  • Page Sharing: Allows multiple processes to share the same physical pages for code and data, reducing memory usage
  • Memory Overcommitment: Enables the system to allocate more virtual memory than physical memory, improving memory utilization
  • Transparent Implementation: Applications are unaware of the virtual memory system and work with virtual addresses transparently
Page Replacement Algorithms:
  • FIFO (First In First Out): Replaces the oldest page in memory, simple to implement but may not consider page usage patterns
  • LRU (Least Recently Used): Replaces the page that has not been accessed for the longest time, based on the principle of locality
  • Optimal: Replaces the page that will not be used for the longest time in the future, theoretical algorithm used for comparison
  • Clock Algorithm: Uses a circular buffer with a reference bit to approximate LRU behavior with lower overhead
  • Second Chance: Gives pages a second chance before replacement by checking the reference bit
  • Working Set: Keeps only the pages that are actively being used by each process in memory

Thrashing

Thrashing: Thrashing is a severe performance degradation condition in virtual memory systems where the operating system spends an excessive amount of time swapping pages between main memory and secondary storage (disk) instead of executing actual user processes. This occurs when the system is overwhelmed with page faults, causing a continuous cycle of page swapping that dramatically reduces system performance and responsiveness.
Detailed Causes and Characteristics of Thrashing:
  • Memory Overcommitment: When too many processes are competing for limited physical memory, causing frequent page faults and continuous swapping
  • Inefficient Page Replacement: Poor page replacement algorithms that don't consider locality of reference, leading to unnecessary page swaps
  • Working Set Exceedance: When the total working set size of all active processes exceeds the available physical memory capacity
  • Locality Violation: Processes that don't exhibit good spatial or temporal locality, causing frequent page faults
  • Memory Fragmentation: Internal or external fragmentation that reduces effective memory utilization
  • I/O Bottleneck: Slow disk I/O operations that cause page swapping to become a major performance bottleneck
  • System Response Degradation: Dramatic increase in response time and decrease in throughput as the system becomes unresponsive
  • CPU Utilization Drop: CPU utilization drops significantly as the system spends most time waiting for I/O operations

Unit IV: File Systems & I/O Management

4.1 File Systems

File Concept

File: A file is a named collection of related information that is stored on secondary storage devices such as hard disks, solid-state drives, or optical media. Files serve as the basic unit of data storage and organization in computer systems, providing a logical abstraction over physical storage devices. Files can contain various types of data including text, images, programs, databases, or any other digital information, and they are managed by the operating system's file system component.
Detailed File Characteristics:
  • Persistence: Files provide permanent storage that survives system reboots and power failures, unlike volatile memory
  • Named Access: Files are accessed through human-readable names that provide a convenient way to reference stored data
  • Hierarchical Organization: Files are organized in directory structures that allow logical grouping and easy navigation
  • Access Control: Files support various protection mechanisms including read, write, execute permissions and user-based access control
  • Metadata Storage: Files store additional information about themselves including creation time, modification time, size, and ownership
  • Device Independence: Files provide a uniform interface regardless of the underlying storage device characteristics
  • Sharing Capability: Files can be shared between multiple users and processes with appropriate access controls
  • Backup and Recovery: Files support backup and recovery operations for data protection and disaster recovery
File Attributes:
  • Name: Human-readable identifier that uniquely identifies the file within its directory context
  • Type: File extension or internal type that indicates the file's format and how it should be processed
  • Location: Pointer to file location on device, including device ID, track, sector, and block information
  • Size: Current file size in bytes, which may change as the file is modified during its lifetime
  • Protection: Access control information including read, write, execute permissions for owner, group, and others
  • Time, Date, User ID: Metadata for tracking creation time, last modification time, last access time, and file ownership

Directory Structures

// Directory Entry Structure
struct directory_entry {
    char filename[32];
    int inode_number;
    char file_type;        
    // 'f' for file, 'd' for directory
    long file_size;
    time_t creation_time;
    time_t modification_time;
};

File Allocation Methods

1. Contiguous Allocation:
  • Storage Method: Files are stored in consecutive blocks on the disk, with each file occupying a continuous sequence of disk blocks
  • Fast Sequential Access: Excellent performance for sequential file access since all blocks are adjacent and can be read in a single disk operation
  • External Fragmentation Problem: Creates external fragmentation as files are created and deleted, leaving gaps that may be too small for new files
  • File Size Limitation: Requires knowing the maximum file size in advance, which can be difficult to predict
  • Simple Implementation: Straightforward to implement and manage, with direct block addressing
2. Linked Allocation:
  • Storage Method: Files are stored as a linked list of disk blocks, where each block contains a pointer to the next block in the file
  • No External Fragmentation: Eliminates external fragmentation since any free block can be used to store file data
  • Slow Random Access: Poor performance for random access since the system must traverse the linked list to reach a specific block
  • Space Overhead: Each block contains a pointer, reducing the effective storage capacity for actual file data
  • Reliability Issues: If one block pointer is corrupted, the entire file becomes inaccessible
3. Indexed Allocation:
  • Storage Method: Uses an index block that contains pointers to all the blocks belonging to a file, similar to a page table in memory management
  • Supports Random Access: Excellent random access performance since any block can be accessed directly through the index
  • Overhead of Index Block: Each file requires an index block, which consumes additional disk space
  • Flexible File Size: Supports files of varying sizes without the fragmentation problems of contiguous allocation
  • Complex Implementation: More complex to implement and manage compared to other allocation methods

4.2 I/O Management

Disk Structure and Scheduling

Disk Structure: A disk consists of multiple platters, each with two surfaces. Each surface is divided into concentric circles called tracks, and tracks are further divided into sectors. The disk head moves across tracks to read/write data, and the time taken for this movement is called seek time. The disk scheduling algorithms optimize the order of disk requests to minimize total seek time and improve overall disk performance.
Disk Performance Metrics:
  • Seek Time: Time taken to move the disk head from current track to target track
  • Rotational Latency: Time for the desired sector to rotate under the disk head
  • Transfer Time: Time to transfer data between disk and memory
  • Total Access Time: Sum of seek time, rotational latency, and transfer time
  • Throughput: Number of I/O operations completed per unit time
// Disk Scheduling Algorithms

// First Come First Serve (FCFS)
int fcfs(int requests[], int n, int head) {
    int total_seek = 0;
    
    for(int i = 0; i < n; i++) {
        total_seek += abs(requests[i] - head);
        head = requests[i];
    }
    
    return total_seek;
}

// Shortest Seek Time First (SSTF)
int sstf(int requests[], int n, int head) {
    int total_seek = 0;
    int visited[1000] = {0};
    
    for(int i = 0; i < n; i++) {
        int min_distance = INT_MAX;
        int min_index = -1;
        
        for(int j = 0; j < n; j++) {
            if(!visited[j]) {
                int distance = abs(requests[j] - head);
                if(distance < min_distance) {
                    min_distance = distance;
                    min_index = j;
                }
            }
        }
        
        total_seek += min_distance;
        head = requests[min_index];
        visited[min_index] = 1;
    }
    
    return total_seek;
}

// SCAN (Elevator) Algorithm
int scan(int requests[], int n, int head, int direction) {
    int total_seek = 0;
    int visited[1000] = {0};
    
    // Sort requests for efficient processing
    for(int i = 0; i < n-1; i++) {
        for(int j = 0; j < n-i-1; j++) {
            if(requests[j] > requests[j+1]) {
                int temp = requests[j];
                requests[j] = requests[j+1];
                requests[j+1] = temp;
            }
        }
    }
    
    // Process requests based on direction
    if(direction == 1) { // Moving towards higher tracks
        for(int i = 0; i < n; i++) {
            if(requests[i] >= head) {
                total_seek += abs(requests[i] - head);
                head = requests[i];
            }
        }
        // Reverse direction and process remaining requests
        for(int i = n-1; i >= 0; i--) {
            if(requests[i] < head) {
                total_seek += abs(requests[i] - head);
                head = requests[i];
            }
        }
    } else { // Moving towards lower tracks
        for(int i = n-1; i >= 0; i--) {
            if(requests[i] <= head) {
                total_seek += abs(requests[i] - head);
                head = requests[i];
            }
        }
        // Reverse direction and process remaining requests
        for(int i = 0; i < n; i++) {
            if(requests[i] > head) {
                total_seek += abs(requests[i] - head);
                head = requests[i];
            }
        }
    }
    
    return total_seek;
}

RAID Organizations

RAID (Redundant Array of Independent Disks): RAID is a storage technology that combines multiple physical disk drives into a single logical unit for the purposes of data redundancy, performance improvement, or both. RAID systems distribute data across multiple disks using various techniques such as striping, mirroring, and parity to achieve different levels of fault tolerance and performance characteristics.
RAID Levels and Their Characteristics:
  • RAID 0 (Striping):
    • Data is distributed across multiple disks in stripes
    • Provides maximum performance improvement through parallel I/O operations
    • No redundancy - if one disk fails, all data is lost
    • Storage efficiency: 100% (no space overhead)
    • Best for: High-performance applications where data loss is acceptable
  • RAID 1 (Mirroring):
    • Data is duplicated on two or more identical disks
    • Provides excellent fault tolerance - can survive failure of all but one disk
    • Read performance is improved due to parallel reads
    • Write performance may be slightly degraded
    • Storage efficiency: 50% (half the space is used for redundancy)
    • Best for: Critical data that requires high availability
  • RAID 5 (Distributed Parity):
    • Uses distributed parity across all disks in the array
    • Can survive failure of one disk without data loss
    • Good read performance, moderate write performance
    • Storage efficiency: (n-1)/n where n is the number of disks
    • Write penalty due to parity calculation
    • Best for: Balanced performance and fault tolerance
  • RAID 6 (Double Parity):
    • Uses two parity blocks distributed across all disks
    • Can survive failure of two disks simultaneously
    • Higher fault tolerance than RAID 5
    • Storage efficiency: (n-2)/n where n is the number of disks
    • Higher write penalty due to dual parity calculation
    • Best for: High-reliability requirements with larger disk arrays
  • RAID 10 (Striped Mirrors):
    • Combines RAID 0 striping with RAID 1 mirroring
    • Excellent performance and fault tolerance
    • Can survive failure of one disk in each mirrored pair
    • Storage efficiency: 50% (same as RAID 1)
    • Requires minimum of 4 disks
    • Best for: High-performance applications requiring fault tolerance
// RAID 5 Parity Calculation
unsigned char calculate_parity(unsigned char data[], int size) {
    unsigned char parity = 0;
    
    for(int i = 0; i < size; i++) {
        parity ^= data[i]; // XOR operation for parity
    }
    
    return parity;
}

// RAID 5 Data Recovery
unsigned char recover_data(unsigned char data[], int size, 
                           unsigned char parity, int failed_index) {
    unsigned char recovered = parity;
    
    for(int i = 0; i < size; i++) {
        if(i != failed_index) {
            recovered ^= data[i];
        }
    }
    
    return recovered;
}

Unit V: Protection, Security & Case Studies

5.1 Protection & Security

Access Control Models

Access Control: Access control is a security mechanism that regulates who or what can view or use resources in a computing environment. It is a fundamental concept in security that minimizes risk to the business or organization. Access control systems perform authorization identification, authentication, access approval, and accountability of entities through login credentials including passwords, personal identification numbers (PINs), biometric scans, and physical or electronic keys.
Access Control Models:
  • Discretionary Access Control (DAC): The owner of the resource determines who can access it and what permissions they have
  • Mandatory Access Control (MAC): Access is controlled by the system based on security labels and clearance levels
  • Role-Based Access Control (RBAC): Access is granted based on the role of the user in the organization
  • Attribute-Based Access Control (ABAC): Access is determined by evaluating attributes of the user, resource, and environment
// Access Control Matrix Implementation
struct access_control_entry {
    char subject[32];    // User or process identifier
    char object[32];     // File or resource identifier
    int permissions;        // Read, Write, Execute permissions
};

// Access Control Matrix
struct access_control_matrix {
    struct access_control_entry entries[1000];
    int entry_count;
};

// Check access permissions
int check_access(char* subject, char* object, int permission) {
    for(int i = 0; i < matrix.entry_count; i++) {
        if(strcmp(matrix.entries[i].subject, subject) == 0 && 
           strcmp(matrix.entries[i].object, object) == 0) {
            return (matrix.entries[i].permissions & permission) == permission;
        }
    }
    return 0; // Access denied
}

// Role-Based Access Control Implementation
struct role {
    char role_name[32];
    int permissions;
};

struct user {
    char username[32];
    char roles[10][32];
    int role_count;
};

int check_rbac_access(struct user* user, char* resource, int permission) {
    for(int i = 0; i < user->role_count; i++) {
        if(has_role_permission(user->roles[i], resource, permission)) {
            return 1;
        }
    }
    return 0;
}

Authentication Mechanisms

Authentication: Authentication is the process of verifying the identity of a user, device, or system. It is a critical component of security that ensures only authorized entities can access protected resources. Authentication mechanisms use various factors including something you know (passwords), something you have (tokens), and something you are (biometrics) to establish identity with varying levels of security and convenience.
Authentication Methods and Their Characteristics:
  • Password-based Authentication:
    • Traditional username/password combination
    • Advantages: Simple to implement, widely supported, low cost
    • Disadvantages: Vulnerable to brute force, phishing, and social engineering
    • Security measures: Password complexity requirements, account lockout, encryption
    • Best practices: Strong passwords, regular changes, secure storage
  • Biometric Authentication:
    • Uses unique physical or behavioral characteristics
    • Types: Fingerprint, retina/iris scan, voice recognition, facial recognition, gait analysis
    • Advantages: Difficult to forge, convenient, no need to remember credentials
    • Disadvantages: Can be expensive, privacy concerns, false acceptance/rejection rates
    • Applications: Mobile devices, secure facilities, financial transactions
  • Token-based Authentication:
    • Uses physical or digital tokens for authentication
    • Types: Smart cards, security tokens, USB keys, mobile apps
    • Advantages: High security, difficult to duplicate, time-based codes
    • Disadvantages: Can be lost or stolen, additional hardware cost
    • Examples: RSA SecurID, Google Authenticator, YubiKey
  • Multi-factor Authentication (MFA):
    • Combines multiple authentication methods for enhanced security
    • Factors: Knowledge (password), possession (token), inherence (biometric)
    • Advantages: Significantly higher security, defense in depth
    • Disadvantages: More complex setup, potential user resistance
    • Implementation: Two-factor (2FA), three-factor (3FA), adaptive authentication
  • Certificate-based Authentication:
    • Uses digital certificates for identity verification
    • Components: Public key infrastructure (PKI), certificate authorities (CA)
    • Advantages: Strong cryptographic security, non-repudiation
    • Disadvantages: Complex infrastructure, certificate management overhead
    • Applications: SSL/TLS, VPN, enterprise authentication
// Password Authentication System
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>

struct user_account {
    char username[32];
    char password_hash[64];
    char salt[16];
    int failed_attempts;
    time_t lockout_time;
};

// Hash password with salt
void hash_password(const char* password, const char* salt, char* hash) {
    char salted_password[256];
    sprintf(salted_password, "%s%s", password, salt);
    
    SHA256_CTX ctx;
    SHA256_Init(&ctx);
    SHA256_Update(&ctx, salted_password, strlen(salted_password));
    SHA256_Final((unsigned char*)hash, &ctx);
}

// Verify password
int verify_password(struct user_account* account, const char* password) {
    char computed_hash[64];
    hash_password(password, account->salt, computed_hash);
    
    return strcmp(account->password_hash, computed_hash) == 0;
}

// Two-Factor Authentication with TOTP
int verify_totp(const char* secret, int code) {
    time_t current_time = time(NULL);
    int time_step = 30; // 30-second window
    int counter = current_time / time_step;
    
    // Generate TOTP code (simplified)
    int expected_code = generate_totp(secret, counter);
    
    // Check current and adjacent windows for clock skew
    return (code == expected_code || 
            code == generate_totp(secret, counter - 1) || 
            code == generate_totp(secret, counter + 1));
}

5.2 Operating System Case Studies

Unix/Linux Operating System

Unix/Linux: Unix is a family of multitasking, multiuser computer operating systems that derive from the original AT&T Unix, developed in the 1970s at the Bell Labs research center. Linux is a Unix-like operating system kernel that was created by Linus Torvalds in 1991. The combination of Linux kernel with GNU tools and other software forms what is commonly known as the Linux operating system.
Key Features and Architecture:
  • Monolithic Kernel Architecture:
    • All operating system services run in kernel space
    • Direct hardware access for better performance
    • Loadable kernel modules for extensibility
    • Efficient system calls and inter-process communication
  • Process Management:
    • Fork-exec model for process creation
    • Process hierarchy with parent-child relationships
    • Process states: running, ready, blocked, zombie
    • Process scheduling with priority-based algorithms
  • File System Philosophy:
    • "Everything is a file" - devices, processes, network connections
    • Hierarchical directory structure starting from root (/)
    • Virtual file system (VFS) for multiple file system support
    • File permissions: read, write, execute for owner, group, others
  • Shell and Command Line:
    • Command-line interface as primary user interface
    • Shell scripting for automation and system administration
    • Pipeline concept for command chaining
    • Environment variables for configuration
  • Multi-user and Multi-tasking:
    • Multiple users can work simultaneously
    • Preemptive multitasking with time-sharing
    • User isolation and resource protection
    • System administration through superuser (root) privileges

Microsoft Windows Operating System

Microsoft Windows: Windows is a family of proprietary graphical operating systems developed by Microsoft. It was first introduced in 1985 and has evolved through multiple versions, becoming the dominant desktop operating system worldwide. Windows combines elements of both monolithic and microkernel architectures to provide a balance of performance and modularity.
Key Features and Architecture:
  • Hybrid Kernel Architecture:
    • Combines monolithic and microkernel design principles
    • Core services in kernel space for performance
    • User-mode subsystems for stability and security
    • Hardware abstraction layer (HAL) for device independence
  • Process and Thread Management:
    • Thread-based architecture with lightweight processes
    • Symmetric multiprocessing (SMP) support
    • Preemptive multitasking with priority-based scheduling
    • Process isolation and memory protection
  • Registry System:
    • Centralized hierarchical database for system configuration
    • Stores settings for hardware, software, and user preferences
    • Registry hives for different system components
    • Registry editing tools for system administration
  • Graphical User Interface:
    • GUI as primary user interface with desktop metaphor
    • Window management and desktop environment
    • Start menu and taskbar for application access
    • Theme support and customization options
  • Security and File System:
    • NTFS file system with advanced permissions
    • User Account Control (UAC) for privilege elevation
    • Windows Defender for malware protection
    • BitLocker for disk encryption

Android Mobile Operating System

Android: Android is a mobile operating system based on a modified version of the Linux kernel and other open source software, designed primarily for touchscreen mobile devices such as smartphones and tablets. It was developed by Google and the Open Handset Alliance, with the first commercial version released in 2008.
Key Features and Architecture:
  • Linux Kernel Foundation:
    • Based on modified Linux kernel for hardware abstraction
    • Hardware drivers and system services in kernel space
    • Memory management and process scheduling
    • Security features and power management
  • Application Runtime:
    • Android Runtime (ART) for native Android apps
    • Dalvik Virtual Machine for Java applications
    • Just-In-Time (JIT) and Ahead-of-Time (AOT) compilation
    • Garbage collection for memory management
  • Application Framework:
    • Rich set of APIs for application development
    • Activity Manager for application lifecycle
    • Content Providers for data sharing
    • Notification Manager and Window Manager
  • Security Model:
    • Sandboxed applications with isolated processes
    • Permission-based access control
    • Application signing for authenticity
    • SE Linux for mandatory access control
  • User Interface:
    • Touch-optimized interface design
    • Gesture recognition and multi-touch support
    • Adaptive layouts for different screen sizes
    • Material Design principles for consistent UI

Practical Component / Lab Work

6.1 Process Management Programs

Process Management Programs: These programs demonstrate fundamental concepts in operating system process management, including process synchronization, inter-process communication, and resource sharing. The producer-consumer problem is a classic synchronization problem that illustrates the challenges of coordinating multiple processes that share a common resource.
// Producer-Consumer Problem Implementation
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>

#define BUFFER_SIZE 5
#define MAX_ITEMS 10

// Shared buffer and synchronization variables
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
sem_t empty, full, mutex;

// Producer thread function
void* producer(void* arg) {
    int item;
    
    for(int i = 0; i < MAX_ITEMS; i++) {
        item = i;
        
        // Wait for empty slot in buffer
        sem_wait(&empty);
        
        // Acquire mutex for critical section
        sem_wait(&mutex);
        
        // Add item to buffer
        buffer[in] = item;
        printf("Producer: Produced item %d at position %d\n", item, in);
        in = (in + 1) % BUFFER_SIZE;
        
        // Release mutex and signal full slot
        sem_post(&mutex);
        sem_post(&full);
        
        // Simulate production time
        usleep(100000);
    }
    
    return NULL;
}

// Consumer thread function
void* consumer(void* arg) {
    int item;
    
    for(int i = 0; i < MAX_ITEMS; i++) {
        // Wait for item in buffer
        sem_wait(&full);
        
        // Acquire mutex for critical section
        sem_wait(&mutex);
        
        // Remove item from buffer
        item = buffer[out];
        printf("Consumer: Consumed item %d from position %d\n", item, out);
        out = (out + 1) % BUFFER_SIZE;
        
        // Release mutex and signal empty slot
        sem_post(&mutex);
        sem_post(&empty);
        
        // Simulate consumption time
        usleep(150000);
    }
    
    return NULL;
}

// Main function to demonstrate producer-consumer
int main() {
    pthread_t producer_thread, consumer_thread;
    
    // Initialize semaphores
    sem_init(&empty, 0, BUFFER_SIZE); // Buffer starts empty
    sem_init(&full, 0, 0);        // No items initially
    sem_init(&mutex, 0, 1);       // Binary semaphore for mutual exclusion
    
    // Create producer and consumer threads
    pthread_create(&producer_thread, NULL, producer, NULL);
    pthread_create(&consumer_thread, NULL, consumer, NULL);
    
    // Wait for threads to complete
    pthread_join(producer_thread, NULL);
    pthread_join(consumer_thread, NULL);
    
    // Clean up semaphores
    sem_destroy(&empty);
    sem_destroy(&full);
    sem_destroy(&mutex);
    
    printf("Producer-Consumer demonstration completed.\n");
    return 0;
}

6.2 File I/O Handling

File I/O Handling: File Input/Output operations are fundamental to operating systems, allowing programs to read from and write to persistent storage. These operations demonstrate how the operating system manages file access, error handling, and resource management. File I/O programs showcase concepts like file descriptors, buffering, error checking, and proper resource cleanup.
// Advanced File Copy Program with Error Handling
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define BUFFER_SIZE 4096
#define MAX_PATH 256

// Function to copy file with progress tracking
int copy_file(char* source, char* destination) {
    FILE *src, *dst;
    char buffer[BUFFER_SIZE];
    size_t bytes_read, bytes_written;
    long total_bytes = 0;
    long file_size;
    
    // Validate input parameters
    if(source == NULL || destination == NULL) {
        fprintf(stderr, "Error: Invalid file paths\n");
        return -1;
    }
    
    // Check if source and destination are the same
    if(strcmp(source, destination) == 0) {
        fprintf(stderr, "Error: Source and destination are the same\n");
        return -1;
    }
    
    // Open source file for reading
    src = fopen(source, "rb");
    if(src == NULL) {
        fprintf(stderr, "Error opening source file '%s': %s\n", source, strerror(errno));
        return -1;
    }
    
    // Get file size for progress tracking
    fseek(src, 0, SEEK_END);
    file_size = ftell(src);
    fseek(src, 0, SEEK_SET);
    
    // Open destination file for writing
    dst = fopen(destination, "wb");
    if(dst == NULL) {
        fprintf(stderr, "Error opening destination file '%s': %s\n", destination, strerror(errno));
        fclose(src);
        return -1;
    }
    
    printf("Starting file copy: %s -> %s\n", source, destination);
    printf("File size: %ld bytes\n", file_size);
    
    // Copy file content in chunks
    while((bytes_read = fread(buffer, 1, BUFFER_SIZE, src)) > 0) {
        bytes_written = fwrite(buffer, 1, bytes_read, dst);
        
        if(bytes_written != bytes_read) {
            fprintf(stderr, "Error writing to destination file: %s\n", strerror(errno));
            fclose(src);
            fclose(dst);
            return -1;
        }
        
        total_bytes += bytes_written;
        
        // Show progress
        if(file_size > 0) {
            int progress = (int)((total_bytes * 100) / file_size);
            printf("\rProgress: %d%% (%ld/%ld bytes)", progress, total_bytes, file_size);
            fflush(stdout);
        }
    }
    
    // Check for read errors
    if(ferror(src)) {
        fprintf(stderr, "\nError reading source file: %s\n", strerror(errno));
        fclose(src);
        fclose(dst);
        return -1;
    }
    
    // Close files
    fclose(src);
    fclose(dst);
    
    printf("\nFile copied successfully: %ld bytes\n", total_bytes);
    return 0;
}

// Function to create directory if it doesn't exist
int ensure_directory(char* path) {
    char temp_path[MAX_PATH];
    char *token;
    
    strcpy(temp_path, path);
    token = strtok(temp_path, "/");
    
    while(token != NULL) {
        if(mkdir(token, 0755) != 0 && errno != EEXIST) {
            fprintf(stderr, "Error creating directory '%s': %s\n", token, strerror(errno));
            return -1;
        }
        token = strtok(NULL, "/");
    }
    
    return 0;
}

// Main function demonstrating file operations
int main(int argc, char* argv[]) {
    if(argc != 3) {
        printf("Usage: %s <source_file> <destination_file>\n", argv[0]);
        return 1;
    }
    
    // Ensure destination directory exists
    char *last_slash = strrchr(argv[2], '/');
    if(last_slash != NULL) {
        *last_slash = '\0';
        if(ensure_directory(argv[2]) != 0) {
            return 1;
        }
        *last_slash = '/';
    }
    
    // Perform file copy
    if(copy_file(argv[1], argv[2]) == 0) {
        printf("File copy operation completed successfully.\n");
        return 0;
    } else {
        printf("File copy operation failed.\n");
        return 1;
    }
}