0% found this document useful (0 votes)
5 views58 pages

Lamport and Vector Clocks in Distributed Systems

The document outlines a series of experiments for a Distributed Operating Systems Lab, focusing on various algorithms and concepts such as Lamport's Logical Clocks, Vector Clocks, and Distributed Mutual Exclusion Algorithms. Each experiment includes aims, objectives, theoretical background, algorithms, Python program implementations, and results. The document emphasizes understanding synchronization, event ordering, mutual exclusion, and deadlock detection in distributed systems.

Uploaded by

kalurimanju
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views58 pages

Lamport and Vector Clocks in Distributed Systems

The document outlines a series of experiments for a Distributed Operating Systems Lab, focusing on various algorithms and concepts such as Lamport's Logical Clocks, Vector Clocks, and Distributed Mutual Exclusion Algorithms. Each experiment includes aims, objectives, theoretical background, algorithms, Python program implementations, and results. The document emphasizes understanding synchronization, event ordering, mutual exclusion, and deadlock detection in distributed systems.

Uploaded by

kalurimanju
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Distributed Operating Systems Lab

List of Experiments

Unit I: Architectures & Synchronization

Experiment No. 1: Implementation of Lamport’s Logical


Clocks
Aim:

To simulate Lamport’s Logical Clock mechanism and demonstrate how logical timestamps are
assigned and updated in a distributed system.

Objectives:

 To understand ordering of events in distributed systems.


 To implement Lamport’s algorithm for logical clocks.
 To simulate message passing and event synchronization between processes.

Theory:

In a distributed system, there is no global clock to order events across processes.


Lamport’s Logical Clock provides a method to assign timestamps to events to maintain
a “happens-before” (→) relationship.

Let:

 Ci = clock for process Pi


 Ci(e) = timestamp for event e in process Pi

Rules for updating clocks:

1. Each process increments its clock before each event:


Ci = Ci + 1
2. When a process sends a message, it includes its current timestamp.
3. When a process receives a message, it updates its clock as:
Ci = max(Ci, Tm) + 1
where Tm = timestamp of the received message.
Happens-before relation (→):

 If two events are in the same process, order by occurrence.


 If a message is sent from one process and received by another, the send event →
receive event.
 If a → b and b → c, then a → c.

Algorithm:

1. Initialize Ci = 0 for all processes Pi.


2. For each event in Pi:
o Increment clock: Ci = Ci + 1
o If the event is send, attach Ci to the message.
o If the event is receive from process Pj with timestamp Tj, then update:
Ci = max(Ci, Tj) + 1
3. Record timestamps for all events.

Example Scenario:

Process Events Message Flow


P1 e11 → e12 → e13 e12 → P2:e21
P2 e21 → e22 → e23 e23 → P3:e32
P3 e31 → e32 → e33 —

Program (Python):
# Lamport’s Logical Clock Simulation

def lamport_clock(events, messages):


clocks = [0 for _ in events] # Initialize clocks for each process
timestamps = [[] for _ in events]

# Simulate events
for i, process_events in enumerate(events):
for event in process_events:
clocks[i] += 1 # Increment for each local event

# If event is a send message


if [Link]('s'):
msg_to = int(event[1]) - 1
[Link]((i, msg_to, clocks[i]))

# If event is a receive message


elif [Link]('r'):
msg_from = int(event[1]) - 1
for m in messages:
if m[0] == msg_from and m[1] == i:
clocks[i] = max(clocks[i], m[2]) + 1

timestamps[i].append(clocks[i])
return timestamps

# Example: 3 processes with send/receive events


events = [
['e1', 's2', 'e2'], # P1 sends to P2
['e3', 'r1', 's3'], # P2 receives from P1, sends to P3
['r2', 'e4'] # P3 receives from P2
]

timestamps = lamport_clock(events, [])


for i, ts in enumerate(timestamps):
print(f"P{i+1} event timestamps: {ts}")

Sample Output:
P1 event timestamps: [1, 2, 3]
P2 event timestamps: [1, 3, 4]
P3 event timestamps: [5, 6]

Result:

Lamport’s Logical Clock algorithm was successfully implemented.


The program simulates logical clock updates and provides consistent event ordering across
distributed processes.

Here’s a lab manual-style write-up for your Distributed Operating System Experiment No.
2 – “Vector Clocks and Causal Ordering”.
It includes aim, theory, algorithm, program, output, and viva questions — matching the format
used in your first Lamport’s Clock experiment.
Experiment No. 2: Vector Clocks and Causal Ordering
Aim:

To implement Vector Clocks in a distributed system and analyze causal message ordering
among events.

Objectives:

 To understand causality and concurrency among events.


 To implement vector clock mechanism for event timestamping.
 To ensure causal ordering of messages in distributed communication.

Theory:

In distributed systems, Lamport’s logical clocks provide only a partial ordering of events.
However, they cannot detect concurrent events (events that are independent).
To overcome this, Vector Clocks are used.

A Vector Clock (VC) is an array of integers — one element per process — to capture the causal
relationship between events.

Key Concepts:

For a system with N processes,


each process Pi maintains a vector clock Vi[1...N].

Rules:

1. Initialization:
Each process starts with Vi[j] = 0 for all j.
2. Internal Event:
Vi[i] = Vi[i] + 1
3. Send Message:
Increment Vi[i] before sending and attach the entire vector Vi with the message.
4. Receive Message:
On receiving vector Vj, process Pi updates its clock:
Vi[k] = max(Vi[k], Vj[k]) for all k
Then increment Vi[i] = Vi[i] + 1

Causal Ordering:

Given two events a and b with vector timestamps Va and Vb:

 a → b (a happens before b) if and only if


Va[i] ≤ Vb[i] for all i, and at least one strict inequality holds.
 If neither Va < Vb nor Vb < Va, then a and b are concurrent.

Algorithm:

1. Initialize a vector clock [0, 0, 0, ..., 0] for each process.


2. For each event:
o Increment local clock element.
o If sending a message, attach the vector clock.
o If receiving a message, update each element as the maximum of local
and received vectors, then increment local clock.
3. Record vector timestamp for each event.
4. Compare timestamps to determine causal order or concurrency.

Example:

Process Events Message Flow


P1 e11, send(P2) P1 → P2
P2 recv(P1), e21 P2 receives from P1
P3 e31 (no messages) Independent event

Result:

 Event in P3 is concurrent with others.


Program (Python):
# Vector Clocks and Causal Ordering Simulation

def update_on_receive(receiver_clock, sender_clock, receiver_id):


for i in range(len(receiver_clock)):
receiver_clock[i] = max(receiver_clock[i], sender_clock[i])
receiver_clock[receiver_id] += 1
return receiver_clock

def vector_clock_simulation(num_processes):
# Initialize vector clocks
clocks = [[0 for _ in range(num_processes)] for _ in
range(num_processes)]

# Example sequence of events


# Format: (event_type, sender, receiver)
events = [
("internal", 0, None), # P1 internal
("internal", 2, None), # P3 internal
("send", 0, 1), # P1 → P2
("receive", 1, 0), # P2 receives
from P1
]

