Operating Systems Practical Workbook
Operating Systems Practical Workbook
SE-303
Operating Systems
Name:
Seat #:
Batch:
Semester:
Year:
Prepared By
Approved By
LAB SESSION 01
Mastering Basic OS Commands
THEORY
Welcome to the command-line interface (CLI), or shell. It's a powerful text-based program that allows
you to interact directly with your operating system. You issue instructions by typing commands, and
the shell executes them for you.
1. An external program, which is an executable file located on your system (e.g., /bin/ls).
2. A shell built-in, which is a command handled directly by the shell itself for efficiency (e.g.,
cd, pwd, history).
Command Structure
• Command: The name of the program or built-in you want to run (e.g., ls).
• Options: Modify the command's behavior. They are usually preceded by a single dash (-l) for
the short form or two dashes (--list) for the long form.
• Arguments: The items the command acts upon, typically filenames or directories (e.g.,
/home/user).
The shell provides powerful features for managing a command's input and output, which is normally
your keyboard and screen.
• > (Redirect Output): Sends the output of a command to a file, overwriting the file if it already
exists.
o ls /bin > bin_contents.txt (Saves the list of files in /bin to bin_contents.txt)
• >> (Append Output): Sends the output of a command to a file, appending to the end of the
file without overwriting.
o ls /etc >> [Link] (Adds the list of files in /etc to the end of [Link])
• | (Pipe): Sends the output of one command to be used as the input for a second command. This
is one of the most powerful features of the command line.
o ls /bin | grep "zip" (Lists all files in /bin and then filters that list to show only lines
containing "zip")
1) ls (List)
4
Department of Software Engineering
Displays the full, absolute path of the directory you are currently in.
3) cd (Change Directory)
Displays the contents of a file one screen at a time, allowing you to scroll up and down. Press q to quit.
Searches for files and directories based on criteria like name, size, or modification time.
• find . -name "*.txt": Finds all files ending in .txt in the current directory (.) and all its
subdirectories.
• find /home -type d -name "Documents": Searches within /home for a directory named
"Documents".
• Common Options:
o -name: Searches by filename (can use wildcards like *).
o -type f: Finds only files.
o -type d: Finds only directories.
o -mtime -2: Finds files modified in the last 2 days.
o -user ann: Finds files owned by the user "ann".
• grep "root" /etc/passwd: Prints all lines in /etc/passwd that contain the word "root".
• Common Options:
o -i: Performs a case-insensitive search.
o -v: Inverts the match, showing lines that do not contain the pattern.
5
Department of Software Engineering
7) wc (Word Count)
8) su (Substitute User)
Allows you to switch your user identity. Caution: Using su to become the root user gives you complete
administrative power. Be very careful.
• su: Prompts for the root user's password to become the superuser.
• su jekyll: Prompts for the user jekyll's password to become that user.
9) man (Manual)
Displays the manual page for any command, providing detailed information about its usage and options.
Press q to quit.
10) history
6
Department of Software Engineering
EXERCISE
1. List all files in your home directory, including hidden ones, in the detailed long format with
human-readable file sizes.
2. Save the list of all executable programs in the /bin directory into a file in your home folder
named bin_list.txt.
3. Find all files in the /etc directory that were modified in the last 5 days.
5. List all files in the current directory that end with the .conf extension and sort them by size,
with the largest file appearing first.
6. Count the total number of files and directories located in your home directory. (Hint: Pipe the
output of one command to another).
7. Recursively search the /home directory for any file containing the text "error" (case-insensitive)
and display only the names of the files that contain it.
8. Display the first 10 lines of the history command's output. (Hint: You will need to use a pipe
and a command like head).
7
Department of Software Engineering
LAB SESSION 02
Handling Files & Directories
THEORY
Think of the Linux filesystem as a large digital filing cabinet. The most basic unit is a file, which is a
distinct chunk of information, like a text document or a program. To keep files organized, we use
directories (which you can think of as folders).
A key feature of the Linux filesystem is its tree-like structure. It all starts from a single, top-level
directory called the root directory, represented by a slash (/). Every other directory is a subdirectory
that branches off from the root or another directory.
Filenames: In Linux, filenames can be up to 256 characters long and are case-sensitive ([Link] is
different from [Link]). You can use letters, numbers, underscores (_), dashes (-), and dots (.). It is
best to avoid using spaces or special shell characters like *, ?, & in filenames.
The Home Directory: Every user has a special place to store their personal files called a home
directory, typically located at /home/username. This is your personal workspace.
Linux organizes system files into standard directories to keep things predictable. Here are some of the
most important ones:
Directory Purpose
/ The root directory; the top of the entire filesystem tree.
/home Contains the personal home directories for all users.
/bin Holds essential user binaries (executable programs) like ls and cp.
/sbin Holds essential system binaries, mainly for administration.
/etc Contains system-wide configuration files.
/var For variable data, such as logs (/var/log).
/tmp A place for temporary files.
/dev Contains special device files that represent hardware (disks, terminals).
8
Department of Software Engineering
o mkdir project_files
• cat (Concatenate): A versatile command. You can use it with output redirection (>) to create
a text file.
o cat > [Link] (Lets you type text. Press CTRL+D on a new line to save and exit).
Every file and directory in Linux has a set of permissions that determines who can read, write, or execute
it. When you run ls -l, you see a string like -rwxr-xr--.
• The first character (-) indicates the file type (d for directory, - for a regular file).
• The next nine characters are three sets of permissions for:
1. User (Owner): The person who created the file. (rwx)
2. Group: The user group the file belongs to. (r-x)
3. Other: Everyone else. (r--)
Permission Meanings
The chmod (change mode) command is used to change permissions. The easiest way is with numeric
(octal) codes.
Code Permission
4 Read (r)
9
Department of Software Engineering
Code Permission
2 Write (w)
1 Execute (x)
0 No Permission (-)
You add the numbers to get the desired permission set. A three-digit code sets permissions for User,
Group, and Other.
• chmod 755 [Link]: rwx r-x r-x -> Owner can do everything; others can read and execute.
(Common for scripts).
• chmod 644 [Link]: rw- r-- r-- -> Owner can read/write; others can only read. (Common for
documents).
• chmod 700 private_folder: rwx --- --- -> Only the owner can access, read, write, and execute.
(Common for private directories).
10
Department of Software Engineering
EXERCISE
In this exercise, you will create and manage the file structure for a fictional project.
• Create an empty file inside the src directory named main.c using the touch command.
• Using the cat command with redirection (>), create a file in the docs directory named
[Link]. Add the following lines of text to it:
• Project Alpha
• This is the main documentation file.
5. Manage Permissions
6. Cleanup
• Remove the entire archive directory and its contents with a single command.
7. Conceptual Questions
• What is the practical difference between the permissions 777 and 775 on a directory?
• What command would you use to find out the file type of an unknown file (e.g., src/main.c)?
(Hint: The command is file).
• What is the difference between the cp and mv commands?
11
Department of Software Engineering
Remarks and
Signature
12
Department of Software Engineering
LAB SESSION 03
Managing System Processes
THEORY
What is a Process?
Everything that runs on a Linux system is a process. From the graphical interface you see to the simple
commands you type, each is a program in a state of execution. A process is a single running program
with its own virtual address space, while a job is a task started by the shell, which might consist of one
or more processes (e.g., in a command pipeline).
Types of Processes:
• Interactive Processes: Started by and interact with a user through a terminal (shell). They
can run in the foreground (locking your terminal until they finish) or background (letting you
continue to use the terminal).
• Daemon Processes: System-related processes that run in the background without any user
interaction. They are typically started at boot and perform essential system tasks (e.g.,
networking, logging).
| Column | Description |
| :--- | :--- |
| PID | The Process ID. A unique number for each process. You need this to control a specific process. |
13
Department of Software Engineering
| STAT | The current status of the process: R (Running), S (Sleeping), T (Stopped), Z (Zombie - a dead process
that hasn't been cleaned up yet). |
2. Controlling Processes
Sending Signals with kill and killall
You don't directly "stop" a process; instead, you send it a signal. The kill command is used to send
signals to a process using its PID.
• Graceful Termination (SIGTERM - 15): This is the default. It asks the process to shut
down cleanly.
o kill 1234
• Forced Termination (SIGKILL - 9): This is a non-ignorable signal that forces the process
to terminate immediately. Use this if the graceful kill doesn't work.
o kill -9 1234
The killall command works similarly but uses the process name instead of the PID.
• killall firefox
14
Department of Software Engineering
EXERCISE
1. Viewing Your Processes
• Use the ps command to find the PID of your current shell (e.g., bash).
• Run the top command. While it's running, sort the processes by memory usage. What is the
command of the process using the most memory?
• Exit top.
2. Backgrounding a Process
• Run the command sleep 500 in the background.
• Use the jobs command to verify that the process is running in the background.
• Use ps aux | grep sleep to find the PID of your sleep process.
4. Terminating Processes
• Using the PID you found in step 2, terminate the sleep 500 process with the standard kill
command. Verify with jobs that it is gone.
• Start another background process: sleep 600 &. This time, use the killall command with the
process name to terminate it.
5. Conceptual Questions
• What is the purpose of the process with PID 1 (often named init or systemd)? Why should you
never try to kill it?
• What is the difference between a normal kill <PID> and kill -9 <PID>? When would you use
the second one?
• What is a "zombie" process? Which command would you use to see if you have any on your
system?
15
Department of Software Engineering
LAB SESSION 04
Practice Shell Scripting
THEORY
Normally, you type commands into the shell one at a time. A shell script is simply a text file containing
a series of commands. When you run the script, the shell executes these commands in sequence, one
after the other. This is incredibly useful for automating repetitive tasks, creating custom commands,
and managing complex workflows.
Step 1: Write the Script Use a text editor (like nano, vim, or gedit) to create a file. The very first line
of your script should be the shebang, which tells the system which shell to use to interpret the script.
For bash, this is #!/bin/bash.
#!/bin/bash
Step 2: Make the Script Executable By default, new text files do not have permission to be executed.
You must grant this permission using the chmod command.
chmod +x [Link]
./[Link]
Output:
Hello, World!
The current date and time is: Mon Aug 25 [Link] PKT 2025
Using Variables
Like any programming language, shell scripting allows you to use variables to store and reuse data. In
bash, variables are untyped, meaning they can hold strings, numbers, etc., without special declaration.
16
Department of Software Engineering
# Correct syntax
GREETING="Hello there"
USER_COUNT=5
Accessing Values: To get the value stored in a variable, precede its name with a dollar sign ($). When
using a variable, it's a best practice to enclose it in double quotes to prevent issues with spaces.
#!/bin/bash
NAME="Ahmed"
echo "Welcome, $NAME"
Command Substitution
Sometimes, you want to store the output of a command in a variable. This is called command
substitution. The modern, recommended syntax is $(command).
#!/bin/bash
Your script can accept arguments from the command line, just like regular commands. These are called
positional parameters.
#!/bin/bash
17
Department of Software Engineering
Execution:
The shell provides several special variables that are very useful.
Variable Description
$# The number of arguments passed to the script.
$? The exit status of the most recently executed command (0 for success, non-zero for failure).
$* All arguments as a single string.
$@ All arguments as separate, individually quoted strings (generally safer to use).
Quoting is one of the most important concepts in shell scripting. It controls how the shell interprets
special characters.
18
Department of Software Engineering
EXERCISE
• Create a script named [Link] that prints a simple welcome message and then on a new
line, displays the output of the pwd command.
2. Using Variables
• Create a script named dir_report.sh that takes a directory path as its only argument.
• The script should perform the following actions:
1. Print a message: "--- Generating report for directory: [directory path] ---"
2. Create a variable that stores the number of items in that directory. (Hint: use command
substitution with a pipeline like ls [directory path] | wc -l).
3. Print another message: "The directory contains [number] items."
19
Department of Software Engineering
Remarks and
Signature
20
Department of Software Engineering
LAB SESSION 05
So far, our scripts have been simple sequences of commands. To write truly powerful scripts, we need
to add logic: making decisions, repeating actions, and organizing code.
To make a decision, we first need to evaluate a condition. In bash, the modern and preferred way to do
this is with the double-bracket [[ ... ]] construct. This is a safer and more versatile version of the older
single-bracket [ ... ] (or test) command.
[[ ... ]] evaluates the expression inside it and returns an exit status of 0 (success/true) or non-zero
(failure/false).
Common Operators for [[ ... ]] There are three main categories of operators you'll use.
Operator Meaning
-eq is equal to
-ne is not equal to
-gt is greater than
-ge is greater than or equal to
-lt is less than
-le is less than or equal to
Operator Meaning
== is identical to
!= is not identical to
-z string has zero length (is empty)
-n string is not empty
21
Department of Software Engineering
Operator Meaning
-e file exists
-f is a regular file
-d is a directory
-r is readable
-w is writable
-x is executable
Syntax:
Example:
#!/bin/bash
if [[ "$1" -gt 100 ]]; then
echo "That's a big number!"
elif [[ "$1" -eq 42 ]]; then
echo "That's the answer to everything."
else
echo "That's a small number."
fi
The case Statement
The case statement is a cleaner way to handle multiple choices when you're comparing a single variable
against several possible values.
Syntax:
case "$variable" in
pattern1)
# code for pattern1
;;
pattern2)
# code for pattern2
;;
*)
# default code if no patterns match
22
Department of Software Engineering
;;
esac
Example:
#!/bin/bash
case "$1" in
start)
echo "Starting the service..."
;;
stop)
echo "Stopping the service..."
;;
*)
echo "Usage: $0 {start|stop}"
;;
esac
2. Performing Arithmetic
A critical missing piece for many scripts is the ability to do math. The modern way to perform arithmetic
in bash is with the $((...)) construct.
x=5
y=10
result=$((x + y))
echo "The sum of $x and $y is $result" # Prints 15
23
Department of Software Engineering
#!/bin/bash
counter=1
while [[ $counter -le 5 ]]; do
echo "Iteration $counter"
counter=$((counter + 1)) # Increment the counter
done
The until loop is the opposite of while: it runs as long as its condition is false.
#!/bin/bash
counter=1
until [[ $counter -gt 5 ]]; do
echo "Iteration $counter"
counter=$((counter + 1))
done
Functions let you group a block of code under a single name, which you can then call whenever you
need it. This makes your scripts cleaner, easier to read, and reusable.
Syntax:
function_name() {
# code to be executed
# Arguments are passed just like to scripts: $1, $2, etc.
}
Example:
#!/bin/bash
24
Department of Software Engineering
EXERCISE
1. Number Checker
• Write a script named num_check.sh that takes one number as an argument.
• The script should use an if...elif...else statement to print whether the number is positive,
negative, or zero.
2. File Inspector
• Write a script named file_info.sh that takes one argument (a name of something in the current
directory).
• The script should use if and file operators (-f, -d) to check and print whether the argument is a
regular file, a directory, or does not exist.
3. Simple Animal Facts
• Write a script named [Link] that accepts one argument: "cat", "dog", or "bird".
• Use a case statement to print a simple fact for the given animal. Include a default case for any
other input.
4. Countdown Timer
• Write a script named [Link] that uses a C-style for loop to count down from 10 to 1,
printing each number. After the loop, it should print "Liftoff!".
5. Summation Calculator
• Write a script named [Link] that takes one number as an argument.
• Using a while loop and the $((...)) syntax for arithmetic, calculate and print the sum of all
integers from 1 up to that number. (e.g., if the input is 5, the output should be 15, which is
1+2+3+4+5).
6. Greeting Function
• Write a script named [Link] that defines a function called say_hello.
• The function should take one argument (a name) and print "Welcome to the team, [name]!".
• Call the function three times from within the script, with three different names.
25
Department of Software Engineering
LAB SESSION 06
Practice Python Scripting
THEORY
Python is a high-level, interpreted programming language known for its emphasis on code readability
and simplicity. Created by Guido van Rossum and first released in 1991, Python's design philosophy
allows programmers to express concepts in fewer lines of code.
It's a powerful, general-purpose language used for web development, data science, machine learning,
system automation, and more. It features a clean syntax and dynamic typing, which means you don't
need to declare a variable's type before using it. This makes Python very approachable for beginners. It
also has a massive standard library and an active community that contributes to a rich ecosystem of
third-party packages.
#!/usr/bin/env python3
print("My first Python program")
That’s the entire program. Save it to a file called my_program.py. You can run it in two common ways
on a UNIX or Linux system.
1. Make the file executable using the chmod command and run it directly:
$ chmod +x my_program.py
$ ./my_program.py
$ python3 my_program.py
Comments
The # character indicates that the rest of the line is a comment. The interpreter simply ignores it.
Python is case-sensitive, so world and World are two different variables. A variable name must start
with a letter or an underscore, followed by letters, numbers, or underscores.
The type of a variable is determined dynamically by the value assigned to it. The primary built-in data
types you'll use are:
26
Department of Software Engineering
Examples:
# 2. List assignments
# The index of the first element is 0.
item_price_list = [5, 8, 24]
item_name_list = ["Apple", "Banana", "Mushroom"]
# 3. Dictionary assignments
# Access elements using their key inside square brackets [].
item_catalog = {"Apple": 5, "Banana": 8, "Mushroom": 24}
item_name = "Banana"
print(f"The price of one {item_name} is {item_catalog[item_name]} gold coins.")
Comparison Operators
Python uses an intuitive set of operators for comparing values within conditional statements.
27
Department of Software Engineering
Arithmetic Operators
Important Note: Python does not have the auto-increment (++) or auto-decrement (--) operators.
Instead, you use augmented assignment operators like x += 1 and x -= 1.
Conditional Statements
Python uses indentation (spaces or tabs) to define code blocks. A colon (:) is used to start a block.
#!/usr/bin/env python3
name = "master"
if name == "master":
print("Hello master")
elif name == "teacher":
print("Hello teacher")
else:
print("Who are you?")
Looping
for loop
28
Department of Software Engineering
while loop
x=0
while x < 10:
print(x)
x += 1 # Remember to increment the counter!
You can alter the flow of a loop using break (to exit the loop entirely) and continue (to skip to the next
iteration).
print(f"Color: {color}")
if color == 'pink':
break # Exit the loop after 'pink'
print("Exited loop!")
29
Department of Software Engineering
EXERCISE
1. Family Shoe Data: A list family holds family member names. A dictionary shoe_color contains the
favorite shoe color per person, and a dictionary shoe_size contains the shoe size. Print the favorite shoe
color and shoe size for each family member. For shoe sizes 10 and above, add the word 'large' to the
output line. (Expected Output Format: "Homer wears large brown shoes size 12")
2. Filter Even Numbers: Loop through and print out all even numbers from a given list of numbers. Do
not print any numbers that come after 237 in the list.
3. Sequential Arithmetic: Follow these instructions and print the result after each step:
1. Assign a variable a to a starting value of 5.
2. Add 6 to the result.
3. Multiply the result by 2.
4. Increment the result by 1.
5. Subtract 9 from the result.
6. Divide the final result by 7.
4. Disk Space Monitor: Write a Python script that monitors disk space on your system and prints a
warning if the usage is above 90%.
Hints:
• You'll need to install a third-party library called psutil. Open your terminal and run: pip install
psutil
• Use the following code snippet as a starting point:
import psutil
30
Department of Software Engineering
Remarks and
Signature
31
Department of Software Engineering
LAB SESSION 07
Demonstrate multi-threading using POSIX Thread Library
(pthread)
THEORY
The POSIX thread (Pthread) library is a standardized C/C++ API for creating and managing threads. A
thread is a concurrent execution flow within the same process. All threads created within a single
process share the same memory space, which makes them lightweight and efficient.
The primary purpose of using Pthreads is to improve application performance. This is achieved in two
main ways:
• Parallelism on Multi-Core Systems: On machines with multiple CPUs or cores, different
threads can be scheduled to run on different cores simultaneously, leading to significant speed
gains.
• Concurrency on Single-Core Systems: Even on a single core, threads can improve
performance. If one thread is blocked (e.g., waiting for a file to be read from a slow disk),
another thread can continue executing. This makes the program more responsive and efficient
by overlapping computation with I/O latency.
32
Department of Software Engineering
CRITICAL POINT: Preventing Premature Process Termination If your main() function finishes
and exits before the threads it created are done, the entire process will terminate, killing all active
threads.
To prevent this, the main thread must wait for the other threads to complete. This is done using
pthread_join() or by having main() call pthread_exit() as its final action, which keeps the process alive
until all threads have finished.
c) Joining Threads
Joining is the standard way for a thread to wait for another thread to finish.
• int pthread_join(pthread_t thread, void **retval);
When you call pthread_join(), the calling thread (e.g., main()) will block and wait until the specified
thread terminates. This is essential for two reasons:
1. It ensures the main thread doesn't exit prematurely.
2. It allows the main thread to safely collect results from the completed thread.
Mutex Functions
33
Department of Software Engineering
• Initialization:
o Static: pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
o Dynamic: pthread_mutex_init(&my_mutex, NULL);
• Locking / Unlocking:
o pthread_mutex_lock(&my_mutex); (Blocks if locked)
o pthread_mutex_trylock(&my_mutex); (Returns immediately if locked)
o pthread_mutex_unlock(&my_mutex);
• Destruction:
o pthread_mutex_destroy(&my_mutex);
Here is a simple serial program that computes the dot product of two vectors. The multi-threaded
version can be dotprod_mutex.c
/**************************************************************
****************
* FILE: dotprod_serial.c
* DESCRIPTION: A simple serial program to compute a vector dot
product.
***************************************************************
***************/
#include <stdio.h>
#include <stdlib.h>
start = 0;
end = [Link];
x = dotstr.a;
y = dotstr.b;
34
Department of Software Engineering
[Link] = len;
dotstr.a = a;
dotstr.b = b;
[Link] = 0;
return 0;
}
35
Department of Software Engineering
EXERCISE
1) Write a C program that creates five threads using the pthread_create() routine. Each thread should
perform the following simple tasks:
• Print a message, such as "Hello from Thread #X!", where X is its identifier.
• Terminate cleanly using pthread_exit().
The main() function should wait for all five threads to complete their execution before it prints a final
"All threads are done." message and exits.
2) Write a C program that creates multiple threads and passes a unique integer ID to each one.
Requirements:
• The main() function should create four threads.
• For each thread, you must pass it a unique integer (e.g., Thread 0 gets 0, Thread 1 gets 1,
etc.).
• Important: You must ensure that each thread receives the correct, unique integer. To do this
safely, create a separate, persistent data structure (like a struct) for each thread's arguments.
Simply passing the address of a single loop variable will cause a race condition and is
incorrect.
• Each thread will receive the pointer to its data structure, extract the integer, print a message
like "Thread received ID: X", and then exit.
• The main() function must wait for all threads to complete using pthread_join().
3) Write a C program that demonstrates how to parallelize the summation of an array using four threads.
Requirements:
1. In main(), create a large integer array (e.g., 1,000,000 elements) and initialize all
its elements to 1.
2. Create a single global variable, long long global_sum = 0;, which will hold the final
sum.
3. Initialize a global mutex variable (pthread_mutex_t) to protect access to
global_sum.
4. Create four worker threads. Each thread will be responsible for summing a specific
quarter of the array. For example:
o Thread 0 sums indices 0 to 249,999.
o Thread 1 sums indices 250,000 to 499,999.
o ...and so on.
5. Each thread should calculate its partial sum in a local variable. Once its calculation
is complete, it must lock the mutex, add its partial sum to the global_sum, and then
immediately unlock the mutex.
6. The main() function must wait for all four threads to complete using pthread_join().
7. After all threads are joined, main() should print the final global_sum. The correct
result should be the total number of elements in the array.
4) Compile and execute the provided dotprod_serial.c program. Observe its behavior and confirm that
it produces the correct output (Sum = 100000.000000). Attach a screenshot of this output.
36
Department of Software Engineering
LAB SESSION 08
Open Ended Lab
OBJECTIVE
The goal of this lab is to apply your knowledge of POSIX threads to a practical problem. You will
convert a sequential program that computes the dot product of two vectors into a multi-threaded version.
This will require you to manage thread creation, partition work, and use a mutex to protect a shared
global variable from race conditions, ensuring a correct final result.
EVALUATION METHOD
Students will be evaluated through open-ended problem-solving tasks using the OEL rubrics (5 marks)
with criteria levels 0 and 1.
/**************************************************************************
****
* FILE: dotprod_serial.c
* DESCRIPTION: A simple serial program to compute a vector dot product.
***************************************************************************
***/
#include <stdio.h>
#include <stdlib.h>
#include <time.h> // For measuring time
#define VECLEN 1000000 // Using a larger vector for more meaningful timing
DOTDATA dotstr;
x = dotstr.a;
y = dotstr.b;
37
Department of Software Engineering
mysum = 0;
for (i = 0; i < [Link]; i++) {
mysum += (x[i] * y[i]);
}
[Link] = mysum;
}
// The main program initializes data, calls the function, and prints the result.
int main (int argc, char *argv[]) {
int i, len;
double *a, *b;
[Link] = len;
dotstr.a = a;
dotstr.b = b;
[Link] = 0;
free(a);
free(b);
return 0;
}
38
Department of Software Engineering
TASKS
DELIVERABLES
Submit the following for evaluation:
1. The complete source code for your dotprod_mutex.c program.
2. Screenshots showing the output of:
o The original dotprod_serial.c.
o Your correct dotprod_mutex.c.
o The output of your multi-threaded version with the mutex disabled, showing an
incorrect result.
3. A brief written report answering the discussion questions from Task 1 and Task 3.
39
Department of Software Engineering
Weighted CLO
Score
Remarks and
Signature
40
Department of Software Engineering
LAB SESSION 09
Design and Simulation of CPU Scheduling Algorithms
THEORY
In a multi-programming operating system, the CPU must be scheduled to switch between multiple
ready-to-run processes to maximize utilization and provide fair service. A CPU scheduling algorithm
determines which process in the ready queue is allocated to the CPU.
• First-Come, First-Served (FCFS): The simplest algorithm where processes are executed in
the order they arrive. It's a non-preemptive algorithm. While fair, it can lead to long average
waiting times if a short process gets stuck behind a long one.
• Priority Scheduling: Each process is assigned a priority, and the CPU is allocated to the
process with the highest priority. A major challenge is starvation, where low-priority processes
may never execute.
• Round Robin (RR): A preemptive algorithm where each process is given a small unit of CPU
time called a time quantum. When the quantum expires, the process is moved to the end of the
ready queue. This ensures that all processes get a fair share of the CPU.
• Shortest-Job-First (SJF): This algorithm selects the process with the smallest burst time to
run next. It is provably optimal for minimizing the average waiting time but requires knowing
the burst time in advance.
Python is an excellent language for simulating OS concepts due to its readability and powerful data
structures. To properly model a process, we will use a Python class. This is an effective design choice
as it allows us to encapsulate all the data related to a process in one neat package.
class Process:
def __init__(self, pid, arrival_time, burst_time, priority=0):
[Link] = pid
self.arrival_time = arrival_time
self.burst_time = burst_time
[Link] = priority
# These will be calculated by the simulator
self.completion_time = 0
self.turnaround_time = 0
self.waiting_time = 0
self.start_time = 0
41
Department of Software Engineering
This runnable script implements the FCFS algorithm, including the Process class, calculation/display
functions, the scheduler, and a main function to run the simulation.
import copy
class Process:
"""A class to represent a process with its scheduling attributes."""
def __init__(self, pid, arrival_time, burst_time, priority=0):
[Link] = pid
self.arrival_time = arrival_time
self.burst_time = burst_time
[Link] = priority
# These will be calculated by the simulator
self.completion_time = 0
self.turnaround_time = 0
self.waiting_time = 0
self.start_time = 0
def calculate_metrics(processes):
"""Calculates turnaround and waiting time for a list of completed processes."""
total_turnaround_time = 0
total_waiting_time = 0
for p in processes:
p.turnaround_time = p.completion_time - p.arrival_time
p.waiting_time = p.turnaround_time - p.burst_time
total_turnaround_time += p.turnaround_time
total_waiting_time += p.waiting_time
def fcfs_schedule(processes):
42
Department of Software Engineering
"""
First-Come, First-Served scheduling algorithm.
Sorts by arrival time and executes in that order.
"""
# Sort processes based on arrival time
processes_sorted = sorted(processes, key=lambda p: p.arrival_time)
current_time = 0
for p in processes_sorted:
if current_time < p.arrival_time:
current_time = p.arrival_time
p.start_time = current_time
p.completion_time = current_time + p.burst_time
current_time = p.completion_time
if __name__ == "__main__":
# Define a sample workload: list of Process objects
# (PID, Arrival Time, Burst Time, Priority)
workload = [
Process(1, 0, 8, 3),
Process(2, 1, 4, 1),
Process(3, 2, 9, 4),
Process(4, 3, 5, 2),
]
43
Department of Software Engineering
EXERCISE
1. Implement the Priority Scheduling Component
Design and implement a non-preemptive priority scheduling algorithm.
• Logic:
1. The algorithm must select the process with the highest priority from the ready
queue. Assume a lower number indicates a higher priority.
2. You must handle processes that arrive while another is running. The active ready
queue should always be sorted by priority.
3. If two processes have the same priority, the one that arrived earlier (FCFS) should
be chosen as the tie-breaker.
• Deliverable:
o A Python function priority_schedule(processes) that implements this logic and
uses the provided print_results function to display the outcome.
44
Department of Software Engineering
LAB SESSION 10
Design and Simulation of Page Replacement Algorithms
THEORY
In an operating system that uses paging for virtual memory, the physical memory is divided into
fixed-size blocks called frames. When the system needs to bring a new page into memory and all
frames are already occupied, a page replacement algorithm must decide which existing page to evict
(swap out) to make room.
The efficiency of a page replacement algorithm is measured by its ability to minimize the number of
page faults. A page fault occurs when the CPU references a page that is not currently in physical
memory, forcing the OS to load it from the disk, which is a very slow operation.
• First-In, First-Out (FIFO): This is the simplest algorithm. The OS maintains a queue of all
pages currently in memory. When a replacement is needed, the page at the front of the queue
(the one that has been in memory the longest) is evicted. While simple to implement, FIFO
can perform poorly as it may evict a frequently used page.
• Least Recently Used (LRU): This algorithm is based on the principle of locality, which
suggests that pages used recently are likely to be used again soon. When a replacement is
needed, LRU evicts the page that has not been accessed for the longest period. It generally
performs much better than FIFO but is more complex to implement as the OS must track
when each page was last used.
Python's clear syntax and data structures make it ideal for simulating page replacement algorithms.
Our simulator will process a page reference string (a sequence of page numbers being requested) and
track the state of memory frames over time.
Here is a complete, runnable script that simulates the FIFO algorithm. It includes a main function to
run the simulation with a sample workload. Use this as a foundation for your own work.
Args:
page_reference_string (list): The sequence of page numbers
requested.
num_frames (int): The number of available frames in memory.
Returns:
int: The total number of page faults.
"""
print("--- Starting FIFO Simulation ---")
print(f"Page Reference String: {page_reference_string}")
45
Department of Software Engineering
if __name__ == "__main__":
# Define a sample workload
reference_string = [7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1,
7, 0, 1]
frame_count = 3
46
Department of Software Engineering
EXERCISE
1. Implement the LRU Scheduling Component
Design and implement the Least Recently Used (LRU) page replacement algorithm.
• Logic:
1. When a page fault occurs and memory is full, you must identify and evict the
page that has been unused for the longest time.
2. To achieve this, you need a mechanism to track the "recency" of each page's use.
Every time a page is referenced (either on a hit or a fault), it becomes the most
recently used.
• Hint: A simple way to track recency is to maintain a list or queue. When a page is
accessed, you can move it to the "most recent" end of the list. The page at the "least
recent" end is the one to evict.
• Deliverable:
o A Python function lru_page_replacement(page_reference_string,
num_frames) that implements this logic and prints the step-by-step state of
memory and the final page fault count, just like the FIFO example.
47
Department of Software Engineering
LAB SESSION 11
Design and Simulation of Memory Allocation Algorithms
THEORY
Memory management is a core function of an operating system. The memory manager must
dynamically allocate blocks of memory to processes when they are requested and free those blocks
when the processes terminate. For contiguous allocation, the manager maintains a list of free memory
blocks (holes) and searches this list to fulfill a process's request.
• First Fit: The memory manager scans the list of holes from the beginning and allocates the
first hole that is large enough to accommodate the process. It's fast but can leave behind
unusable small fragments.
• Best Fit: The manager searches the entire list of holes to find the smallest hole that is large
enough for the process. This strategy aims to minimize the size of the leftover hole (internal
fragmentation).
• Worst Fit: The manager searches the entire list and allocates the largest available hole. The
idea is that the leftover fragment will be large enough to be useful for future processes.
To model this system, we can use a class-based approach in Python, which is a clean and effective
design. We'll represent each memory block and process as an object.
• MemoryBlock: A class to represent a block of memory. It will have attributes for its size,
whether it's free, and which process (if any) is currently using it.
• Process: A simple class to represent a process with its required memory size.
Here is a complete, runnable Python script that simulates the First Fit algorithm. Use this code as the
foundation for your own implementations.
import copy
# --- Component Design: Classes for Memory and Processes ---
class MemoryBlock:
def __init__(self, size, is_free=True, process_id=None):
[Link] = size
self.is_free = is_free
self.process_id = process_id
class Process:
def __init__(self, pid, size):
[Link] = pid
[Link] = size
48
Department of Software Engineering
allocated = True
break # Move to the next process
if not allocated:
print(f"Process P{[Link]} (size {[Link]}K) could not be allocated.")
print_memory_layout(memory)
return memory
49
Department of Software Engineering
EXERCISE
1. Implement the Best Fit Component
Design and implement the Best Fit memory allocation algorithm.
• Logic: For each process, you must search the entire list of free memory blocks. You need
to find the block that is large enough for the process but also results in the smallest possible
leftover fragment (i.e., [Link] - [Link] should be minimized).
• Deliverable: A Python function best_fit(initial_holes, processes) that implements this logic
and prints the final memory layout.
50
Department of Software Engineering
LAB SESSION 12
Solving the Producer-Consumer Problem with Semaphores
THEORY
This is a classic synchronization problem where two types of processes (or threads), Producers and
Consumers, share a common, fixed-size buffer.
• The Producer's job is to generate data (items) and put them into the buffer.
• The Consumer's job is to remove items from the buffer and consume them.
To solve this, we use three synchronization primitives. In Python's threading module, these are
Semaphore objects and a Lock (which acts as a mutex).
51
Department of Software Engineering
You are provided with the following Python script skeleton. Your task is to complete the producer and
consumer functions by adding the correct semaphore logic.
# producer_consumer.py
import threading
import time
import random
from collections import deque
# Add to buffer
[Link](item)
print(f"Producer {producer_id} produced {item}. Buffer: {list(buffer)}")
item_counter += 1
52
Department of Software Engineering
global buffer
while True:
# --- EXERCISE: Add consumer synchronization logic here ---
# Wait for all threads to complete (they won't in this infinite loop example)
for thread in threads:
[Link]()
53
Department of Software Engineering
EXERCISE
1. Implement the Synchronized Logic
Complete the producer and consumer functions in the producer_consumer.py script by adding the
correct calls to acquire() and release() on the empty, full, and mutex primitives.
• Producer Logic:
1. A producer must first wait for an empty slot to be available.
2. Then, it must acquire the lock to ensure exclusive access to the buffer.
3. After adding the item, it must release the lock.
4. Finally, it must signal that a new item is available for consumers.
• Consumer Logic:
1. A consumer must first wait for a filled slot to be available.
2. Then, it must acquire the lock for exclusive access.
3. After removing the item, it must release the lock.
4. Finally, it must signal that a new empty slot is available for producers.
Deliverable:
• The complete, working producer_consumer.py script. Attach a printout of your code and a
screenshot of the output showing producers and consumers running concurrently without
error.
1. Role of mutex: What would happen if you removed the [Link]() and
[Link]() calls but kept the empty and full semaphores? Describe a specific race
condition that could occur.
2. Role of empty: What problem does the empty semaphore solve? What would happen if it
were removed?
3. Role of full: What problem does the full semaphore solve? What would happen if it were
removed?
54
Department of Software Engineering
LAB SESSION 13
Complex Engineering Activity
CLO Covered
CLO 3: Apply understanding of design and development principles in the construction of Operating
Systems Components. (C3-PLO3)
CPA1: Depth of analysis required: Have no obvious solution and require abstract thinking, originality
in analysis to formulate suitable models.
CPA2: Level of interaction: Require resolution of significant problems arising from interactions
between wide-ranging or conflicting technical, engineering or other issues
CPA3: Familiarity: Can extend beyond previous experiences by applying principles-based approaches
Problem Statement
The goal of this activity is to analyze and modify the xv6 operating system's process scheduler to
understand its direct impact on the performance of I/O-bound and CPU-bound applications. Students
will act as OS performance engineers. They are tasked with implementing a new scheduling policy in
xv6 and then developing custom user-level applications to benchmark and analyze how this change in
OS design affects application performance. This requires a deep understanding of the relationship
between kernel-level scheduling decisions and user-level application behavior.
Tasks:
• Environment Setup: Configure the xv6 development environment using the QEMU emulator
and the RISC-V toolchain.
• Code Familiarization: Study the existing xv6 source code for the scheduler (kernel/proc.c),
system call interface, and process management.
• Baseline Compilation: Compile and run the unmodified xv6 to ensure the environment is
working correctly and establish a performance baseline.
o If a process yields the CPU (e.g., for I/O), it remains at its current priority level.
55
Department of Software Engineering
• System Call for Performance Analysis: Add a new system call, getprocinfo(int pid), that
returns performance metrics for a specific process, such as total CPU ticks consumed and the
number of times it has been scheduled.
• Develop an I/O-Bound Application: Write a user-level program that frequently performs I/O
operations (e.g., repeatedly creating and writing small amounts of data to a file) to simulate an
I/O-bound workload.
• Functional Testing: Verify that the new MLFQ scheduler correctly manages processes and
that the getprocinfo system call returns accurate data.
• Benchmark Execution: Run both the CPU-bound and I/O-bound applications simultaneously
on both the original round-robin scheduler and your new MLFQ scheduler.
• Performance Report: Write a technical report analyzing your findings. The report must:
o Compare the performance (e.g., completion time, CPU ticks) of both applications under
each scheduler.
o Explain why the MLFQ scheduler impacts the performance of CPU-bound and I/O-
bound tasks differently.
o Conclude with a clear analysis of how this specific OS design choice (the scheduler)
directly influences application design and performance.
1. All kernel modifications must be performed within the xv6 RISC-V source code.
2. Students must use a controlled QEMU virtual environment for all testing to ensure consistent
results.
3. The analysis must be supported by quantitative data (benchmarks) collected from your test
applications.
Submission Details
1. Submit complete working project as zip file containing all the files, code, data etc.
56
Department of Software Engineering
57
Department of Software Engineering
LAB SESSION 14
Solving the Dining Philosophers Problem
THEORY
The problem describes five philosophers sitting around a circular table. Between each pair of
philosophers is a single fork. A philosopher's life consists of alternating between two states: thinking
and eating.
To eat, a philosopher needs to pick up both the fork to their left and the fork to their right. The challenge
is to design a protocol that allows them to do this without creating a deadlock (a situation where
everyone is stuck waiting for a resource held by someone else) or starvation (where a philosopher is
perpetually denied a chance to eat).
A naive solution, where each philosopher picks up their left fork and then waits for their right fork,
leads to a classic deadlock scenario: if all five pick up their left fork simultaneously, no one can pick
up their right fork, and they all wait forever.
The solution we will implement is a monitor-like approach that ensures a philosopher can only pick up
their forks if neither of their neighbors is currently eating.
1. A states array: This shared array tracks the current state of each philosopher (THINKING,
HUNGRY, or EATING).
2. A Lock: A single mutex lock is used to ensure that only one philosopher can change the states
array or their own state at any given moment. This prevents race conditions.
3. A Condition variable: This powerful primitive is tied to the lock. It allows a philosopher who
cannot eat (because a neighbor is eating) to wait() patiently. When a neighbor finishes eating,
they can notify() the waiting philosopher that it's now possible to check for forks again.
You are provided with the following Python script skeleton. Your task is to complete the take_forks
and put_forks methods by implementing the described logic.
# dining_philosophers.py
import threading
import time
import random
58
Department of Software Engineering
THINKING = 'THINKING'
HUNGRY = 'HUNGRY'
EATING = 'EATING'
def run(self):
while True:
[Link]()
self.take_forks()
[Link]()
self.put_forks()
def think(self):
print(f"Philosopher {[Link]} is thinking.")
[Link]([Link](1, 3))
def eat(self):
print(f"Philosopher {[Link]} is EATING.")
[Link]([Link](1, 4))
states[i] = EATING
# Notify the waiting philosopher that they can now proceed.
condition.notify_all()
return True
return False
def take_forks(self):
print(f"Philosopher {[Link]} is hungry.")
# --- EXERCISE: Implement the logic for taking forks ---
# 1. Acquire the condition lock.
# 2. Set your state to HUNGRY.
# 3. Try to acquire forks by calling _test().
# 4. If you can't eat, wait() on the condition variable.
# 5. Release the lock.
59
Department of Software Engineering
def put_forks(self):
print(f"Philosopher {[Link]} is putting forks down.")
# --- EXERCISE: Implement the logic for putting forks down ---
# 1. Acquire the condition lock.
# 2. Set your state back to THINKING.
# 3. Notify your neighbors that you are done, so they can check if they can eat now.
# (Hint: call _test() for both the left and right neighbor).
# 4. Release the lock.
pass # Remove this line after implementing
for p in philosophers:
[Link]()
for p in philosophers:
[Link]()
60
Department of Software Engineering
EXERCISE
1. Implement the Deadlock-Free Solution
Complete the take_forks and put_forks methods in the dining_philosophers.py script. You must use
the shared condition variable to acquire the lock and to wait() and notify() other threads.
• take_forks Logic:
1. Acquire the lock using with condition:.
2. Set your own state to HUNGRY.
3. Attempt to start eating by calling self._test([Link]).
4. If _test returns False (meaning you couldn't start eating), you must wait() on the
condition variable until another philosopher notifies you.
• put_forks Logic:
1. Acquire the lock using with condition:.
2. Set your own state back to THINKING.
3. Check if your neighbors can now eat. Call self._test(...) for your left neighbor and
your right neighbor. This "wakes up" a neighbor if they were waiting.
Deliverable:
• The complete, working dining_philosophers.py script. Attach a printout of your code and
a screenshot of the output showing philosophers successfully alternating between thinking
and eating without deadlocking.
61
Department of Software Engineering
LAB SESSION 15
Final Exam
OBJECTIVE
The objective of the Final Lab is to evaluate students on the overall knowledge and skills gained
throughout all lab sessions of the course. This session consolidates learning by requiring students to
demonstrate mastery of Operating Systems through integrated tasks that reflect real-world problem-
solving.
EVALUATION METHOD
Students will be assessed using the Final Lab Rubrics (5 marks), consisting of five criteria, each marked
on the basis of 0 or 1.
62
Department of Software Engineering
Weighted CLO
Score
Remarks and
Signature
63