Practical Operating Systems
Practical Operating Systems
ACKNOWLEDGMENTS i
0 Introduction 1
Terminating Processes 54
4 Chapter 4: Threads 60
First and foremost, I would like to thank my family for their unwavering
encouragement and patience at every step along this journey. Their
confidence in me kept me motivated through the long hours of writing and
editing.
0
INTRODUCTION
Welcome to "Practical Operating Systems: A Hands-On Approach with
Python," a comprehensive guide designed to empower engineering students with a profound
understanding of operating systems, blending essential theory with practical applications.
The main goal of this book is to teach readers about operating systems in a
practical, hands-on way. We want to:
Why Python?
Python has been selected as the cornerstone programming language for this
book owing to its distinct attributes:
I
F igure 1.1: Operating System
Components
n this chapter, we will delve into
the world of operating systems,
uncovering their essential
functions and historical evolution.
Historical Perspective
2. Harvard Architecture
4. R
Figure 1.3: CISC Architecture
ISC (Reduced Instruction Set Computer)
Figure
1.4: RISC Architecture
he Relevance of Architecture to Operating Systems
1. Operating Systems
2. Application Software
3. Middleware
The interactions that transpire within and between these software layers are
as intricate as they are essential for the harmonious operation of a computer
system. Let us delve into these pivotal interactions:
3. OS and Middleware:
T
Figure
1.5: The Interplay of Software Ecosystem
his visual representation illustrates the intricate interplay of software layers
in a computer system, highlighting the symbiotic relationships between
operating systems, application software, and middleware.
Interrupts
T
Figure 1.6: Interrupts: A way for hardware and software to notify the CPU
of important events.
he operating system maintains a list of pending interrupts. When an
interrupt occurs, the CPU adds the interrupt to the list of pending interrupts.
The operating system will then handle the interrupts in the list one by one.
When an interrupt occurs, the CPU stops what it is doing and transfers
control to a special routine called an interrupt service routine (ISR). The
ISR handles the interrupt and then returns control to the CPU.
Dual-Mode Operation
Kernel mode is the most privileged mode. Only the operating system
can run in kernel mode.
User mode is less privileged than kernel mode. User programs can
only run in user mode.
Figure 1.7: Dual mode: Kernel mode and user mode, protecting the
system kernel while giving users access to resources.
rivilege Levels
In addition to kernel mode and user mode, some CPUs also have additional
privilege levels. These privilege levels are used to further restrict the access
of user programs to system resources.
Ring 0: The most privileged level. Only the operating system can run
in Ring 0.
Figure 1.8: Dual mode kernel protection rings, with Ring 0 (kernel mode)
at the top and Ring 3 (user mode) at the bottom.
he Importance of Interrupts and Dual-Mode Operation
Interrupts and dual-mode operation are essential for the reliable and secure
operation of operating systems. Interrupts allow the operating system to
respond to events quickly and efficiently, while dual-mode operation
protects the operating system from unauthorized access by user programs.
def handle_keyboard_input(event):
if event.name == 'a':
print("You pressed 'a'.")
elif event.name == 'b':
print("You pressed 'b'.")
keyboard.on_press(handle_keyboard_input)
import threading
def periodic_task():
print("This task runs periodically.")
The code below illustrates hardware interrupt handling with the 'signal'
module:
import signal
def main():
# Register the timer interrupt handler
signal.signal(signal.SIGALRM, handle_timer_interrupt)
if __name__ == "__main__":
main()
We will then take a closer look at system calls, which are the mechanisms
through which processes request services from the operating system.
System calls are essential for performing tasks such as file I/O, memory
management, and process scheduling.
Overview of OS Architectures
U
Figure 2.3: Hybrid kernel architecture: Combines the best features of
monolithic and microkernel architectures.
nderstanding Architectural Choices and Trade-offs
Monolithic Kernel:
Advantages:
Disadvantages:
Microkernel:
Advantages:
Disadvantages:
Advantages:
Disadvantages:
Throughout this chapter and the book, we will delve deeper into these
architectural choices, offering insights into their effects on OS design,
performance, and functionality.
2.2. THE SIGNIFICANCE OF APIS IN OPERATING
SYSTEMS
This section delves into the central role of APIs (Application Programming
Interfaces) in the realm of operating systems. APIs function as
intermediaries, enabling software applications to communicate and interact
with the underlying OS. Understanding API definition, significance, and
design principles is fundamental for harnessing modern OS capabilities.
APIs act as bridges between user-level code and the OS kernel. When a
user-level application needs to perform an action requiring kernel-level
privileges or hardware access, it requests it through the relevant API. The
API communicates with the kernel, which executes the request and returns
the result to the application.
This interaction is crucial for various operations, including file I/O, process
management, memory allocation, and hardware access. APIs define the
syntax and semantics of these interactions, ensuring effective
communication with the OS.
File System API: Enables file creation, reading, writing, deletion, and
directory navigation.
Syscalls fall into distinct categories, each tailored for specific tasks. Let's
explore three primary categories:
System calls may encounter errors due to various reasons like invalid
arguments or resource shortages. Proper error handling ensures application
robustness and reliability. Error handling methods include checking system
call return values for error codes and using error-specific functions like
perror or strerror to obtain human-readable error messages.
System Calls:
System calls are a subset of APIs. They are low-level interfaces that allow
user-level programs to interact with the OS kernel. System calls provide
direct access to the kernel's core functionalities, such as process
management, memory allocation, and hardware access. When an
application needs to perform an action that requires kernel-level privileges
or hardware access, it makes a system call. The OS kernel executes the
requested action and returns the result to the application.
Examples:
API Example - File System API:
In this example, we use Python's os module, which offers an API for file
system operations. We create a new directory and list files in the current
directory using the API functions.
int main() {
int fd;
if (fd == -1) {
perror("Error opening file");
return 1;
}
return 0;
}
In this C code example, we use system calls to open and close a file. The
open() and close() functions are system calls that directly interact
with the OS kernel to perform file operations.
Python itself doesn't have a direct system call interface like lower-level
programming languages such as C or assembly. In Python, system calls are
typically abstracted away and provided through higher-level interfaces like
the Python standard library and various third-party libraries.
So, while Python doesn't expose raw system calls in the same way
languages like C do, you can achieve similar functionality by using
Python's built-in modules or by creating Python extensions in C that
directly interact with system calls. These extensions can then be imported
and used in Python scripts.
Virtualization Fundamentals
Hardware Virtualization
Software Virtualization
With this foundation in cloud computing and its synergy with virtualization,
we're prepared to explore practical aspects in the upcoming chapters. These
technologies empower efficient resource utilization, scalability, and the
seamless operation of modern operating systems and applications.
Use Cases: Ideal for users needing full OS and software stack
control, suitable for hosting applications.
Python equips developers with two crucial modules, os and subprocess, for
seamless interaction with the operating system and effortless execution of
system calls.
os Module:
Example:
import os
Example:
import subprocess
import os
File Reading:
File Deletion:
import os
# Delete a file
if os.path.exists("myfile.txt"):
os.remove("myfile.txt")
import subprocess
import os
import signal # Import the signal module
In the upcoming chapters, we'll delve deeper into these Python modules,
showcasing their applications for interacting with system calls and APIs.
This knowledge will empower you to perform a broad spectrum of system-
related tasks efficiently and with confidence.
2.6. USING PYTHON TO INTERACT WITH OS APIS
In this section, we'll explore the world of operating system APIs and
demonstrate how Python can act as a powerful intermediary between your
applications and these APIs. Specifically, we'll introduce Python's ctypes
library, which facilitates seamless interaction with OS-specific APIs.
Through practical examples, we'll illustrate how to access and utilize these
APIs while considering important factors for cross-platform compatibility.
import ctypes
2. Accessing Functions:
Once the library is loaded, functions within it can be accessed using dot
notation. These functions correspond to the API calls provided by the
library.
ctypes necessitates the definition of data types for function arguments and
return values to ensure proper interaction with the API. These details must
be specified for accurate API function calls.
Let's dive into practical examples of using ctypes to interact with OS-
specific APIs. We'll explore two common scenarios: calling a Windows API
function to retrieve system information and invoking a Unix API function
to manipulate files.
In this example, we'll utilize ctypes to call the Windows API function
GetSystemInfo, which retrieves information about the system's hardware
and operating system.
import ctypes
system_info = SYSTEM_INFO()
import ctypes
if result == 0:
print(f"Directory '{directory_name}' created
successfully.")
else:
print(f"Error creating directory '{directory_name}'")
Library Names: Shared library names and paths may vary across
operating systems. Use platform-specific library names or utilize tools
like ctypes.util.find_library to locate the appropriate
library.
Process States:
Processes can exist in various states as they execute within the operating
system. These states include:
New: This is the initial state when a process is created, but it has not
yet started execution.
Wait: Processes in the wait state are temporarily halted, often waiting
for some event or resource, such as user input or data from a file.
Terminated: This is the final state when a process has completed its
execution or has been terminated by the system.
Figure 3.2: Process states: New, ready, running, waiting, terminated
Open Files: A list of open files associated with the process, along with
file pointers indicating the current position in each file, is recorded.
This data enables processes to read and write files without conflicts.
Introduction
L
Figure 3.4: Context switching: Saving and restoring process state
et's dissect the steps involved in context-switching:
Load New Process State: The system then loads the state of the next
process scheduled for execution. This entails setting the program
counter to the correct memory address, injecting CPU registers with
the new process's values, and preparing it for execution.
3
Figure 3.5: Concurrent processes: Interleaving execution of two
processes.
In the upcoming sections, we'll delve into the strategies and techniques
employed by operating systems to optimize context-switching and strike the
right equilibrium between multitasking and system performance.
3.4 SCHEDULERS IN OPERATING SYSTEMS
Schedulers are the essential decision-makers behind the efficient allocation
of CPU time to processes in an operating system. In this section, we'll
explore the distinct types of schedulers that operating systems employ to
guarantee smooth process execution. We'll delve into the functions of short-
term, medium-term, and long-term schedulers, unveiling the algorithms and
policies guiding their actions.
E
Figure 3.6: Job scheduling: Short-term and long-term
xample: Short-Term Scheduling
E
Figure 3.7: Medium-Term Scheduler: Swapping processes to/from
memory
xample: Medium-Term Scheduling
hortest Job First (SJF): SJF schedules the process with the shortest
estimated execution time next. For example, if processes P1, P2, and
P3 have estimated execution times of 10 milliseconds, 20 milliseconds,
and 30 milliseconds, respectively, then P1 will be scheduled first,
followed by P2, and then P3.
S
Forking is akin to cloning an existing worker. When you fork a process, you
create an exact copy, known as the child process, of the original process,
referred to as the parent process. Initially, both the parent and child
processes run the same code, but they can diverge and perform different
tasks as needed.
Imagine a text editor application. When you open a new file, the editor may
fork a process for each open file. These child processes start with the same
code as the parent process, including the text editor's functionalities.
However, each child process handles specific file operations, such as
reading, writing, and saving, while sharing the editor's core codebase. This
approach allows multiple files to be edited simultaneously, with each file
operation occurring independently within its respective child process.
3.5.1.2 Spawning New Processes
Spawning a new process is akin to hiring a specialized worker with a
specific skill set tailored for a particular task. Unlike forking, where
processes start identical, spawning allows you to create processes with
different program code and roles.
ipes
Example: Synchronization
import multiprocessing
import urllib.request
if __name__ == "__main__":
urls = ["https://example.com/file1.pdf",
"https://example.com/file2.pdf",
"https://example.com/file3.pdf"]
save_paths = ["file1.pdf", "file2.pdf", "file3.pdf"]
import multiprocessing
if __name__ == "__main__":
operations = ['add', 'subtract', 'multiply',
'divide']
values = [(10, 5), (15, 7), (8, 4), (12, 0)] # Some
example values
For example, we can launch a process and then check if it is still running:
import psutil
import time
process = psutil.Popen(["notepad.exe"])
while True:
if not psutil.pid_exists(process.pid):
print("Process has exited.")
break
time.sleep(1)
This loops and checks the process ID to see if the process is still alive.
TERMINATING PROCESSES
We can also forcibly terminate processes:
import psutil
process = psutil.Popen(["notepad.exe"])
process.terminate()
process.wait()
Here we use terminate() to send a SIGTERM signal, then wait for the
process to exit.
LISTING ALL PROCESSES
To view all running processes, we can use:
import psutil
import time
running = True
def main_loop():
global running
while running:
print("Processing...")
time.sleep(1)
if __name__ == "__main__":
try:
main_loop()
except KeyboardInterrupt:
print("Terminating...")
running = False
This allows the process to finish up any critical operations before exiting
when it receives a termination signal.
import time
from contextlib import contextmanager
@contextmanager
def managed_loop():
print("Starting...")
try:
yield
finally:
print("Cleaning up...")
with managed_loop():
while True:
print("Processing...")
time.sleep(1)
The cleanup code in the finally block will execute even if the loop
terminates early.
import subprocess
child = subprocess.Popen(["python3", "child.py"])
import multiprocessing
def analyze_sentiment(review):
# Perform sentiment analysis here
pass
if __name__ == "__main__":
reviews = [...] # List of customer reviews
num_processes = 4
pool.close()
pool.join()
Use Case 2: System Monitoring and Resource Management
import psutil
import time
def monitor_cpu_threshold(threshold):
while True:
cpu_percent = psutil.cpu_percent(interval=1)
if cpu_percent > threshold:
# Take necessary actions, e.g., send alerts
or scale resources
print(f"CPU Usage exceeds {threshold}%")
time.sleep(60) # Check every minute
if __name__ == "__main__":
threshold = 90 # Define the CPU usage threshold
monitor_cpu_threshold(threshold)
import shutil
import os
import datetime
os.makedirs(backup_folder, exist_ok=True)
if __name__ == "__main__":
source_directory = "/path/to/source/files"
backup_directory = "/path/to/backup/location"
backup_files(source_directory, backup_directory)
These real-world use cases demonstrate the versatility and power of Python
in process management. Whether parallelizing data processing, monitoring
system resources, or automating tasks, Python's process management
capabilities make it a valuable tool for a wide range of applications.
4
THREADS
For example, consider a web server responsible for serving multiple client
requests simultaneously. Without multi-threading, the server might process
requests sequentially, leading to slow response times. However, by
employing multi-threading, each incoming request can be assigned to a
separate thread. These threads work concurrently to handle client requests,
significantly improving the server's responsiveness and overall
performance.
Fault Tolerance: Blocking one kernel-level thread does not affect the
execution of other threads in the same process.
Overhead: Creating and managing kernel-level threads typically involves more overhead
compared to user-level threads, impacting performance for applications with many threads.
Less Portability: Kernel-level thread implementations can vary between operating systems,
reducing portability compared to user-level threads.
There are three main models for mapping user threads to kernel threads:
Mapping
Advantages Disadvantages
Model
The support for mapping user threads to kernel threads varies depending on
the operating system.
Linux: Linux supports all three mapping models. The default mapping
model is one-to-one, but it can be changed to many-to-one or many-to-
many using the ulimit command.
The choice of mapping model and thread scheduler depends on the specific
application requirements. For example, an application that requires a high
degree of responsiveness may choose a preemptive scheduler with a one-to-
one mapping, while an application that is CPU-intensive may choose a non-
preemptive scheduler with a many-to-one mapping.
4.2 THREAD MANAGEMENT IN OPERATING
SYSTEMS
Thread management encompasses critical aspects of modern operating
systems, enabling the efficient and concurrent execution of tasks. In this
section, we will delve into the intricacies of thread management, focusing
on how operating systems handle threads, with a specific emphasis on the
role of the kernel in creating, scheduling, and synchronizing threads.
Thread Creation:
Thread Scheduling:
Thread Scheduler: Housed within the kernel, the thread scheduler assumes responsibility for
determining which threads should run and for how long. It employs scheduling algorithms to
make these decisions.
Context Switching: During a context switch, the kernel performs a crucial task. It saves the
current thread's state, including registers and the program counter, in memory and restores the
state of the thread that will run next.
Thread Synchronization:
Blocking and Wake-Up: The kernel efficiently manages thread blocking and wake-up
operations. When one thread attempts to access a resource held by another, it may be
temporarily blocked. The kernel ensures that blocked threads are awakened efficiently when
the resource becomes available.
Thread States:
Ready: Threads in the ready state are prepared to run but await the
scheduler's allocation of CPU time. Multiple threads in the ready state
can coexist within a process.
Imagine a simple program with two threads that are trying to increment a
shared counter variable. The threads perform the following steps:
Thread 1:
1. Reads the current value of the shared counter (e.g., it reads 5).
3. Writes the new value back to the shared counter (sets it to 6).
Thread 2:
1. Reads the current value of the shared counter (still the old value, i.e.,
5).
3. Writes the new value back to the shared counter (also sets it to 6).
5. Both Thread 1 and Thread 2 write their local copies (6) back to the
shared counter.
Locks:
import threading
# Shared resource
shared_counter = 0
# Mutex for synchronization
mutex = threading.Lock()
def increment_counter():
global shared_counter
with mutex: # Acquire the mutex
shared_counter += 1 # Perform a synchronized
operation
print(f"Counter: {shared_counter}")
This code creates five threads that all increment the shared_counter
variable. Without a mutex, it is possible for the threads to interleave their
operations, resulting in an incorrect value for the counter.
The print() statement releases the mutex after the variable has been
incremented. This allows other threads to acquire the mutex and access the
variable.
The output of this code is always 5, because each thread is only able to
increment the counter once.
The mutex is a simple but powerful synchronization primitive that can be
used to prevent race conditions in multi-threaded programs.
import threading
def render_web_page():
# Simulate rendering a web page
print("Rendering web page...")
def download_images():
# Simulate downloading images
print("Downloading images...")
import threading
def process_image(image):
# Simulate image processing
print(f"Processing {image} on thread
{threading.current_thread().name}")
Resource Optimization
Multi-threading not only enhances CPU utilization but also optimizes other
system resources. For instance, in a multi-threaded web server, threads can
concurrently handle incoming client requests, reducing idle time and
making efficient use of network and memory resources.
Conclusion
import threading
import requests
from bs4 import BeautifulSoup
Conclusion
Conclusion
Creating Threads
2. Define a function that represents the task you want the thread to
perform.
import threading
Starting Threads
Once you have created a thread, start it using the start() method. Starting a
thread initiates its execution, and it runs concurrently with other threads.
Joining Threads
To ensure that a thread completes its execution before the main program
exits, use the join() method. Calling join() on a thread blocks the main
program's execution until the thread finishes.
Complete Example
Here's a complete example that creates and manages two threads to print
numbers concurrently:
import threading
By following these steps, you can easily create and manage threads in
Python using the threading module, enabling concurrent execution of tasks
in your Python applications, enhancing performance and responsiveness.
import requests
import threading
Another common challenge arises when multiple processes try to print data
to a shared printer. Without synchronization, simultaneous print jobs may
overlap or mix up output. Such scenarios underscore the importance of
synchronized resource access for maintaining order and reliability.
Imagine two processes updating a shared bank account balance. The code
responsible for balance updates constitutes a critical section. Without
synchronization, simultaneous execution can lead to erroneous results.
Critical sections ensure only one process enters at a time, averting conflicts.
5.2.2 Mutexes: Ensuring Exclusive Access
In the world of concurrent programming, mutexes (short for mutual
exclusion) play a vital role in ensuring that only one process or thread can
access a shared resource at a given time. Mutexes are synchronization
primitives used to prevent data corruption and race conditions in
multithreaded or multiprocess applications.
import threading
# Create a Mutex
mutex = threading.Lock()
mutex.acquire()
4. Releasing the Mutex: When the process finishes its work within the
critical section, it releases the mutex to allow other processes to
acquire it and access the shared resource.
mutex.release()
import threading
# Create a Mutex
mutex = threading.Lock()
# Shared counter
counter = 0
def increment_counter():
global counter
# Acquire the Mutex
mutex.acquire()
try:
for _ in range(100000):
counter += 1
finally:
# Release the Mutex, even if an exception occurs
mutex.release()
In this example, the mutex ensures that only one thread can increment the
counter at a time, preventing conflicts and ensuring data consistency.
import threading
In this scenario, the database represents the shared resource, and processes
act as users. The semaphore maintains a value of 1, symbolizing the
available database connections. When a process intends to access the
database, it invokes the wait operation on the semaphore. If the semaphore's
value is 0, the process enters a blocked state. Upon completing database
operations, the process executes the signal operation, enabling another
process to access the database.
import threading
# Create a semaphore with an initial value of 1
semaphore = threading.Semaphore(1)
def access_shared_resource(thread_id):
global shared_resource
with semaphore:
print(f"Thread {thread_id} is accessing the
resource.")
# Simulate resource access
shared_resource += f" (modified by Thread
{thread_id})"
print(f"Resource data: {shared_resource}")
print(f"Thread {thread_id} released the
resource.")
Imagine a file download manager (producer) acquiring files from the web
and storing them in a local directory (buffer). Concurrently, a video player
(consumer) plays these downloaded videos. Effective synchronization
mechanisms are paramount to avoid storage overflows and maintain
seamless video playback.
# Producer thread
def producer():
while True:
# Acquire semaphore
semaphore.acquire()
# Release semaphore
semaphore.release()
# Consumer thread
def consumer():
while True:
# Acquire semaphore
semaphore.acquire()
# Release semaphore
semaphore.release()
producer_thread.start()
consumer_thread.start()
print("Program completed.")
In this setup, the producer generates data and deposits it into the buffer,
while the consumer retrieves and processes it. The semaphore ensures
exclusive access to the buffer, avoiding conflicts and maintaining order.
import time
num_philosophers = 5
def philosopher(id):
while
True:
#
Think
time.sleep(0.5
)
# Pick up
chopsticks
chopsticks[id].acquire(
)
chopsticks[(id + 1) %
num_philosophers].acquire()
#
Eat
print(f"Philosopher {id}
eating")
time.sleep(0.5
)
# Put down
chopsticks
chopsticks[id].release(
)
chopsticks[(id + 1) %
num_philosophers].release()
threads = []
for i in range(num_philosophers):
t = Thread(target=philosopher, args=
(i,))
threads.append(t
)
t.start(
)
for t in threads:
t.join(
)
print("Done.")
In this implementation, the philosopher function takes the left fork, the right
fork, and the monitor as arguments. It begins by pondering for a while.
Then, it secures both the left and right forks, permitting it to dine. The
monitor ensures that only one philosopher interacts with the forks
simultaneously, evading conflicts and deadlocks.
5.3.3 The Readers-Writers Problem
Figure 5.3.3: The Readers-Writers Problem
Consider a database system with several clients. Many clients may read
data from the database concurrently (readers), but only one client should
modify the database (writer) at any given time. Effective synchronization
mechanisms are essential to enable concurrent reading while upholding data
integrity during writing.
5.3.4 Other Classical Synchronization Problems
Beyond the mentioned synchronization challenges, several other classical
hurdles exist in concurrent computing. Each problem boasts unique
attributes and solutions, serving as yardsticks to evaluate synchronization
mechanisms and algorithms. Examples include the Sleeping Barber
problem, the Cigarette Smokers problem, and the Bounded Buffer problem.
In the era of modern processors, equipped with SMT technology like Intel's
Hyper-Threading or AMD's SMT, multitasking performance soars. SMT-
aware scheduling ensures efficient thread resource sharing.
import threading
def task1():
print("Task 1 is running")
def task2():
print("Task 2 is running")
# Create threads
thread1 = threading.Thread(target=task1)
thread2 = threading.Thread(target=task2)
# Start threads
thread1.start()
thread2.start()
import threading
# Create a mutex
mutex = threading.Lock()
def protect_shared_resource():
with mutex:
# Access the shared resource safely
pass
import threading
def access_shared_resource():
semaphore.acquire()
# Access the resource
semaphore.release()
import threading
# Create an RLock
rlock = threading.RLock()
def nested_access():
with rlock:
# First access
with rlock:
# Nested access
pass
import threading
def wait_for_condition():
with condition:
while not some_condition:
condition.wait()
# Condition is met
Event Objects: Event objects are used to signal the occurrence of an event.
Threads can wait for an event to be set and then proceed when it happens.
import threading
# Create an event object
event = threading.Event()
def wait_for_event():
event.wait() # Wait for the event to be set
# Event occurred
Imagine a busy print shop that offers printing services to customers. The
print shop has a high-end color printer that can handle multiple print jobs
simultaneously, but it has a limitation: it can only process three print jobs at
a time due to its high cost and complexity.
import threading
def customer_thread(customer_id):
print(f"Customer {customer_id} is waiting to print.")
Nodes: Nodes in the resource allocation graph represent two types of entities: processes and
resource instances. Each process and each resource instance is represented by a unique node.
For processes, nodes are usually depicted as rectangles, while resource instances are shown as
circles or ellipses.
Edges: Edges in the graph represent the allocation and request relationships between
processes and resource instances. There are two types of edges: allocation edges and request
edges. An allocation edge from a process node to a resource instance node signifies that the
process currently holds that resource. Conversely, a request edge from a process to a resource
instance indicates that the process is requesting that resource.
D N
eadlock o Deadlock
Figure 6.2: Resource allocation graph: Visualizing the allocation of resources to processes
Resource States: Resource instances can be in one of two states: allocated or available. An
allocated resource is currently being used by a process, while an available resource is idle and
can be allocated to a requesting process.
Process States: Processes can be in one of three states: running, blocked, or requesting. A
running process is actively executing, a blocked process is waiting for a resource it has already
requested, and a requesting process is actively requesting a resource it needs.
In the subsequent sections, we will explore how the resource allocation graph is used in deadlock
avoidance, prevention, detection, and recovery. It serves as a fundamental tool in managing and
mitigating deadlocks in complex computing environments.
6.4 DEADLOCK HANDLING: AVOID, PREVENT,
AND RECOVER
Deadlocks in computing systems pose challenges like resource contention
and process stagnation. To combat this, three strategies are employed:
deadlock avoidance, prevention, and recovery. Here's an overview of these
techniques and their algorithms:
6.4.1 Deadlock Avoidance
Objective: Prevent deadlocks by analyzing resource allocation dynamically
and ensuring it won't create circular waits.
Details:
Methods:
Safe State: A system state where the processes can complete their
execution without entering a deadlock. In a safe state, resources can be
allocated to processes in such a way that they will eventually release
them, ensuring progress. The Banker's Algorithm, mentioned earlier, is
used to determine whether a system is in a safe state by analyzing
resource allocation requests.
In systems where each resource type has only one instance, and tasks
adhere to the single resource request model, the deadlock detection
algorithm relies on graph theory. The goal is to unearth cycles within the
resource allocation graph, a telltale sign of the circular-wait condition and
the presence of deadlocks.
An arrow from a task to a resource signifies the task's desire for the
resource.
3. Insert the node into L and check if it already exists there. If found, a
cycle is present, signaling a deadlock. The algorithm concludes. If not,
remove the node from N.
4. Verify if untraversed outgoing arcs exist from this node. If all arcs are
traversed, proceed to step 6.
Step 1: N = { R1, T1, R2, T2, R3, T3, R4, T4, T5, R5, T6 }
Step 3: L = { R1 }; no cycles found; N = { T1, R2, T2, R3, T3, R4, T4,
T5, R5, T6 }
Step 3: L = { R1, T1 }; N = { R2, T2, R3, T3, R4, T4, T5, R5, T6 }; no
cycles found
import networkx as nx
def deadlock_detection(resource_allocation_graph):
"""Detects deadlocks in a system with single resource
instances.
Args:
resource_allocation_graph: A directed graph representing
the resource allocation
graph of the system.
Returns:
True if a deadlock is detected, False otherwise.
"""
while stack:
# Get the current node from the stack.
node = stack.pop()
This algorithm can be used to detect deadlocks in any system where each
resource type has only one instance and tasks adhere to the single resource
request model. To use the algorithm, simply pass in the resource allocation
graph of the system to the deadlock_detection() function. The
function will return True if a deadlock is detected and False otherwise.
# Add nodes
resource_allocation_graph.add_nodes_from(["R1", "R2",
"R3", "R4", "R5", "T1", "T2", "T3", "T4", "T5"])
# Add edges
resource_allocation_graph.add_edges_from([("R1", "T1"),
("T1", "R2"), ("R2", "T3"), ("T3", "R4"), ("R4", "T4"),
("T4", "R3"), ("R3", "T5"), ("T5", "R5"), ("R5", "T2"),
("T2", "R1")])
# Detect deadlocks
deadlock_detected =
deadlock_detection(resource_allocation_graph)
# Print result
if deadlock_detected:
print("Deadlock detected!")
else:
print("No deadlock detected.")
Output:
Deadlock detected!
Total System Resources Table (N): Captures the total number of units
for each resource type (N1, N2, N3, …, Nk).
1. Identify a row (i) in table D where Dij < Aj holds for all 1 ≤ j ≤ k. If no
such row exists, a deadlock is confirmed, and the algorithm concludes.
Step 1 seeks a task whose resource demands can be satisfied. If such a task
is found, it can proceed until completion. The resources freed from this task
are returned to the pool (step 2), becoming available for other tasks,
allowing them to continue and finish their execution.
Illustrative Example:
N: [4, 6, 2]
A: [1, 2, 0]
C: [0, 2, 0]
Task 1: [1, 1, 0]
Task 2: [1, 1, 1]
Task 3: [1, 0, 1]
D: [2, 2, 2]
Task 1: [1, 1, 0]
Task 2: [0, 1, 0]
Task 3: [1, 1, 1]
def deadlock_detection(total_system_resources,
available_system_resources,
tasks_resources_assigned_table,
tasks_resources_demand_table):
"""Detects deadlocks in a system with multiple resource
instances.
Args:
total_system_resources: A list of the total number of
units for each resource
type.
available_system_resources: A list of the remaining
units for each resource
type available for allocation.
tasks_resources_assigned_table: A 2D list where each
row represents a task
and each column represents a resource type. The
value at each row and column
represents the number of units of the resource
assigned to the task.
tasks_resources_demand_table: A 2D list where each
row represents a task
and each column represents a resource type. The
value at each row and column
represents the number of units of the resource
demanded by the task.
Returns:
True if a deadlock is detected, False otherwise.
"""
# Create a list of the tasks that are still incomplete.
incomplete_tasks = []
for i in range(len(tasks_resources_demand_table)):
if any(tasks_resources_demand_table[i][j] >
available_system_resources[j]
for j in
range(len(tasks_resources_demand_table[0]))):
incomplete_tasks.append(i)
This algorithm can be used to detect deadlocks in any system where each
resource type has multiple instances and tasks adhere to the AND model of
resource requests. To use the algorithm, simply pass in the total system
resources, available system resources, task resources assigned table, and
task resources demand table to the deadlock_detection() function.
The function will return True if a deadlock is detected and False
otherwise.
# Detect deadlocks.
deadlock_detected =
deadlock_detection(total_system_resources,
available_system_res
ources,
tasks_resources_assi
gned_table,
tasks_resources_dema
nd_table)
Output:
A deadlock has been detected.
However, if we modify the task resources demand table such that task 3
requires [0, 1, 1] instead of [0, 1, 0], then the deadlock detection algorithm
will return True.
6.5.3 BANKER'S ALGORITHM: A DEADLOCK
AVOIDANCE STRATEGY
One of the most prominent deadlock avoidance algorithms in computing is
the Banker's Algorithm. Named after its analogy to a bank managing
customer resource requests, this algorithm operates on key principles:
Banker's Algorithm operates under the assumption that there are n account
holders (processes) in a bank (system) with a total sum of money
(resources). The bank must ensure that it can grant loans (allocate
resources) without risking bankruptcy (deadlock). The algorithm ensures
that, even if all account holders attempt to withdraw their money (request
resources) simultaneously, the bank can meet their needs without going
bankrupt.
Scenario:
Resource Information:
Work = Available
Finish[P0] = True
Repeat the steps until all processes finish. If all processes finish, the system
is in a safe state. In this case, the safe sequence is <P0, P1, P3, P4, P2>.
safe_sequence = []
# Example usage
if __name__ == "__main__":
# Define available resources
available_resources = [3, 3, 2]
import threading
def process1():
with resource_locks[0]:
print("Process 1 acquired Resource 1")
with resource_locks[1]:
print("Process 1 acquired Resource 2")
def process2():
with resource_locks[1]:
print("Process 2 acquired Resource 2")
with resource_locks[0]:
print("Process 2 acquired Resource 1")
thread1 = threading.Thread(target=process1)
thread2 = threading.Thread(target=process2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
import threading
def process1():
with resource_locks[0]:
print("Process 1 acquired Resource 1")
with resource_locks[1]:
print("Process 1 acquired Resource 2")
def process2():
while True:
with resource_locks[1]:
print("Process 2 acquired Resource 2")
with resource_locks[0]:
print("Process 2 acquired Resource 1")
thread1 = threading.Thread(target=process1)
thread2 = threading.Thread(target=process2)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
import threading
def process1():
with resource_locks[0]:
print("Process 1 acquired Resource 1")
with resource_locks[1]:
print("Process 1 acquired Resource 2")
def process2():
while True:
with resource_locks[1]:
print("Process 2 acquired Resource 2")
with resource_locks[0]:
# Deadlock detected!
break
import threading
import time
def process1():
with resource_locks[0]:
print("Process 1 acquired Resource 1")
time.sleep(1)
with resource_locks[1]:
print("Process 1 acquired Resource 2")
def process2():
with resource_locks[1]:
print("Process 2 acquired Resource 2")
time.sleep(1)
with resource_locks[0]:
print("Process 2 acquired Resource 1")
To implement the deadlock detection and resolution part, you will need to
design a mechanism to detect the deadlock (e.g., by checking if both threads
are alive) and preempt one of the threads gracefully, freeing the resources it
holds. After preemption, the remaining thread can proceed without
deadlock constraints.
Figure 7.1: Physical vs. logical address: The MMU maps logical addresses
to physical addresses.
ddress binding takes on three primary forms:
At its heart, the MMU maintains a page table—a critical mapping tool that
associates logical page numbers with their physical counterparts. Logical
pages typically represent discrete memory blocks, usually 4KB in size,
while physical pages mirror this size and reside in physical memory.
When the CPU needs to access memory, it sends a logical address to the
MMU. The MMU, with assistance from the relocation register, utilizes the
page table to translate the logical address into the corresponding physical
address. This resulting physical address is then sent to the memory
controller, which handles the retrieval of data from memory.
Figure 7.2: Relocation register: Logical address 324 mapped to physical
address 2324
Beyond this essential translation function, the MMU, working alongside the
relocation register, fulfills several other crucial roles:
In the figure above, you can see how external fragmentation appears in the
memory space. Various allocated and free memory blocks are interspersed,
making it difficult to find a continuous region for a new process.
I
Figure 7.4: Internal fragmentation: Unused memory within an allocated
block
n the figure above, internal fragmentation is demonstrated. The allocated
memory block (in orange) is larger than what the process actually requires,
leading to wasted memory space.
7.3.2.1 Compaction
C
Figure 7.5: Compaction: Rearranging memory to eliminate fragmentation
ompaction, illustrated in Figure 7.3.4.1, involves rearranging memory
contents to eliminate external fragmentation. It works by relocating
allocated memory blocks to one end of the memory and consolidating free
memory into a contiguous block at the other end. While effective,
compaction can be resource-intensive and may require temporary process
halting.
7.3.2.2 Segmentation
Figure 7.6: Segmentation: Dividing memory into logical segments
P
Figure 7.7: Paging: Memory management using fixed-size pages
aging, as depicted in Figure 7.7, divides memory into fixed-size blocks
called pages and allocates memory to processes in page-sized increments.
This approach eliminates both external and internal fragmentation,
simplifying memory management. Paging is widely adopted in modern
operating systems.
A
Figure 7.8: Paging Concept
s illustrated in Figure 7.8, both physical and logical memory undergo a
transformation into discrete, uniform-sized pages. Each page is equipped
with a unique identifier, simplifying the process of addressing. Yet, an
integral aspect often mentioned in tandem with paging is the concept of
"frames."
In the paging puzzle, frames serve as the counterpart to pages. While pages
represent units of logical and physical memory, frames exclusively pertain
to physical memory. The relationship between pages and frames is pivotal.
In essence, while pages serve as the units for logical and physical
addressing, frames are the missing piece of the puzzle, exclusively
dedicated to physical memory. Together, they form the foundation of
efficient memory management in modern computer systems, assuring
streamlined allocation and utilization of memory resources.
7.4.2 Translation Lookaside Buffer (TLB)
As modern computer systems grapple with the management of ever-
expanding memory capacities, the efficiency of accessing data stored in
pages has emerged as a paramount concern for achieving optimal
performance. The Translation Lookaside Buffer (TLB), showcased in
Figure 7.4.2, takes center stage as a mission-critical component, operating
as a high-speed cache to significantly amplify memory access capabilities.
TLB Hit:
TLB Miss:
2. The TLB is examined, but the sought-after page table entry is not
found (TLB miss).
3. In this scenario, the page number becomes the key to accessing the
page table residing in main memory (assuming that the page table
encompasses all page table entries).
4. Once the page number is matched to the page table, the corresponding
frame number is unveiled, divulging the main memory page's location.
5. Simultaneously, the TLB is updated with the new Page Table Entry
(PTE). If there is no space available, one of the replacement
techniques, such as FIFO, LRU, or MFU, comes into play to make
room for the new entry.
The TLB plays a pivotal role in the quest to reduce memory access time. As
a high-speed associative cache, it significantly contributes to enhancing
system efficiency. The concept of Effective Memory Access Time (EMAT)
encapsulates the benefits of TLB integration:
Where:
h (TLB hit ratio) = 0.85, indicating that 85% of the time, the required
page table entry is found in the TLB.
Now, let's calculate the Effective Memory Access Time (EMAT) using the
formula:
Benefits of COW
Example
Now, let's say process A decides to modify the file. At this point, and only
then, COW comes into play. When process A attempts to make changes,
COW ensures that a duplicate page is created exclusively for process A.
This new page contains the modified data, while process B continues to use
the original shared page.
Conside
Figure 7.10: Shared Memory
r an example to illustrate Shared Memory:
def process1(data):
# Process 1 reads data
print("Process 1 reads data:", data)
def process2(data):
# Process 2 also reads data
print("Process 2 reads data:", data)
if __name__ == "__main__":
# Create a multiprocessing pool
pool = multiprocessing.Pool(processes=2)
import multiprocessing
# Create a manager
manager = multiprocessing.Manager()
def process1(data):
# Modify shared memory
data[0] = 1
print("Process 1 writes to shared memory")
def process2(data):
# Access shared memory
print("Process 2 reads from shared memory:", data[0])
if __name__ == "__main__":
# Create a multiprocessing pool
pool = multiprocessing.Pool(processes=2)
import mmap
These Python examples provide a glimpse into how you can implement
page sharing techniques in Python, making memory management accessible
and efficient within a high-level language. Understanding these techniques
can greatly benefit developers in optimizing their applications and
achieving better memory efficiency.
8
VIRTUAL MEMORY MANAGEMENT
Memory Mapping - Files can be mapped into a process's address space for
simplified access. Changes are reflected in both memory and the file
system.
Virtual memory allows computers to run programs that are much larger than
the amount of physical memory available, enabling the development of
more complex and powerful software applications.
R
Figure 8.1: A visual representation of Demand Paging in action,
optimizing memory usage.
eal-World Illustration:
L2 Page Tables: Located below the L1 page table, these tables further
refine the mapping by breaking down virtual address regions into
smaller segments. Each entry in an L2 page table typically points to an
L3 page table or directly to physical memory.
PTBR: The PTBR is a register that stores the physical address of the
L1 page table. The CPU uses the PTBR to start walking the page table
when it needs to translate a virtual address to a physical address.
Here is an example of how the PTBR and PTE are used to translate a virtual
address to a physical address:
1. The CPU starts by checking the TLB. If the virtual address is not
found in the TLB, a TLB miss occurs and the CPU must walk the page
table.
3. The CPU walks the L1 page table to find the PTE for the virtual
address.
4. The CPU uses the information in the PTE to access the memory
location.
If the PTE for the virtual address is not found in the L1 page table, a page
fault occurs and the operating system must load the PTE into memory. The
operating system then updates the L1 page table with the new PTE and the
CPU can retry the translation.
Multi-level page tables use the same basic principles, but the PTBR and
PTEs are more complex. For example, the PTBR for a multi-level page
table might point to an L2 page table instead of an L1 page table. The L2
page table would then contain pointers to L3 page tables, which in turn
would contain the PTEs for individual pages.
The use of multi-level page tables allows the operating system to efficiently
manage a large virtual address space while using only a small amount of
physical memory.
Architecture-Dependent Variations:
It's important to note that the specific formats and structures of page tables
can vary significantly across computer architectures. Different architectures
may employ variations in the number of levels in their page tables, the sizes
of page tables, and the format of page table entries. These details are highly
architecture-dependent and are optimized for the particular characteristics
and requirements of the hardware.
With pure demand paging, no pages are preemptively loaded into memory
at program startup. Pages are only brought into physical memory on an on-
demand basis when a page fault occurs. This approach maximizes memory
utilization efficiency, since no unused pages occupy RAM. However, it can
increase page fault frequency and the associated overhead, since every
single page access initially triggers a page fault. This repeated paging
activity can degrade program performance.
2. The OS walks the multi-level page table to translate the logical address
into a physical frame number and offset.
3. The page table entry contains a valid bit indicating if the page is
resident in physical memory.
4. If valid bit is 1, the page is present in a memory frame. The CPU uses
the frame number to directly access the contents.
5. If valid bit is 0, a page fault exception is triggered. This signals that the
page needs to be demand paged into memory.
6. The OS page fault handler uses the page number to lookup the page
contents from the backing store/swap space on disk.
7. The page is loaded into a free physical frame. The page table is
updated to map the logical page to the frame, with valid bit now set.
8. The instruction that triggered the page fault is restarted and now
succeeds because the page is resident in memory.
9. The CPU continues execution by accessing the page contents using the
translated physical address.
Implementation of Segmentation:
When the CPU needs to translate a virtual address into a physical address, it
consults the segment table to determine the appropriate segment for the
given address. Once the segment is identified, the CPU uses the information
from the table entry to access the corresponding memory location in
physical memory.
Example of Segmentation:
Consider two processes, Process 1 and Process 2, each with its own set of
segments:
Process 1:
Process 2:
For each process, the operating system maintains separate segment tables. If
Process 1 attempts to access a memory location within the code segment of
Process 2, the CPU consults Process 1's segment table and finds no entry
for that address. Consequently, a page fault or memory access violation is
generated, and the operating system can intervene to handle the situation
appropriately.
Advantages of FIFO:
Disadvantages of FIFO:
class FIFOPageReplacement:
def __init__(self, capacity):
self.capacity = capacity
self.memory = []
def display_memory(self):
print("Current Memory State:", self.memory)
# Example usage:
if __name__ == "__main__":
memory_capacity = 3
page_reference_sequence = [2, 3, 4, 2, 1, 3, 7, 5, 4,
3]
fifo = FIFOPageReplacement(memory_capacity)
Advantages of LRU:
Disadvantages of LRU:
Here's a Python code example to illustrate the LRU (Least Recently Used)
page replacement algorithm:
class LRUPageReplacement:
def __init__(self, capacity):
self.capacity = capacity
self.memory = OrderedDict() # Using an
OrderedDict to maintain page order
def display_memory(self):
print("Current Memory State:",
list(self.memory.keys()))
# Example usage:
if __name__ == "__main__":
memory_capacity = 3
page_reference_sequence = [2, 3, 4, 2, 1, 3, 7, 5, 4,
3]
lru = LRUPageReplacement(memory_capacity)
The example usage at the end demonstrates how the LRU algorithm handles
a sequence of page references and displays the memory state after each
page reference. You can adjust the memory_capacity and
page_reference_sequence variables to test different scenarios.
for i in range(len(reference_string)):
page = reference_string[i]
page_faults += 1
future_references[page] =
reference_string[i:].index(page) + i
return page_faults
# Example usage:
if __name__ == "__main__":
reference_string = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4,
5]
frames = 3
page_faults =
optimal_page_replacement(reference_string, frames)
print("Total Page Faults:", page_faults)
Can lead to inefficient memory usage and high page fault rates.
import random
page_faults += 1
return page_faults
# Example usage:
if __name__ == "__main__":
reference_string = [1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4,
5]
frames = 3
page_faults =
random_page_replacement(reference_string, frames)
print("Total Page Faults:", page_faults)
In this code, the random_page_replacement function simulates the
Random Page Replacement algorithm. It randomly selects a page from
memory to replace when a page fault occurs. This algorithm is
straightforward to implement but does not consider page usage patterns,
making it less efficient compared to more advanced page replacement
algorithms.
Total Page Faults with Random: 9 (In this case with randomly chosen
pages)
Summary:
4. Low CPU Utilization: The CPU dedicates more time to servicing page
faults than executing processes.
4. Tune Swap Space: Adjusting the size of the swap file can provide
additional virtual memory, reducing the pressure on physical memory.
For instance, consider a system equipped with 8GB of RAM; the operating
system could allocate up to 12GB of virtual memory to processes, with the
expectation that only 8GB will be actively used.
Sizing Swap Space: Properly sizing the swap space to handle peak
paging requirements is essential to avoid potential bottlenecks during
memory reclamation.
2. Page Faults: Page faults, which occur when a requested page is not in
physical memory, can impact system performance. Excessive page
faults, especially in scenarios with insufficient physical memory, can
lead to thrashing and significantly degrade overall system
responsiveness.
7. Disk I/O: Frequent page swaps between physical memory and disk
can lead to increased disk I/O operations, which may wear out storage
devices over time. Additionally, heavy disk I/O can become a
bottleneck in terms of system performance.
1. Identify the missing page. The operating system uses the page table
to identify the missing page. The page table is a data structure that
maps virtual memory addresses to physical memory addresses.
2. Load the missing page into memory. The operating system retrieves
the missing page from secondary storage (disk) and loads it into
physical memory.
3. Update the page table. The operating system updates the page table
to indicate that the missing page is now in memory.
T
Figure 8.5 Page fault: A memory access exception that occurs when the
CPU cannot find a valid page table entry for a given virtual address.
ypes of page faults
Minor page fault: A minor page fault occurs when the missing page
is already in memory, but it is not marked as being loaded. This
typically happens when a program first accesses a memory page.
Major page fault: A major page fault occurs when the missing page is
not in memory and must be loaded from disk.
Invalid page fault: An invalid page fault occurs when the program
tries to access a memory page that does not exist.
Minor page faults are typically less disruptive to system performance than
major page faults.
Page fault time: The page fault time is the total time it takes to handle
a page fault. This includes the time to find the required page on disk,
swap it into RAM, update data structures, and restart the interrupted
program.
Page fault delay: The page fault delay is the time interval between the
moment a page fault occurs and the time when the corresponding page
is fully loaded into the main memory, allowing the program to
continue its execution.
Hard page fault: A hard page fault occurs when the missing page is
not present in any available storage, including both RAM and
secondary storage.
Soft page fault: A soft page fault occurs when the required page is
found elsewhere in memory or storage but hasn't been assigned to the
correct address space of the requesting process.
Minor page fault: A minor page fault signifies that the page being
accessed is already in memory, but it is marked as not yet loaded. It
typically happens when a program first accesses a memory page,
causing the OS to load it into the main memory for future use.
Data Isolation: After the copy is created, the process that triggered the
copy possesses its own isolated version of the memory page. Other
processes that continue to access the original page remain unaffected
by this change.
In addition to the benefits mentioned above, COW also offers the following
advantages:
A
Figure 8.6 Belady’s Anomaly
s illustrated in Figure 8.6, at point P, an increase in the number of page
frames from 7 to 8 results in an unexpected increase in the number of page
faults, going from 3 to 4. This occurrence highlights the presence of
Belady's anomaly at point Q.
We will analyze the page frames using the FIFO replacement policy with
different numbers of available page frames.
Initially, we have three page frames available, and we'll see how page faults
occur:
Now, let's see how the same page sequence behaves when we increase the
number of available page frames to four:
It's worth noting that frame allocation strategies are not mutually exclusive.
Proportional and priority-based allocation strategies can be combined to
create hybrid approaches. For instance, a certain number of frames could be
allocated to each process based on its priority, with the remaining frames
divided among the processes in proportion to their memory needs. These
hybrid approaches seek to strike a balance between fairness and efficiency.
A Practical Scenario
The choice of frame allocation strategy in this case will determine how
these diverse memory requirements are met, directly influencing system
performance and fairness.
Local Replacement:
Advantage: The pages in memory for a particular process and the page
fault ratio are influenced by only that process.
When a process needs a page not in memory, it can allocate a frame from
the pool of all frames, even if that frame is currently allocated to another
process.
Example Scenario:
3. Efficient Data Sharing: Multiple processes can map the same file into
their address spaces, facilitating efficient data sharing.
import mmap
Alternatively, the numpy library can be utilized with the following Python
code to create a memory-mapped array:
import numpy as np
import multiprocessing
Demand Paging
class PageTable:
def __init__(self, num_pages):
self.num_pages = num_pages
self.page_table = [-1] * num_pages
class MemoryManager:
def __init__(self, num_frames, backing_store):
self.num_frames = num_frames
self.memory = [None] * num_frames
self.backing_store = backing_store
self.frame_fifo = []
class BackingStore:
def __init__(self, filename):
self.filename = filename
In the next subsections, we'll delve deeper into page fault management in
Python and provide more comprehensive code examples to explore virtual
memory management further.
4. Update Page Table: The Page Table is updated to reflect the new
mapping of the logical page to the physical frame in memory.
class MemoryManager:
# ... (Previous code for MemoryManager)
In this section, we will provide Python code examples for virtual memory
management, building upon the concepts we've discussed throughout
Chapter 8. These examples will help you understand how virtual memory
works and how it can be simulated in Python.
In this example, we'll create a simple virtual memory system with demand
paging. We'll simulate a process accessing logical pages, and the memory
manager will handle page faults by loading pages from the backing store
into physical memory.
class MemoryManager:
def __init__(self, num_frames):
self.num_frames = num_frames
self.memory = [None] * num_frames
self.backing_store = BackingStore()
if self.memory[logical_page] is None:
# Page fault: Load the page from the backing
store
page_data =
self.backing_store.read_page(logical_page)
# Load the page into physical memory
self.memory[logical_page] = page_data
return logical_page # Return the same
logical page number
else:
# Page is already in physical memory
return logical_page
class BackingStore:
def read_page(self, logical_page):
# Simulate reading a page from the backing store
return f"Page {logical_page} data"
# Create a virtual memory system
memory_manager = MemoryManager(num_frames=4)
# Simulate a process accessing logical pages
logical_pages = [0, 1, 2, 3, 4, 5, 2, 1, 6, 0]
for logical_page in logical_pages:
physical_frame =
memory_manager.handle_page_fault(logical_page)
if physical_frame is not None:
print(f"Accessed logical page {logical_page},
mapped to physical frame {physical_frame}")
else:
print(f"Invalid logical page {logical_page}")
This Python code simulates demand paging, where pages are loaded into
physical memory as needed. It uses a MemoryManager class to manage
page faults and a BackingStore class to simulate the backing store.
In this example, we'll extend the virtual memory system to include page
replacement using the FIFO (First-In-First-Out) algorithm. When physical
memory is full, the least recently used page is replaced with the incoming
page.
class MemoryManager:
def __init__(self, num_frames):
self.num_frames = num_frames
self.memory = [None] * num_frames
self.backing_store = BackingStore()
if self.memory[logical_page] is None:
# Page fault: Load the page from the backing
store
page_data =
self.backing_store.read_page(logical_page)
# Load the page into physical memory
self.memory[logical_page] = page_data
return logical_page # Return the same
logical page number
else:
# Page is already in physical memory
return logical_page
class BackingStore:
def read_page(self, logical_page):
# Simulate reading a page from the backing store
return f"Page {logical_page} data"
class MemoryManagerFIFO(MemoryManager):
def __init__(self, num_frames):
super().__init__(num_frames)
self.page_queue = []
if self.memory[logical_page] is None:
if len(self.page_queue) < self.num_frames:
# Frame is available, load the page
physical_frame = len(self.page_queue)
else:
# Replace the oldest page using FIFO
oldest_page = self.page_queue.pop(0)
# No need to use self.memory to index
logical pages
physical_frame = logical_page
self.memory[physical_frame] = None
Files: Files are the elemental vessels of data storage within a computer
system. They can house diverse forms of information, ranging from
text and programs to multimedia content.
File Paths: The concept of file paths is akin to a file's digital address.
It represents the file's location within the directory hierarchy.
Understanding file paths is essential for precisely locating and
referencing files within the vast digital landscape.
These components form the bedrock of file systems, allowing users and
computer systems to organize, access, and safeguard data with precision
and efficiency.
9.1.2 File and Directory Hierarchy
File systems organize files and directories in a hierarchical manner, much
like the branches of a tree. At the top is the root directory, which is
represented as "/" in Unix-based systems. Below the root directory, there
can be multiple subdirectories, each containing its own files and
subdirectories. This hierarchy provides a structured and organized way to
store and access data.
For example, the following file system hierarchy shows how files and
directories might be organized on a personal computer:
/
├── Desktop
│ ├── Documents
│ │ ├── Projects
│ │ │ └── My Project
│ │ │ └── main.py
│ │ └── Other Documents
│ └── Other Files
├── Downloads
│ └── my_downloaded_file.pdf
├── Music
│ └── my_favorite_song.mp3
├── Pictures
│ └── my_favorite_picture.jpg
└── Videos
└── my_favorite_video.mp4
Desktop: This directory contains files and directories that the user
accesses frequently.
File paths are used to specify the location of a file within the file system
hierarchy. For example, the file path
/Desktop/Documents/Projects/My Project/main.py refers
to the file main.py in the My Project subdirectory of the Documents
directory on the desktop.
File Name: The name of the file, which serves as its identifier.
File Size: The size of the file in bytes, indicating how much storage it
occupies.
File Type: The type or format of the file, such as text, image, or
executable.
Creation Date: The date and time when the file was created.
Slow Random Access: HDDs suffer from slower random access times
due to seek time (the time it takes to position the read/write head) and
rotational latency (the time it takes for the desired sector to rotate
under the head). As a result, file systems designed for HDDs
emphasize strategies that minimize head movement, such as
optimizing for data locality and promoting contiguous storage.
Figure 9.1Magnetic Hard Disk Drives
3. Tape Drives
Buffering and Streaming: File systems optimized for tape drives use
buffering and streaming techniques to maintain a continuous flow of
data. They also organize data in an append-only structure to simplify
data storage.
Figure 9.3: Tape Drives
Sequential Access: Optical discs like CDs and DVDs primarily allow
sequential access to data.
5
Figure 9.4: Optical Discs
. Cloud and Object Storage
Remote Data Storage: Cloud and object storage solutions store data
remotely over the network, introducing unique challenges for file
systems.
Advantages:
Limitations:
Files are constrained by their initial allocated size and cannot expand
beyond it.
9
Figure 9.5: Contiguous Allocation
Advantages:
Limitations:
A
Figure 9.7: Indexed Allocation
dvantages:
Limitations:
Reduced Disk Seeks: Larger blocks reduce the number of disk seeks
required for data retrieval, as more data can be read or written in a
single operation.
Suitable for Small Files: Smaller block sizes are ideal for efficiently
storing small files without significant wasted space.
Increased Disk Seeks: Smaller blocks require more disk seeks for I/O
operations, potentially impacting I/O performance.
Block sizes typically range from 512 bytes to 8192 bytes, depending on the
specific usage and requirements of the file system. For example, database
systems often favor smaller block sizes to optimize storage efficiency, while
media streaming systems may use larger block sizes to maximize
throughput.
In some cases, flexibility is provided through multi-block clustering, where
logical blocks are grouped into larger physical clusters on disk while
maintaining separate block allocation metadata. This approach balances the
benefits of large and small block sizes.
File Attributes: File attributes are the specific properties associated with
each file. They can include read, write, and execute permissions, owner
information, group information, and access timestamps. The combination of
these attributes determines how a file can be accessed and manipulated.
File Types: Files can have various types, such as regular files, directories,
symbolic links, and device files. Understanding the file type is essential for
determining how the file should be treated and processed.
9.3.2.1 Special File Types
Device Files: These files represent access points to I/O devices. Reads and
writes to these files communicate directly with the associated hardware
device.
Sparse Files: Sparse files are files with empty or unset blocks, designed to
save space on data that is inherently sparse. The file system optimizes
storage allocation for such files, ensuring efficient disk usage.
These special file types and attributes support a wide range of advanced
functionalities, including hardware access, network communication,
efficient data exchange between processes, and specialized data storage and
preservation techniques. Understanding these features provides deeper
insight into the advanced capabilities of file systems.
Root Directory: At the top of the hierarchy is the root directory, denoted by
'/'. It serves as the starting point for the entire directory tree and contains all
other directories and files.
File Paths: File paths are used to specify the location of a file within the
directory structure. An absolute path starts from the root directory and
provides a full path to the file, while a relative path starts from the current
directory and specifies a path relative to the current location.
Understanding file paths and directory structures is vital for effective file
navigation and management within an operating system.
Recovery Process: On reboot after a crash, the file system replays the
journal to reconstruct unsaved updates and restore consistency. The specific
journaling approach determines crash recovery performance.
import os
directory_path = "/path/to/directory"
if os.path.exists(directory_path) and
os.path.isdir(directory_path):
print(f"The directory {directory_path} exists.")
else:
print(f"The directory {directory_path} does not
exist.")
import os
directory_path = "/path/to/directory"
files = os.listdir(directory_path)
print(f"Files in {directory_path}:")
for file in files:
print(file)
os.mkdir('root')
os.mkdir('root/bin')
os.mkdir('root/usr')
os.mkdir('root/tmp')
Next, we can create files with various types, sizes and metadata:
# Create text file
f.write('This is a text
file')
This allows us to model key aspects of a real file system like hierarchy,
metadata and different file types. We could further extend the simulation by
adding user permissions, directories as special files, free space management
and other advanced features. Simulating file systems is a great way to
understand how they work under the hood.
1
0I/O MANAGEMENT
In the world of computing, Input/Output (I/O) operations are like the vital
links that connect computers with the outside world. They're the bits and
pieces that let your computer read data from storage, take input from your
keyboard, and send info across the internet. Welcome to Chapter 10, where
we're about to get our hands dirty and explore the intriguing realm of I/O
Management, a fundamental building block of today's computer systems.
Here, we're going to explore why efficient I/O management is a big deal,
why interrupts are like the superheroes of I/O operations, the inner
workings of the I/O subsystem, various ways to manage I/O, and how
Python swoops in to save the day when it comes to handling I/O like a pro.
Once you wrap up this chapter, you'll be packing some serious knowledge
and tools in your tech arsenal to turbocharge your computer systems.
They'll be running smoother, more reliable, and as responsive as a hot knife
slicing through butter.
10.1 IMPORTANCE OF I/O MANAGEMENT
Getting to Grips with Effective I/O Management
Imagine you're typing away on your keyboard. You'd expect those letters to
show up on the screen instantly, right? Well, that magic is all thanks to I/O
management making sure things flow seamlessly behind the scenes.
But why should you care about I/O management beyond speedy typing?
Well, my friend, it's because I/O management doesn't just affect your user
experience; it's the beating heart of your computer's performance and
responsiveness.
Let's talk servers for a sec. When oodles of folks hop onto a website
simultaneously, that server needs to juggle a ton of I/O operations to fetch
web pages, images, and all sorts of goodies. If the I/O system isn't up to
snuff, it can slow things down to a crawl or even bring the whole server
crashing down.
That's where efficient I/O management swoops in as the hero of the day. It
ensures those I/O operations happen lickety-split, cutting down on delays
and turbocharging your system's responsiveness. Think of I/O management
as that secret sauce that makes your computer go from good to great.It's the
behind-the-scenes wizardry that keeps everything running smoothly, yet it
doesn't always get the credit it deserves.But fear not, because in this
chapter, we're pulling back the curtain to reveal all the tricks of the trade.
You'll walk away with the skills to wield I/O management like a seasoned
pro.
10.2 INTERRUPTS AND THEIR ROLE IN I/O
OPERATIONS
Unpacking Interrupts
Interrupts are the CPU's way of staying on its toes. Instead of sitting around
twiddling its digital thumbs, the CPU can keep doing its thing, and when an
interrupt comes knocking, it knows it's time to switch gears and deal with
whatever's happening.
Alright, time to get down to the real nitty-gritty. Let's talk about how
interrupts totally change the game when it comes to I/O operations. Imagine
your computer is busy reading data from a network card. Without interrupts,
the CPU would be stuck in an endless loop, constantly checking the
network card's status. It's like repeatedly asking, "Are we there yet?" during
a road trip—it gets old real fast, and it's not efficient.
Interrupts flip the script. Instead of bugging the network card every
millisecond, the CPU can kick back and relax. When the network card has
data ready to go, it sends an interrupt signal, like a digital smoke signal, and
the CPU knows it's time to get to work. This game-changer makes the
whole system run like a well-oiled machine, super-efficient and lightning-
fast.
Types of Interrupts in I/O Management
3. Maskable Interrupts: These are like the "Do Not Disturb" signs for
interrupts. The CPU can snooze them temporarily to deal with more
urgent stuff.
Alright, buckle up because in the next sections, we're going to dig even
deeper into I/O management and uncover some sweet tricks of the trade to
make interrupts work their magic.
10.3 EXPLORING THE I/O SUBSYSTEM
Now, let's journey into the heart of the matter—the I/O subsystem. This
baby is like the conductor of the computer orchestra, making sure every
note plays just right. Here are the key players:
1. I/O Devices: These are the hardware whiz kids that let your computer
talk to the outside world. Think keyboards, mice, monitors, hard
drives, network cards, and printers.
3. I/O Channels: These are the highways where data cruises between the
CPU and I/O devices. Think of them as the express lanes for
information flow.
1. Block Devices: These folks are all about dealing with data in fixed-
size blocks or sectors. Hard drives and SSDs are the rock stars here.
These devices do their thing using interfaces like SATA, NVMe, or
SCSI.
4. Graphics Devices: These are the artists, painting the pixels on your
screen. GPUs (Graphics Processing Units) are the stars, and they speak
languages like HDMI, DisplayPort, or VGA.
Smooth communication between the CPU and I/O devices is the secret
sauce for peak system performance. It's like a well-choreographed dance:
a. Initiation: The CPU says, "Let's boogie!" and sends a command to the
device controller, telling it what to do (read or write) and where to do
it.
b. Execution: The device controller takes the lead, making sure the I/O
device busts a move.
d. Data Transfer: Data glides between the device controller and the CPU
through memory buffers or registers, depending on the device and
system architecture.
Exploring the inner workings of the I/O subsystem, from its nuts and bolts
to its communication wizardry, is like getting an exclusive backstage pass
to the tech world's most exciting gig. We're here to help you build computer
systems that are not just powerful but also nimble and responsive to your
commands. As we move forward, we'll dive deeper into I/O management
techniques and reveal how Python can be your trusty sidekick in conquering
complex I/O tasks, making the developer's life a whole lot smoother.
PCI Express (PCIe): This one's the speed demon, perfect for
connecting powerhouses like GPUs and NVMe SSDs. It's got the
bandwidth to make data zoom.
Picking the right bus architecture is like choosing the perfect car for a cross-
country adventure. Absolutely, it's all about fine-tuning the data flow for
your gadgets, ensuring they get the speed and attention they rightfully
deserve. Understanding these bus technologies is like having a secret
weapon for fine-tuning I/O performance, and we're about to unlock its
potential.
10.4 I/O MANAGEMENT TECHNIQUES AND
SYSTEM PERFORMANCE
Absolutely, let's get into the juicy details! This section is where the real I/O
management magic happens, and you're about to discover some seriously
cool techniques that can turbocharge your system's performance. So, grab
your tech wizard hat, and let's jump in. Efficient I/O management is like the
secret sauce for making your system lightning-fast and super responsive,
and it's packed with some awesome concepts.
3. Smoothing Data Flow: This one's like traffic management for your
data. Picture your CPU as a sports car and your output device as a
slow-moving truck. Without buffering, you'd be racing ahead and
constantly slamming on the brakes to match the truck's speed – not
efficient at all. But with buffering, you have a traffic cop (the buffer)
who lets the sports car (your CPU) cruise at full speed. The buffer
collects data and makes sure it's handed over to the truck at a pace it
can handle, like a smooth relay race. No data gets lost, and everything
flows like a well-choreographed dance. That's the magic of buffering!
Shortest Seek Time First: Armed with the knowledge of head movement,
this algorithm chooses the request with the shortest distance to traverse
from the current position. The goal is to optimize seek time, ensuring
swifter data retrieval.
Request A at track 10
Request B at track 22
Request C at track 15
Request D at track 40
Request E at track 5
We'll compare how each algorithm schedules these requests starting from
track 20.
Shortest Seek Time First: This algorithm selects the request with the
shortest seek time from the current position. Starting at track 20, it
would service E, A, C, B, and then D.
But hold on, there's a catch. If you're not careful, this magic show can turn
into a circus. Imagine your favorite book is open on the table, and it starts
flipping pages all by itself. That would be chaos, right? Well, in the tech
world, improperly accessed memory-mapped areas can lead to system
crashes. It's like your book flying around the room – exciting at first, but
then things get messy.
USB 4: It's like USB on steroids. More speed, more power, and it plays
nice with Thunderbolt.
CXL (The Compute Express Link): Sharing is caring, and CXL lets
devices share memory bandwidth. Data centers are getting a
makeover!
These tech advancements are like turbochargers for your system. They
boost speed, cut down delays, and bring new tricks to the table. Knowing
about these shiny new toys helps you future-proof your system designs.
10.5 PYTHON FOR I/O OPERATIONS
Absolutely, let's keep the momentum going as we explore Python's prowess
in the realm of I/O operations. Python, often hailed for its simplicity and
adaptability, is your go-to tool for tackling I/O tasks like a seasoned pro.
We're about to embark on a journey that will unveil Python's potential as
your trusty sidekick in I/O management. But that's not all – we'll also roll
up our sleeves and get hands-on with some practical code examples to make
all of this come to life. So, are you ready to unlock the power of Python in
the world of I/O operations? Let's dive right in!
Python's appeal is like universal glue for I/O operations, and here's why:
Interrupts are like uninvited guests at your party, but Python knows how to
handle them with grace. Here's how:
Let's put Python's I/O prowess to the test with a real-world example.
Imagine you're tasked with reading data from a sensor and jotting it down in
a file. Python's got your back with a code snippet like this:
import asyncio
def log_data(sensor_data):
# Log the sensor data to a file
with open("sensor_log.txt", "a") as file:
file.write(sensor_data + "\n")
if __name__ == "__main__":
asyncio.run(read_sensor())
Python's adaptability makes it your go-to partner for I/O tasks, whether
you're reading sensors or orchestrating intricate network communication.
With Python's magic wand in hand, you can build efficient, reliable systems
that play nice with various I/O devices and handle those surprise party
crashers like a pro.
R
EFERENCES
Silberschatz, Galvin, and Gagne. Operating System Concepts. 10th
ed., Wiley, 2018. - This is a classic and comprehensive operating
systems textbook that covers fundamental concepts and modern
developments. It can be referenced for foundational OS topics.