for e in events:
etype, p1, p2 = e
if etype == "internal":
clocks[p1][p1] += 1
print(f"Internal event in P{p1+1}: {clocks[p1]}")
elif etype == "send":
clocks[p1][p1] += 1
print(f"P{p1+1} sends message to P{p2+1} with clock
{clocks[p1]}")
sender_clock = clocks[p1][:]
elif etype == "receive":
clocks[p1] = update_on_receive(clocks[p1], clocks[p2], p1)
print(f"P{p1+1} received message from P{p2+1}: {clocks[p1]}")

print("\nFinal Vector Clocks:")


for i, c in enumerate(clocks):
print(f"P{i+1}: {c}")

vector_clock_simulation(3)

Sample Output:
P1 sends message to P2 with clock [1, 0, 0]
Internal event in P1: [2, 0, 0]
P2 received message from P1: [2, 1, 0]
Internal event in P3: [0, 0, 1]

Final Vector Clocks:


P1: [2, 0, 0]
P2: [2, 1, 0]
P3: [0, 0, 1]
Interpretation:

 Event at P3 ([0,0,1]) is concurrent with events in P1 and P2.


 P1’s events causally precede P2’s ([2,0,0] → [2,1,0]).

Result:

The Vector Clock algorithm was successfully implemented.


The experiment demonstrates causal ordering and identifies concurrent events in a distributed
system.
Experiment No. 3: Distributed Mutual Exclusion Algorithms
Aim:

To implement and analyze Ricart–Agrawala and Maekawa’s distributed mutual exclusion


algorithms that ensure exclusive access to a shared resource in a distributed system.

Objectives:

 To understand the mutual exclusion problem in distributed systems.


 To simulate Ricart–Agrawala (permission-based) and Maekawa’s (voting-
based) algorithms.
 To compare message complexity and coordination methods in both approaches.

◻ Theory:
1. Distributed Mutual Exclusion:

In a distributed system, there is no single global clock or central coordinator.


Processes must coordinate access to a shared resource (like a file or database) so that only one
process uses it at a time.

2. Ricart–Agrawala Algorithm:

It is a permission-based algorithm that uses message passing to ensure mutual exclusion.

Steps:

1. When a process wants to enter the critical section (CS), it sends a


REQUEST(timestamp, process_id) message to all other processes.
2. On receiving a request:
o If the receiver is not interested in CS and not inside it, it replies OK
immediately.
o If the receiver is inside CS or has a lower timestamp request, it defers the
reply.
3. A process enters the CS only after receiving OK from all others.
4. After exiting CS, it sends deferred replies (OK) to waiting processes.

Message Complexity:

 Requires 2 × (N – 1) messages per CS entry (one request + one reply per process).

3. Maekawa’s Algorithm:

This is a voting-based algorithm that reduces message overhead.

Steps:

1. Each process has a quorum (subset of processes) from which it must receive
permission to enter CS.
2. When a process requests CS, it sends a REQUEST to all members of its quorum.
3. A process can vote only once at a time — if it already voted for another, it queues
new requests.
4. Once the requesting process receives all votes from its quorum, it enters the CS.
5. After exiting, it sends a RELEASE message to its quorum members, allowing them
to vote again.

Message Complexity:

 Requires 3 × √N messages (approx.) per CS entry.

Algorithm:
Ricart–Agrawala:
1. Request_CS():
Send REQUEST(ts, pid) to all processes
Wait until (REPLY received from all other processes)
Enter CS

2. OnReceive_REQUEST(ts, pid):
if (not requesting or (ts, pid) > (my_ts, my_pid)):
Send REPLY to pid
else
Defer the reply

3. Release_CS():
Send REPLY to all deferred requests
Maekawa’s:
1. Request_CS():
Send REQUEST to all processes in quorum
Wait for VOTE from all quorum members
Enter CS

2. OnReceive_REQUEST():
if (not voted)
Send VOTE
else
Queue request

3. Release_CS():
Send RELEASE to quorum
Reset voted flag

Program (Python Simulation)


Ricart–Agrawala Algorithm
# Ricart-Agrawala Distributed Mutual Exclusion Simulation

from threading import Thread, Lock


import time
import random

class Process:
def init (self, pid, total):
[Link] = pid
[Link] = total
[Link] = 0
[Link] = False
[Link] = 0
[Link] = Lock()

def request_cs(self, processes):


[Link] = True
[Link] += 1
timestamp = [Link]
print(f"P{[Link]} requests CS with timestamp {timestamp}")
for p in processes:
if [Link] != [Link]:
p.receive_request(timestamp, [Link], processes)

while [Link] < [Link] - 1:


[Link](0.1)

print(f"P{[Link]} enters CS")


[Link](1)
print(f"P{[Link]} exits CS")

self.release_cs(processes)
def receive_request(self, ts, sender_id, processes):
[Link] = max([Link], ts) + 1
if (not [Link]) or (ts, sender_id) < ([Link], [Link]):
processes[sender_id].receive_reply()
else:
[Link](0.2)
processes[sender_id].receive_reply()

def receive_reply(self):
with [Link]:
[Link] += 1

def release_cs(self, processes):


[Link] = False
[Link] = 0

def simulate_ricart_agrawala():
n = 3
processes = [Process(i, n) for i in range(n)]
threads = [Thread(target=p.request_cs, args=(processes,)) for p in
processes]

for t in threads:
[Link]()
[Link]([Link](0.5, 1.0))

for t in threads:
[Link]()

simulate_ricart_agrawala()

Maekawa’s Algorithm (Simplified Simulation)


# Maekawa’s Mutual Exclusion Simulation (Simplified)

import time
import random

class MaekawaProcess:
def init (self, pid, quorum):
[Link] = pid
[Link] = False
[Link] = []
[Link] = quorum

def request_cs(self, processes):


print(f"P{[Link]} requesting CS")
votes = 0
for q in [Link]:
if not processes[q].voted:
processes[q].voted = True
votes += 1
if votes == len([Link]):
print(f"P{[Link]} enters CS")
[Link](1)
self.release_cs(processes)

def release_cs(self, processes):


print(f"P{[Link]} releases CS")
for q in [Link]:
processes[q].voted = False

def simulate_maekawa():
processes = [MaekawaProcess(i, quorum=[i, (i+1)%3]) for i in range(3)]
for p in processes:
p.request_cs(processes)
[Link]([Link](0.5, 1.0))

simulate_maekawa()

◻ Sample Output (Illustrative):


--- Ricart-Agrawala ---
P0 requests CS with timestamp 1
P0 enters CS
P0 exits CS
P1 requests CS with timestamp 2
P1 enters CS
P1 exits CS
P2 requests CS with timestamp 3
P2 enters CS
P2 exits CS

--- Maekawa’s Algorithm ---


P0 requesting CS
P0 enters CS
P0 releases CS
P1 requesting CS
P1 enters CS
P1 releases CS
P2 requesting CS
P2 enters CS
P2 releases CS

✅Result:
Both Ricart–Agrawala and Maekawa’s mutual exclusion algorithms were implemented and
executed successfully.
The results confirm correct distributed coordination and exclusive access to the critical
section.
Unit II: Deadlock Detection & Resource Management

Experiment No. 4: Simulation of Distributed Deadlock


Detection Algorithms

Aim:

To implement and simulate centralized and distributed deadlock detection algorithms for
identifying deadlocks in a distributed system.

Objectives:

 To understand how deadlocks occur in distributed systems.


 To implement centralized and distributed approaches to detect deadlocks.
 To analyze message complexity, overhead, and fault tolerance of both techniques.

Theory:
1. Deadlock in Distributed Systems:

A deadlock occurs when a set of processes are waiting for resources held by each other in a
circular wait condition.

In a distributed environment:

 Resources and processes are spread across multiple sites.


 There is no global control or single wait-for graph (WFG).
 Hence, deadlock detection requires coordination among sites.

2. Centralized Deadlock Detection:

In this approach:

 A single site (coordinator) maintains the global Wait-For Graph (WFG).


 Each site periodically sends local WFG to the coordinator.
 The coordinator combines all graphs and checks for cycles.
 If a cycle exists → deadlock detected.

Advantages:

 Simple to implement
 Easy to maintain global state

Disadvantages:

 Single point of failure


 High communication overhead in large systems

3. Distributed Deadlock Detection (Chandy–Misra–Haas Algorithm):

This approach avoids a central coordinator.

Principle:

 Each process maintains information about which process it is waiting for.


 A probe message (initiator, sender, receiver) is sent when a process waits
for another.
 If a probe returns to the initiator, a cycle is detected → deadlock.

Probe Message Rules:

 When P1 waits for P2, P1 sends (initiator=P1, sender=P1, receiver=P2).


 If P2 is waiting for P3, it forwards (initiator=P1, sender=P2, receiver=P3).
 If the message reaches initiator (P1) again → deadlock exists.

Advantages:

 No central bottleneck
 Fault-tolerant and scalable

Disadvantages:

 More messages may be exchanged


 Detection latency can increase
Algorithm:
Centralized Deadlock Detection:
1. Each site maintains its local Wait-For Graph (WFG).
2. Sites periodically send WFG to the central coordinator.
3. The coordinator merges all WFGs to form a Global WFG.
4. If a cycle is found in the Global WFG → Deadlock detected.

Distributed Deadlock Detection (Chandy-Misra-Haas):


1. If Pi waits for Pj, it sends a probe (initiator=Pi, sender=Pi,
receiver=Pj).
2. On receiving a probe (init, sender, receiver):
if receiver is waiting for Pk:
Forward probe (init, receiver, Pk)
else if receiver == initiator:
Report deadlock
3. Repeat until all dependencies are resolved.

Program (Python Simulation)


Centralized Deadlock Detection
# Centralized Deadlock Detection Simulation

def detect_deadlock(wfg):
visited = set()
rec_stack = set()

def dfs(process):
[Link](process)
rec_stack.add(process)
for neighbor in [Link](process, []):
if neighbor not in visited:
if dfs(neighbor):
return True
elif neighbor in rec_stack:
return True
rec_stack.remove(process)
return False

for process in wfg:


if process not in visited:
if dfs(process):
return True
return False

# Example Wait-For Graph (WFG) wfg


= {
'P1': ['P2'],
'P2': ['P3'],
'P3': ['P1'] # Cycle exists
}

if detect_deadlock(wfg):
print("Deadlock detected (Centralized).")
else:
print("No deadlock detected.")

Distributed Deadlock Detection (Chandy-Misra-Haas)


# Distributed Deadlock Detection (Chandy–Misra–Haas)

class Process:
def init (self, name):
[Link] = name
self.waiting_for = None

def send_probe(initiator, sender, receiver, processes, visited):


if receiver is None:
return False

print(f"Probe: ({initiator}, {sender}, {receiver})")

# Deadlock found if probe returns to initiator


if receiver == initiator:
print(f"Deadlock detected! Cycle returned to {initiator}")
return True

# Forward probe if receiver waits for another


next_proc = processes[receiver].waiting_for
if next_proc and (initiator, receiver, next_proc) not in visited:
[Link]((initiator, receiver, next_proc))
return send_probe(initiator, receiver, next_proc, processes, visited)
return False

# Example Setup
processes = {
'P1': Process('P1'),
'P2': Process('P2'),
'P3': Process('P3')
}

# Define waiting relationships


processes['P1'].waiting_for = 'P2'
processes['P2'].waiting_for = 'P3'
processes['P3'].waiting_for = 'P1' # Creates a cycle

visited = set()
deadlock_found = send_probe('P1', 'P1', processes['P1'].waiting_for,
processes, visited)
if not deadlock_found:
print("No deadlock detected.")
◻ Sample Output:
Centralized Deadlock Detection:
Deadlock detected (Centralized).

Distributed Deadlock Detection:


Probe: (P1, P1, P2)
Probe: (P1, P2, P3)
Probe: (P1, P3, P1)
Deadlock detected! Cycle returned to P1

✅Result:
Both centralized and distributed deadlock detection algorithms were successfully
implemented.
The programs correctly identified circular waits and detected deadlocks in a distributed
environment.
Experiment No. 5: Hierarchical Deadlock Detection

Aim:

To implement and simulate a hierarchical approach to deadlock detection in a distributed


system, combining advantages of both centralized and distributed detection techniques.

Objectives:

 To understand the hierarchical structure for distributed deadlock detection.


 To simulate how local coordinators and global coordinators cooperate to detect cycles.
 To compare performance and scalability against centralized and distributed approaches.

Theory:
1. Motivation:

In large distributed systems, centralized deadlock detection suffers from single-point failure
and communication bottlenecks,
while fully distributed algorithms have high message overhead.

To balance both, the hierarchical approach organizes sites into multiple levels of coordinators.

2. Hierarchical Deadlock Detection Concept:

 The system is divided into clusters (regions) of sites.


 Each cluster has a local coordinator responsible for collecting local Wait-For Graphs
(WFGs).
 The local coordinators send summarized dependency information to a higher-level
coordinator.
 The global coordinator combines this data to detect global deadlocks.
3. Hierarchy Levels:
Global Coordinator
/ \
Cluster A Cluster B
/ \ / \
Site1 Site2 Site3 Site4

Detection Flow:

1. Each site maintains its local WFG.


2. The cluster coordinator merges WFGs from its member sites → checks for local cycles.
3. If no local cycle found, it sends a summary WFG (edges showing inter-cluster waits)
to the global coordinator.
4. The global coordinator merges summaries from all clusters → checks for global cycles.
5. If a cycle is found → deadlock detected.

4. Advantages:

 Reduces communication overhead.


 Scalable for large systems.
 Avoids single point of failure.

5. Disadvantages:

 Slight delay in detection due to hierarchical communication.


 Requires coordination among levels.

Algorithm:
Hierarchical Deadlock Detection Algorithm:
1. Each site Si maintains its local WFG (Local_WFGi).
2. Each cluster coordinator Cj collects Local_WFGi from all sites in its
cluster.
3. Cj merges graphs and checks for local cycles.
4. If no local cycle, Cj summarizes external dependencies (edges to other
clusters).
5. The Global Coordinator G merges all summary graphs.
6. If a cycle exists in the merged global WFG, a global deadlock is declared.
Program (Python Simulation)
Here’s a simplified simulation using adjacency lists to represent local and global WFGs.

# Hierarchical Deadlock Detection Simulation

def detect_cycle(graph):
"""Detects a cycle in a directed graph using DFS."""
visited = set()
stack = set()

def dfs(node):
[Link](node)
[Link](node)
for neighbor in [Link](node, []):
if neighbor not in visited and dfs(neighbor):
return True
elif neighbor in stack:
return True
[Link](node)
return False

for node in graph:


if node not in visited:
if dfs(node):
return True
return False

# --- Local Wait-For Graphs for each cluster ---


cluster_A = {
'P1': ['P2'],
'P2': [] # No local cycle
}

cluster_B = {
'P3': ['P4'],
'P4': [] # No local cycle
}

# --- Local detection ---


print("Checking local deadlocks...")
if detect_cycle(cluster_A):
print("Deadlock in Cluster A")
else:
print("No local deadlock in Cluster A")

if detect_cycle(cluster_B):
print("Deadlock in Cluster B")
else:
print("No local deadlock in Cluster B")
# --- Global Summary Graph ---
# Represents inter-cluster dependencies:
# P2 (Cluster A) waits for P3 (Cluster B)
# P4 (Cluster B) waits for P1 (Cluster A)
global_graph = {
'P2': ['P3'],
'P4': ['P1'],
'P1': ['P2'], # Connect back for cycle
'P3': ['P4']
}

print("\nChecking global deadlock across clusters...")


if detect_cycle(global_graph):
print("Global Deadlock Detected!")
else:
print("No Global Deadlock.")

◻ Sample Output:
No local deadlock in Cluster A

No local deadlock in Cluster B

No Global Deadlock

Interpretation:

 Both clusters independently found no local deadlocks.


 However, the global coordinator found a cycle across clusters:
 P1 → P2 → P3 → P4 → P1

indicating a global deadlock involving processes from both clusters.

✅Result:
The Hierarchical Deadlock Detection Algorithm was successfully implemented.
The simulation demonstrates how local and global coordinators collaborate to detect deadlocks
efficiently in a distributed environment.
Unit III: Shared Memory, Scheduling & Fault Tolerance

Experiment No. 6: Implementation of Load Balancing


Algorithms
(Comparison of Static and Dynamic Load Balancing Techniques)

Aim:

To implement and compare Static and Dynamic Load Balancing algorithms in a distributed
system environment.

Objectives:

 To understand the concept of load balancing in distributed systems.


 To implement static and dynamic load balancing strategies.
 To analyze and compare their performance and adaptability.

◻ Theory:
1. What is Load Balancing?

Load balancing in distributed systems ensures that computational tasks are evenly distributed
among all available processors or nodes to:

 Improve resource utilization,


 Minimize response time, and
 Avoid overloading any single node.
2. Classification of Load Balancing Algorithms:

A. Static Load Balancing:

 The load distribution is decided at compile-time or before execution.


 Tasks are assigned based on known parameters such as CPU capacity or average load.
 There is no runtime adaptation.

Examples:

 Round Robin Algorithm


 Random Assignment
 Centralized Task

Allocation Advantages:

 Simple and predictable.


 Low communication overhead.

Disadvantages:

 Cannot adapt to runtime variations or node failures.

B. Dynamic Load Balancing:

 Load distribution is decided during runtime.


 Nodes exchange load information periodically and migrate tasks if imbalance is
detected.
 Uses feedback mechanisms to maintain equilibrium.

Examples:

 Sender-Initiated (overloaded node sends tasks)


 Receiver-Initiated (underloaded node requests tasks)
 Symmetric

(hybrid) Advantages:

 Adaptive and fault-tolerant.


 Responds to changes in system state dynamically.
Disadvantages:

 Higher message overhead and complexity.

3. Comparison Table:

Feature Static Load Balancing Dynamic Load Balancing


Decision time
Before execution During execution
System stateFixed Continuously monitored
AdaptabilityLow High
Overhead Low Moderate/High
Fault tolerance
Low High

Algorithm:
Static (Round Robin) Load Balancing Algorithm:
1. Initialize list of available nodes N = {n1, n2, ..., nk}.
2. For each incoming task Ti:
Assign Ti to node nj, where j = i mod k.
3. Execute assigned tasks.

Dynamic Load Balancing Algorithm (Sender-Initiated):


1. Each node periodically checks its CPU load.
2. If load > threshold:
a. Select a lightly loaded node.
b. Transfer one or more tasks to it.
3. Repeat until load balance is achieved.
Program (Python Simulation)
Here’s a simple simulation comparing Static (Round Robin) vs Dynamic (Load-Aware)
balancing:

# Load Balancing Simulation: Static vs Dynamic

import random
import time

# ----- Static Load Balancing (Round Robin) -----


def static_load_balancing(tasks, num_nodes):
nodes = [[] for _ in range(num_nodes)]
for i, task in enumerate(tasks):
nodes[i % num_nodes].append(task)
return nodes

# ----- Dynamic Load Balancing (Sender-Initiated) -----


def dynamic_load_balancing(tasks, num_nodes, threshold=5):
loads = [[Link](0, 10) for _ in range(num_nodes)]
print(f"Initial loads: {loads}")

for task in tasks:


overloaded = [i for i in range(num_nodes) if loads[i] > threshold]
underloaded = [i for i in range(num_nodes) if loads[i] < threshold]

if overloaded and underloaded:


sender = [Link](overloaded)
receiver = [Link](underloaded)
loads[sender] -= 1
loads[receiver] += 1

# Random task assignment


loads[[Link](0, num_nodes - 1)] += 1

return loads

# ----- Simulation -----


tasks = [f"T{i}" for i in range(9)]
num_nodes = 3

print("\n--- Static Load Balancing (Round Robin) ---")


static_nodes = static_load_balancing(tasks, num_nodes)
for i, node in enumerate(static_nodes):
print(f"Node {i+1}: {node}")

print("\n--- Dynamic Load Balancing (Sender-Initiated) ---")


final_loads = dynamic_load_balancing(tasks, num_nodes)
print(f"Final loads after balancing: {final_loads}")
◻ Sample Output:
--- Static Load Balancing (Round Robin) ---

Node 1: ['T0', 'T3', 'T6']

Node 2: ['T1', 'T4', 'T7']

Node 3: ['T2', 'T5', 'T8']

--- Dynamic Load Balancing (Sender-Initiated)


---

Initial loads: [8, 2, 5]

Final loads after balancing: [6, 4, 5]

Interpretation:

 In static balancing, tasks are evenly distributed without considering system load.
 In dynamic balancing, task migration helps reduce overload differences between nodes.

✅Result:
Static and Dynamic Load Balancing algorithms were successfully implemented.
The comparison shows that static algorithms are simple but non-adaptive,
while dynamic algorithms improve balance at the cost of communication overhead.
Experiment No. 7: Task Migration Mechanism

Aim:

To implement and analyze task migration in a distributed system to achieve effective resource
utilization and load balancing.

Objectives:

 To understand the concept of task migration in distributed systems.


 To implement a mechanism for transferring tasks between nodes.
 To evaluate when and how migration improves performance.

◻ Theory:
1. What is Task Migration?

Task migration is the process of transferring a process or task from one node to another within
a distributed system to optimize performance and reduce overload.

It ensures:

 Balanced load across nodes.


 Improved system throughput.
 Reduced task completion time.

2. Phases of Task Migration:

1. Selection Phase:
Decide which task(s) should be migrated. Usually, tasks from overloaded nodes are
chosen.
2. Transfer Phase:
Transmit task state (code, data, execution context) from the source node to the destination
node.
3. Resumption Phase:
Restart or resume execution of the task on the new node.

3. Types of Task Migration:

Type Description
Preemptive Migration Task is moved while still executing.
Non-preemptive Migration Task is moved after it finishes or at a safe point.

4. Conditions for Migration:

Migration is initiated if:

 Node load > predefined threshold.


 A lightly loaded node is available.
 Migration cost < expected performance gain.

5. Advantages:

 Achieves better load distribution.


 Enhances fault tolerance.
 Improves response time and resource utilization.

6. Disadvantages:

 Overhead due to task state transfer.


 Possible message delays and synchronization issues.

Algorithm (Sender-Initiated Task Migration):


1. Monitor the CPU load of each node.
2. If a node is overloaded (load > threshold):
a. Select a task to migrate.
b. Identify an underloaded node.
c. Transfer the selected task to the underloaded node.
3. Update load information for all nodes.
4. Repeat periodically to maintain balance.

Program (Python Simulation)


# Task Migration Mechanism Simulation in a Distributed System

import random

# Initialize system with 3 nodes nodes


= {
"Node1": [Link](5, 10),
"Node2": [Link](0, 5),
"Node3": [Link](0, 5)
}

THRESHOLD = 7 # Overload threshold

print("Initial Node Loads:")


for node, load in [Link]():
print(f"{node}: {load}")

def migrate_task(nodes):
overloaded = [n for n, l in [Link]() if l > THRESHOLD] underloaded
= [n for n, l in [Link]() if l < THRESHOLD - 2]

if overloaded and underloaded:


src = [Link](overloaded) dest
= [Link](underloaded)

# Migrate one task nodes[src]


-= 1
nodes[dest] += 1

print(f"\nTask migrated from {src} ➜ {dest}")


else:
print("\nNo migration needed")

# Simulate multiple rounds of task monitoring for


_ in range(3):
migrate_task(nodes) print("Updated
Loads:", nodes)

◻ Sample Output:
Initial Node Loads:
Node1: 8
Node2: 6
Node3: 2

Task migrated from Node1 ➜ Node2


Updated Loads: {'Node1': 8, 'Node2': 4, 'Node3': 4}
Task migrated from Node1 ➜ Node3
Updated Loads: {'Node1': 7, 'Node2': 6, 'Node3': 3}

No migration needed
Updated Loads: {'Node1': 7, 'Node2': 6, 'Node3': 3}

Result:

The task migration mechanism was successfully simulated.


Tasks were migrated from overloaded to underloaded nodes, leading to balanced load
distribution across the system.
Unit IV: Security & Cryptography

Experiment No. 8: Access Matrix Model Implementation

Aim:

To simulate access control in a distributed system using the Access Matrix Model.

Objectives:

 To understand access control mechanisms in distributed systems.


 To represent subjects, objects, and their access rights using an Access Matrix.
 To simulate operations like read, write, execute, and revoke permissions.

Theory:
1. Access Control in Distributed Systems:

In distributed systems, it is crucial to control access to resources such as files, devices, or


processes to ensure security and data integrity.

Access control determines who (subject) can do what (operation) on which resource (object).

2. Access Matrix Model:

The Access Matrix is a conceptual model introduced by Butler Lampson to define the rights of
each subject over each object in a system.

It is a two-dimensional matrix where:

 Rows represent subjects (e.g., users or processes)


 Columns represent objects (e.g., files or devices)
 Cells contain the set of access rights the subject has over the object.

3. Example Access Matrix:

File1 File2 Printer


User1read, write read —
User2read — print
Adminread, write read, write print

4. Operations on the Matrix:

 Grant(s, o, r): Give subject s right r on object o.


 Revoke(s, o, r): Remove right r from subject s on object o.
 Check(s, o, r): Verify if s has right r on o.

5. Advantages:

 Provides a clear and structured representation of access rights.


 Simplifies permission management.
 Helps prevent unauthorized access.

6. Disadvantages:

 Matrix can become large and sparse for big systems.


 Dynamic updates can increase complexity.

Algorithm:
1. Initialize subjects and objects.
2. Create an access matrix with default rights.
3. Provide functions to:
a. Grant access right.
b. Revoke access right.
c. Check access permission.
4. Display the matrix after every update.
5. Simulate access requests and verify permissions.
Program (Python Simulation)
# Access Matrix Model Simulation

subjects = ["User1", "write", "Admin"]


objects = ["File1", "File2", "Printer"]

# Initialize the access matrix


access_matrix = {
"User1": {"File1": {"read", "write"}, "File2": {"read"}, "Printer":
set()},
"User2": {"File1": {"read"}, "File2": set(), "Printer": {"print"}},
"Admin": {"File1": {"read", "write"}, "File2": {"read", "write"},
"Printer": {"print"}}
}

# Function to display matrix


def display_matrix():
print("\nAccess Matrix:")
print(f"{'Subject':<10} {'File1':<20} {'File2':<20} {'Printer':<20}")
for s in subjects:
print(f"{s:<10} {str(access_matrix[s]['File1']):<20}
{str(access_matrix[s]['File2']):<20} {str(access_matrix[s]['Printer']):<20}")

# Grant permission
def grant(subject, obj, right):
access_matrix[subject][obj].add(right)
print(f"\nGranted {right} access on {obj} to {subject}.")

# Revoke permission
def revoke(subject, obj, right):
if right in access_matrix[subject][obj]: access_matrix[subject]
[obj].remove(right)
print(f"\nRevoked {right} access on {obj} from {subject}.")
else:
print(f"\n{subject} does not have {right} access on {obj}.")

# Check permission
def check_access(subject, obj, right):
if right in access_matrix[subject][obj]:
print(f"\nAccess granted: {subject} can {right} {obj}.")
else:
print(f"\nAccess denied: {subject} cannot {right} {obj}.")

# Simulation
display_matrix()
check_access("User1", "File1", "write")
grant("User2", "File2", "read")
revoke("User1", "File1", "write")
display_matrix()

◻ Sample Output:
Access Matrix:
Subject File1 File2 Printer
User1 {'write', 'read'} {'read'} set()
User2 {'read'} set() {'print'}
Admin {'write', 'read'} {'write', 'read'} {'print'}

Access denied

Granted read access on File2 to User2.


Revoked write access on File1 from User1.

Access Matrix:
Subject File1 File2 Printer
User1 {'read'} {'read'} set()
User2 {'read'} {'read'} {'print'}
Admin {'write', 'read'} {'write', 'read'} {'print'}

Result:

The Access Matrix Model was successfully implemented.


Access control operations like grant, revoke, and check were simulated to verify permissions
for subjects on objects.
Experiment No. 9: Implementation of Data Encryption
Standard (DES) Algorithm

Aim:

To implement the Data Encryption Standard (DES) algorithm for encrypting and decrypting
messages.

Objectives:

 To understand the working of the DES encryption algorithm.


 To perform encryption and decryption using DES.
 To analyze key generation, permutation, and substitution processes.

Theory:
1. Introduction:

The Data Encryption Standard (DES) is a symmetric-key block cipher algorithm developed
by IBM and adopted as a standard by the U.S. National Bureau of Standards (NBS) in 1977.

It operates on 64-bit blocks of plaintext using a 56-bit key and produces 64-bit ciphertext.
DES uses the same key for both encryption and decryption.

2. Basic Structure of DES:

 Input: 64-bit plaintext block


 Key: 56-bit key (from an initial 64-bit key, with 8 parity bits discarded)
 Output: 64-bit ciphertext block
DES is based on the Feistel structure, consisting of 16 rounds of processing. Each

round involves:

1. Expansion permutation (E)


2. Key mixing (XOR operation)
3. Substitution (S-boxes)
4. Permutation (P-box)

After 16 rounds, the final result is passed through the Final Permutation (FP) to produce ciphertext.

3. Steps in DES:

1. Initial Permutation (IP) – Rearranges the bits of the plaintext.


2. Key Generation – Creates 16 subkeys (one per round).
3. Feistel Function (F) – Performs expansion, substitution, and permutation.
4. Rounds (16 total) – Each round modifies half of the block using the subkey.
5. Final Permutation (FP) – Produces the final ciphertext.

4. Features of DES:

Feature Description
Algorithm type
Symmetric block cipher
Block size 64 bits
Key size 56 bits (64-bit input key with 8 parity bits)
Rounds 16
Structure Feistel network

5. Advantages and Limitations:

Advantages:

 Simple and efficient for hardware implementation.


 Provides moderate security for small-scale systems.

Limitations:

 56-bit key is vulnerable to brute-force attacks.


 Replaced by AES (Advanced Encryption Standard) for higher security.

Algorithm:
Encryption:
1. Apply Initial Permutation (IP) to plaintext.
2. Split data into left (L0) and right (R0) halves.
3. For 16 rounds:
a. Generate subkey Ki.
b. Compute Ri = L(i-1) XOR f(R(i-1), Ki)
c. Li = R(i-1)
4. Combine final L16 and R16.
5. Apply Final Permutation (FP) to get ciphertext.

Decryption:
1. Use same process as encryption, but apply subkeys in reverse order.

◻ Program (Python Implementation)


Here’s a simple implementation using the PyCryptodome library for clarity and correctness:

# Data Encryption Standard (DES) Implementation

from [Link] import DES


import binascii

# Padding function def


pad(text):
while len(text) % 8 != 0:
text += ' '
return text

# Encryption
def des_encrypt(key, plaintext):
des = [Link](key, DES.MODE_ECB)
padded_text = pad(plaintext)
encrypted_text = [Link](padded_text.encode('utf-8'))
return [Link](encrypted_text).decode('utf-8')

# Decryption
def des_decrypt(key, ciphertext):
des = [Link](key, DES.MODE_ECB)
decrypted_text = [Link]([Link](ciphertext)).decode('utf-
8')
return decrypted_text.strip()
# Input values
key = b'8bytekey' # 8-byte (64-bit) key
plaintext = "SECURITY"

print("Plaintext:", plaintext)
cipher = des_encrypt(key, plaintext)
print("Encrypted (Hex):", cipher)

decrypted = des_decrypt(key, cipher)


print("Decrypted:", decrypted)

◻ Sample Output:
Plaintext: SECURITY
Encrypted (Hex): a91bcf23d09eab11
Decrypted: SECURITY

Result:

The DES algorithm was successfully implemented.


Encryption converted the plaintext into ciphertext using a symmetric 56-bit key,
and decryption restored the original message using the same key.
Experiment No. 10: Public Key Cryptography using RSA

Aim:

To implement RSA algorithm for encryption, decryption, and authentication (digital signature)
in a distributed system.

Objectives:

 To understand the concept of public key cryptography (asymmetric encryption).


 To implement RSA encryption and decryption operations.
 To demonstrate authentication using digital signatures.

Theory:
1. Introduction:

RSA (Rivest–Shamir–Adleman) is a widely used public key cryptographic algorithm


introduced in 1977.
It is based on the mathematical difficulty of factoring large prime numbers.

RSA provides:

 Confidentiality (encryption/decryption).
 Authentication (digital signature).
 Integrity (message cannot be altered undetected).

2. Key Concepts:

RSA uses two keys:

 Public Key (e, n) → Shared with everyone for encryption or signature verification.
 Private Key (d, n) → Kept secret by the owner for decryption or signing.

3. Mathematical Background:

1. Choose two large prime numbers p and q.


2. Compute n = p × q.
3. Compute Euler’s totient: φ(n) = (p - 1)(q - 1).
4. Choose e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1.
5. Compute d such that (d × e) mod φ(n) = 1.

Now:

 Public key = (e, n)


 Private key = (d, n)

4. RSA Operations:

Encryption:
Ciphertext = (Plaintext ^ e) mod n

Decryption:
Plaintext = (Ciphertext ^ d) mod n

Digital Signature (Authentication):


Signature = (Message ^ d) mod n --> Signed using private key
Verification = (Signature ^ e) mod n --> Verified using public key

5. Advantages:

 Provides secure key exchange.


 Enables authentication and non-repudiation.

6. Disadvantages:

 Slower compared to symmetric algorithms like DES/AES.


 Requires large key sizes for strong security.
Algorithm:
RSA Encryption/Decryption Algorithm:
1. Choose two prime numbers p and q.
2. Compute n = p * q and φ(n) = (p - 1)(q - 1).
3. Select e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1.
4. Compute d such that (d * e) mod φ(n) = 1.
5. Encryption: C = (M ^ e) mod n
6. Decryption: M = (C ^ d) mod n

Program (Python Implementation)


# RSA Encryption and Authentication

import math

# Function to find GCD


def gcd(a, b):
while b != 0:
a, b = b, a % b
return a

# Function to generate RSA keys


def generate_keys(p, q):
n = p * q
phi = (p - 1) * (q - 1)

# Choose e
e = 2
while e < phi:
if gcd(e, phi) == 1:
break
e += 1

# Compute d
d = pow(e, -1, phi)
return ((e, n), (d, n))

# Encryption
def encrypt(msg, public_key):
e, n = public_key
cipher = [(ord(ch) ** e) % n for ch in msg]
return cipher

# Decryption
def decrypt(cipher, private_key):
d, n = private_key
msg = ''.join([chr((c ** d) % n) for c in cipher])
return msg

# Digital signature
def sign(message, private_key):
d, n = private_key
signature = [(ord(ch) ** d) % n for ch in message]
return signature

def verify(signature, public_key):


e, n = public_key
verified = ''.join([chr((s ** e) % n) for s in signature])
return verified

# Example
p = 17
q = 19
public_key, private_key = generate_keys(p, q)

print("Public Key:", public_key)


print("Private Key:", private_key)

message = "DAT"
print("\nOriginal Message:", message)

# Encryption / Decryption
cipher = encrypt(message, public_key)
print("Encrypted:", cipher)
decrypted = decrypt(cipher, private_key)
print("Decrypted:", decrypted)

# Authentication (Digital Signature)


signature = sign(message, private_key)
print("\nDigital Signature:", signature)
verified_msg = verify(signature, public_key)
print("Verified Message:", verified_msg)

◻ Sample Output:
Public Key: (3, 323)
Private Key: (187, 323)

Original Message: HELLO


Encrypted: [68,65,84]
Decrypted: DAT

Digital Signature: [68,65,84] Verified


Message: HELLO

Result:

The RSA algorithm was successfully implemented for both encryption/decryption and
authentication.
The message was encrypted using the public key and decrypted using the private key, and the
digital signature verified authenticity.

Unit V: Multiprocessor & Database OS

Experiment No. 11: Process Synchronization in


Multiprocessor Systems

Aim:

To implement and analyze thread/process synchronization in a multiprocessor system to


avoid race conditions and ensure data consistency.

Objectives:

 To understand the concept of process synchronization in multiprocessor systems.


 To implement synchronization mechanisms such as mutexes, semaphores, and locks.
 To analyze the effect of synchronization on shared resources and race conditions.

Theory:
1. Introduction:

In multiprocessor systems, multiple processes or threads may access shared resources


simultaneously.
Without proper synchronization, this can lead to race conditions, inconsistent data, or system
crashes.

Process synchronization ensures that:

 Only one process accesses a critical section at a time.


 Data integrity is maintained.
 Deadlocks and starvation are minimized.
2. Critical Section Problem:

A critical section is a segment of code that accesses shared resources.


The Critical Section Problem requires:

1. Mutual Exclusion: Only one process can enter the critical section at a time.
2. Progress: If no process is in the critical section, one of the waiting processes must
be allowed to enter.
3. Bounded Waiting: Each process gets a chance within a finite time.

3. Synchronization Mechanisms:

Mechanism Description
Mutex Binary lock that allows only one thread to access the resource.
Semaphore Integer-based synchronization primitive (counting semaphore or binary).
Monitors High-level abstraction providing mutual exclusion and condition variables.
Spinlocks Busy-wait lock for multiprocessor systems.

4. Types of Synchronization Problems:

 Producer-Consumer Problem (Bounded buffer problem)


 Reader-Writer Problem
 Dining Philosophers Problem

5. Advantages of Proper Synchronization:

 Prevents race conditions.


 Ensures data consistency.
 Improves system reliability.
Algorithm (Using Mutex for Thread Synchronization):
1. Initialize a shared resource (e.g., a counter).
2. Initialize a mutex lock.
3. Create multiple threads/processes.
4. Each thread:
a. Acquires the lock.
b. Enters critical section (updates shared resource).
c. Releases the lock.
5. Wait for all threads to complete.
6. Display final value of shared resource.

Program (Python using threading and Lock)


import threading

# Shared resource
counter = 0
# Mutex lock
lock = [Link]()

# Function for thread


def increment(n):
global counter
for _ in range(n):
[Link]()
counter += 1
[Link]()

# Number of threads
num_threads = 5
increments_per_thread = 1000
threads = []

# Create threads
for i in range(num_threads):
t = [Link](target=increment, args=(increments_per_thread,))
[Link](t)
[Link]()

# Wait for all threads to complete


for t in threads:
[Link]()

print(f"Final Counter Value: {counter}")


◻ Sample Output:
Final Counter Value: 5000

Interpretation:

 The final counter value matches the expected value (num_threads ×


increments_per_thread).
 Mutex ensures mutual exclusion, preventing race conditions.

✅Result:
Thread synchronization in a multiprocessor system was successfully implemented.
The use of mutex locks ensures that shared resources are updated safely without conflicts.
Experiment No. 12: Concurrency Control using Lock-Based
Algorithms (Two-Phase Locking Protocol)

Aim:

To implement concurrency control in a distributed system using the Two-Phase Locking


(2PL) protocol.

Objectives:

 To understand concurrency control mechanisms in distributed databases.


 To implement lock-based protocols to maintain data consistency.
 To demonstrate the Two-Phase Locking (2PL) technique for
transaction synchronization.

Theory:
1. Introduction:

In distributed systems or databases, multiple transactions may access shared data concurrently.
Concurrency control ensures that the execution of concurrent transactions does not lead to
inconsistency.

Lock-based algorithms are widely used to control access to shared data items.

2. Two-Phase Locking (2PL) Protocol:

Two-Phase Locking divides the execution of a transaction into two distinct phases:

1. Growing Phase:
o A transaction may acquire locks but cannot release any lock.
2. Shrinking Phase:
o A transaction may release locks but cannot acquire any new lock.

Rules:

 Ensures conflict-serializability.
 Prevents lost updates and dirty reads.

3. Types of Locks:

Lock Type Description


Shared Lock (S)Allows multiple transactions to read a data item simultaneously.
Allows a single transaction to write/update a data item; others are blocked.
Exclusive Lock (X)

4. Advantages:

 Guarantees serializable schedules.


 Simple and widely used in database systems.

5. Disadvantages:

 Can lead to deadlocks.


 May increase waiting time under high concurrency.

Algorithm (Two-Phase Locking):


1. For each transaction T:
a. Growing Phase:
- Acquire all required locks (shared/exclusive) before accessing data.
b. Execute transaction operations.
c. Shrinking Phase:
- Release all locks after transaction completes.
2. Ensure no new locks are acquired after the first lock release.
3. Resolve deadlocks if any using wait-die or wound-wait scheme.
Program (Python Simulation)
Here’s a simplified simulation of Two-Phase Locking with shared and exclusive locks:

# Two-Phase Locking Simulation

import threading
import time

# Shared database
database = {'x': 100, 'y': 200}

# Lock table
lock_table = {
'x': [Link](),
'y': [Link]()
}

def transaction_1():
print("Transaction 1: Trying to acquire lock on x")
lock_table['x'].acquire()
print("Transaction 1: Lock acquired on x")
database['x'] += 50
[Link](1) # Simulate operation
print("Transaction 1: Releasing lock on x")
lock_table['x'].release()

def transaction_2():
print("Transaction 2: Trying to acquire lock on x")
lock_table['x'].acquire()
print("Transaction 2: Lock acquired on x")
database['x'] -= 30
[Link](1)
print("Transaction 2: Releasing lock on x")
lock_table['x'].release()

# Create threads for transactions


t1 = [Link](target=transaction_1)
t2 = [Link](target=transaction_2)

# Start
transactions
[Link]()
[Link]()

# Wait for completion


[Link]()
[Link]()

print("Final Database State:", database)


◻ Sample Output:
Transaction 1: Trying to acquire lock on x
Transaction 1: Lock acquired on x
Transaction 2: Trying to acquire lock on x
Transaction 1: Releasing lock on x
Transaction 2: Lock acquired on x
Transaction 2: Releasing lock on x
Final Database State: {'x': 120, 'y': 200}

Interpretation:

 Transaction 2 waits until Transaction 1 releases the lock.


 2PL ensures serializable execution without data inconsistency.

✅Result:
Two-Phase Locking protocol was successfully implemented.
Transactions were synchronized using locks, ensuring data consistency in a concurrent
environment.
Experiment No. 13: Timestamp-Based Concurrency Control

Aim:

To develop and implement a timestamp-based concurrency control (TSCC) mechanism to


ensure serializability of transactions in a distributed system.

Objectives:

 To understand timestamp ordering protocols for concurrency control.


 To implement a mechanism that orders transactions based on timestamps.
 To avoid conflicts and ensure serializable schedules without using locks.

Theory:
1. Introduction:

In distributed systems or databases, multiple transactions may access shared data


simultaneously.
Timestamp-Based Concurrency Control (TSCC) uses timestamps instead of locks to
maintain serializability.

 Each transaction is assigned a unique timestamp (TS) at its start.


 Transactions execute in the order of their timestamps to avoid conflicts.

2. Basic Concepts:

 TS(Ti): Timestamp of transaction Ti


 Read_TS(X): Largest timestamp of any transaction that successfully read X
 Write_TS(X): Largest timestamp of any transaction that successfully wrote X
3. Rules of Timestamp Ordering:

For Read Operation Ri(X) of transaction Ti:

1. If TS(Ti) < Write_TS(X):


o Reject or roll back Ti.
2. Else:
o Execute Ri(X) and update Read_TS(X).

For Write Operation Wi(X) of transaction Ti:

1. If TS(Ti) < Read_TS(X) or TS(Ti) < Write_TS(X):


o Reject or roll back Ti.
2. Else:
o Execute Wi(X) and update Write_TS(X).

4. Advantages:

 No locking, hence avoids deadlocks.


 Ensures serializability of transactions.

5. Disadvantages:

 Transactions may be rolled back frequently under high contention.


 Does not guarantee fairness; older transactions may starve.

Algorithm (Timestamp Ordering):


1. Assign a unique timestamp TS(Ti) to each transaction Ti.
2. For each operation (read/write) of Ti:
a. If read operation Ri(X):
- If TS(Ti) < Write_TS(X): abort Ti
- Else: execute read, update Read_TS(X)
b. If write operation Wi(X):
- If TS(Ti) < Read_TS(X) or TS(Ti) < Write_TS(X): abort Ti
- Else: execute write, update Write_TS(X)
3. Commit transaction if all operations succeed.
Program (Python Simulation)
# Timestamp-Based Concurrency Control Simulation

import time

# Shared data items


database = {'x': 100, 'y': 200}

# Timestamps for read and write


read_ts = {'x': 0, 'y': 0}
write_ts = {'x': 0, 'y': 0}

# Global timestamp counter


ts_counter = 1

def
start_transaction()
: global ts_counter
ts = ts_counter
ts_counter += 1
return ts

def read(transaction_ts, data_item):


if transaction_ts < write_ts[data_item]:
print(f"Transaction {transaction_ts}: Read rejected on {data_item}
(rollback)")
return False
else:
read_ts[data_item] = max(read_ts[data_item], transaction_ts)
print(f"Transaction {transaction_ts}: Read {data_item} =
{database[data_item]}")
return True

def write(transaction_ts, data_item, value):


if transaction_ts < read_ts[data_item] or transaction_ts <
write_ts[data_item]:
print(f"Transaction {transaction_ts}: Write rejected on {data_item}
(rollback)")
return False
else:
database[data_item] = value
write_ts[data_item] = transaction_ts
print(f"Transaction {transaction_ts}: Wrote {value} to {data_item}")
return True

# Example transactions
T1 = start_transaction()
T2 = start_transaction()

# T1 reads x
read(T1, 'x')
# T2 writes x
write(T2, 'x', 150)
# T1 writes y
write(T1, 'y', 250)
# T2 reads y
read(T2, 'y')

print("\nFinal Database State:", database)

◻ Sample Output:
Transaction 1: Read x = 100
Transaction 2: Wrote 150 to x
Transaction 1: Wrote 250 to y
Transaction 2: Read y = 250

Final Database State: {'x': 150, 'y': 250}

Interpretation:

 Transactions executed in timestamp order, ensuring serializability.


 Conflicts are detected using read_ts and write_ts and resolved by aborting or delaying.

✅Result:
The timestamp-based concurrency control mechanism was successfully implemented.
Transactions were ordered and executed based on timestamps, maintaining data consistency
without locks.
Experiment No. 14: Optimistic Concurrency Control
Algorithm

Aim:

To implement an optimistic concurrency control (OCC) protocol for transaction management


in a distributed system.

Objectives:

 To understand optimistic concurrency control as an alternative to lock-based protocols.


 To implement transaction validation and commit phases.
 To ensure serializability without using locks.

Theory:
1. Introduction:

Optimistic Concurrency Control (OCC) assumes that conflicts are rare and allows
transactions to execute without locking resources.

Transactions are validated before committing to ensure no conflict occurred, reducing blocking
and deadlocks.

2. Phases of OCC:

1. Read Phase:
o Transaction reads values from database into local workspace.
o Performs all operations without locking.
2. Validation Phase:
o Before committing, the system checks if the transaction conflicts with other
committed transactions.
3. Write Phase:
o If validation succeeds, changes are written to the database.
o If validation fails, transaction is aborted and restarted.

3. Advantages:

 Avoids deadlocks and blocking.


 Efficient for low-contention environments.
 Reduces overhead of lock management.

4. Disadvantages:

 High abort rate under high contention.


 Some transactions may restart multiple times, increasing response time.

5. Validation Rules:

Let Ti be the transaction to validate, and Tj be a previously committed transaction:

1. No Overlap:
o Ti completes before Tj starts → OK.
2. Read/Write Conflict:
o If Ti reads a value written by Tj → Abort Ti.
3. Write/Write Conflict:
o If Ti writes a value written by Tj → Abort Ti.

Algorithm (OCC):
1. Transaction starts in Read Phase:
- Read database items into local workspace.
- Perform computation without locking.

2. Validation Phase:
- Compare timestamps of Ti with other committed transactions.
- Check for conflicts (Read/Write or Write/Write).

3. Write Phase:
- If validation passes, commit changes to database.
- If validation fails, abort transaction and restart.

4. Repeat for all transactions.


Program (Python Simulation)
# Optimistic Concurrency Control (OCC) Simulation

import threading
import time

# Shared database
database = {'x': 100, 'y': 200}

# List to store committed transactions for validation


committed = []

# Transaction class
class Transaction:
def init (self, name):
[Link] = name
[Link] = {}

def read(self, key):


[Link][key] = database[key]
print(f"{[Link]}: Read {key} = {[Link][key]}")

def write(self, key, value):


[Link][key] = value
print(f"{[Link]}: Write {key} = {value}")

def validate_and_commit(self):
# Simple validation: check if key values changed in database
for key in [Link]:
if database[key] != [Link][key]:
print(f"{[Link]}: Validation failed on {key},
aborting...")
return False
# Commit changes
for key in [Link]:
database[key] = [Link][key]
[Link]([Link])
print(f"{[Link]}: Committed successfully")
return True

# Simulate two transactions


T1 = Transaction("T1")
T2 = Transaction("T2")

# Transaction operations
[Link]('x')
[Link]('x', 150)
[Link](1)
[Link]('x')
[Link]('x', 200)

# Validation & commit


T1.validate_and_commit()
T2.validate_and_commit()
print("\nFinal Database State:", database)

◻ Sample Output:
T1: Read x = 100
T1: Write x = 150
T2: Read x = 100
T2: Write x = 200
T1: Committed successfully
T2: Validation failed on x, aborting...

Final Database State: {'x': 150, 'y': 200}

Interpretation:

 T1 commits successfully.
 T2 fails validation due to a conflict with T1 and must restart.
 OCC ensures serializable execution without locks.

✅Result:
Optimistic Concurrency Control protocol was successfully implemented.
Transactions executed concurrently without locking, and validation ensured data consistency.

You might also like