CS2850 2024 Past Paper Solution
CS2850: Operating Systems - 2024 Past Paper Solution
Question 1: Theory Questions (25 marks)
(a) Briefly describe I/O handling based on interrupts. Give one advantage of this method over busy waiting.
Description of Interrupt-based I/O Handling:
Interrupt-based I/O handling is a mechanism where the CPU initiates an I/O operation with a device and then continues executing other tasks. Instead of the CPU continuously checking the status of the I/O device (as in busy waiting), the I/O device itself notifies the CPU once the requested operation is complete or if an error occurs. This notification is called an interrupt.
The process is as follows:
- The CPU issues a command to an I/O module (e.g., disk controller) to perform an operation (e.g., read data).
- The I/O module starts the operation. The CPU is now free to perform other tasks or run other processes.
- When the I/O module completes the operation (or an error occurs), it sends an interrupt signal to the CPU.
- The CPU detects the interrupt. It suspends its current execution (saving its state, like the program counter and registers).
- The CPU then executes a special routine called an Interrupt Service Routine (ISR) or interrupt handler, which is specific to the type of interrupt received. This routine determines the cause of the interrupt, performs any necessary data transfer, and clears the interrupt.
- After the ISR completes, the CPU restores the saved state of the suspended task and resumes its execution.
Advantage over Busy Waiting:
- Increased CPU Efficiency: The primary advantage of interrupt-based I/O over busy waiting is significantly improved CPU utilization. In busy waiting, the CPU wastes cycles repeatedly polling the device status. With interrupts, the CPU can perform useful work on other processes or tasks while the I/O operation is in progress, only attending to the device when it explicitly signals completion. This leads to better overall system performance and responsiveness.
(b) This question is about the Round Robin scheduling algorithm for interactive systems:
i. Briefly explain how the algorithm works.
Round Robin (RR) Algorithm Explanation:
The Round Robin scheduling algorithm is designed primarily for time-sharing systems. It is a preemptive scheduling algorithm.
- Ready Queue as FIFO: Processes ready to run are kept in a First-In, First-Out (FIFO) queue called the ready queue.
- Time Quantum (Time Slice): Each process is allocated a small unit of CPU time called a time quantum or time slice (typically 10-100 milliseconds).
- Execution and Preemption:
- The scheduler picks the first process from the ready queue and dispatches it to the CPU.
- The process runs for, at most, the duration of one time quantum.
- If the process completes its CPU burst before the time quantum expires, it voluntarily relinquishes the CPU. The scheduler then proceeds to the next process in the ready queue.
- If the CPU burst of the currently running process is longer than the time quantum, the timer goes off, generating an interrupt. The OS then preempts the process, context-switches it out, moves it to the tail of the ready queue, and dispatches the next process from the head of the ready queue.
- Fairness: RR ensures that each process gets a fair share of the CPU over time, preventing any single process from monopolizing the CPU. This makes it suitable for interactive systems where responsiveness is important.
ii. Consider four processes A, B, C, and D. Suppose their running times are:
A: 3ms, B: 7ms, C: 4ms, D: 5ms.
Indicate how these processes are scheduled by the Round Robin scheduling algorithm until all finish their execution, assuming a quantum length of 4ms. The initial list of runnable processes is A, B, C, D (A is at the head). Make sure to show all your workings.
Round Robin Scheduling Execution:
Quantum = 4ms
Initial Ready Queue: [A, B, C, D]
Process Running Times: A=3, B=7, C=4, D=5
Time | Process Running | CPU Time Used | Remaining Time | Ready Queue | Notes |
---|---|---|---|---|---|
0 | A | A=3, B=7, C=4, D=5 | [B, C, D] | A starts | |
3 | A | 3 (ends) | A=0, B=7, C=4, D=5 | [B, C, D] | A finishes (used 3ms < 4ms quantum) |
3 | B | B=7, C=4, D=5 | [C, D] | B starts | |
7 | B | 4 | B=3, C=4, D=5 | [C, D, B] | B quantum expires, B preempted |
7 | C | B=3, C=4, D=5 | [D, B] | C starts | |
11 | C | 4 (ends) | B=3, C=0, D=5 | [D, B] | C finishes (used 4ms = 4ms quantum) |
11 | D | B=3, D=5 | [B] | D starts | |
15 | D | 4 | B=3, D=1 | [B, D] | D quantum expires, D preempted |
15 | B | B=3, D=1 | [D] | B starts (remaining 3ms) | |
18 | B | 3 (ends) | B=0, D=1 | [D] | B finishes (used 3ms < 4ms quantum) |
18 | D | D=1 | [] | D starts (remaining 1ms) | |
19 | D | 1 (ends) | D=0 | [] | D finishes (used 1ms < 4ms quantum) |
Execution Order & Gantt Chart:
1 |
|
Workings:
- 0-3ms: A runs for 3ms and finishes. Ready Queue: [B, C, D].
- 3-7ms: B runs for its quantum of 4ms. Remaining time for B is 7-4 = 3ms. B is moved to the end of the queue. Ready Queue: [C, D, B].
- 7-11ms: C runs for 4ms and finishes. Ready Queue: [D, B].
- 11-15ms: D runs for its quantum of 4ms. Remaining time for D is 5-4 = 1ms. D is moved to the end of the queue. Ready Queue: [B, D].
- 15-18ms: B runs for its remaining 3ms and finishes. Ready Queue: [D].
- 18-19ms: D runs for its remaining 1ms and finishes. Ready Queue: [].
All processes have completed execution.
(c) Explain in which scenario context switching is faster for threads than for processes.
Context switching is faster for threads within the same process (often called kernel-level threads or lightweight processes if they share the same address space) than for traditional heavyweight processes primarily because:
- Shared Address Space: Threads within the same process share the same memory address space. When switching between these threads, the operating system does not need to change the memory management context (e.g., page tables, TLB entries). This is a significant overhead in process context switching, as each process typically has its own distinct address space. Flushing and reloading page table information can be time-consuming.
- Shared Resources: Besides the address space, threads within the same process also share other resources like open files, global variables, and child processes. Switching between threads of the same process involves saving and restoring fewer resources compared to switching between different processes which have their own isolated sets of these resources.
Therefore, a context switch between threads of the same process mainly involves saving and restoring the thread-specific context, such as the program counter, registers, stack pointer, and thread-specific data. This is a much lighter operation than a full process context switch which requires changing the entire memory map and managing a larger set of distinct resources.
User-level threads, if managed entirely within a single process without kernel involvement for each switch, can be even faster, as a context switch might just involve a procedure call within the process. However, the question likely refers to kernel-supported threads where the OS is aware of them.
(d) Consider a program that consists of the following two concurrent processes sharing the variable x:
P1: x=4; if (x>3) { x=x+5; } else { x=x*3; }
P2: x=2; x=x*2;
How many different values may the variable x have when the program terminates? Give a possible execution sequence for each case.
Let’s analyze the possible interleavings of operations. Assume x
is initially undefined or 0 before P1 or P2 assign to it. The critical operations are the assignment to x
and the read of x
in P1’s conditional.
Operations:
- P1_assign_4:
x = 4
- P1_check_cond:
if (x > 3)
- P1_add_5:
x = x + 5
- P1_mul_3:
x = x * 3
- P2_assign_2:
x = 2
- P2_mul_2:
x = x * 2
Possible Final Values of x:
Scenario 1: P1 executes completely, then P2 executes completely.
- P1_assign_4 (x becomes 4)
- P1_check_cond (4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- P2_assign_2 (x becomes 2)
- P2_mul_2 (x becomes 2 * 2 = 4)
- Final x = 4
Scenario 2: P2 executes completely, then P1 executes completely.
- P2_assign_2 (x becomes 2)
- P2_mul_2 (x becomes 2 * 2 = 4)
- P1_assign_4 (x becomes 4)
- P1_check_cond (4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- Final x = 9
Scenario 3: P1 starts, P2 interrupts before P1’s conditional.
- P1_assign_4 (x becomes 4)
- P2_assign_2 (x becomes 2)
- P2_mul_2 (x becomes 2 * 2 = 4)
- P1_check_cond (x is 4, so 4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- Final x = 9 (Same as Scenario 2 in this case because P2’s final x is 4, which P1 then overwrites its initial assignment with and proceeds based on that.)
Scenario 4: P1 starts, P2 interrupts P1 after P1’s assignment
x=4
but beforeif (x>3)
and P2 completes.- P1_assign_4 (x becomes 4)
- P2_assign_2 (x becomes 2)
- P2_mul_2 (x becomes 2 * 2 = 4)
- P1_check_cond (current x is 4, 4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- Final x = 9
Scenario 5: P2 starts, P1 interrupts P2 after P2’s assignment
x=2
but beforex=x*2
, and P1 completes.- P2_assign_2 (x becomes 2)
- P1_assign_4 (x becomes 4)
- P1_check_cond (4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- P2_mul_2 (x, which is 9, becomes 9 * 2 = 18)
- Final x = 18
Scenario 6: P1 assigns
x=4
, P1 checksx>3
(true). P2 interrupts beforex=x+5
and completes.- P1_assign_4 (x becomes 4)
- P1_check_cond (4 > 3 is true, so P1 will execute
x=x+5
) - P2_assign_2 (x becomes 2)
- P2_mul_2 (x becomes 2 * 2 = 4)
- P1_add_5 (P1 now uses the current value of x, which is 4, so x becomes 4 + 5 = 9)
- Final x = 9
Scenario 7: P2 assigns
x=2
. P1 executes completely. Then P2 doesx=x*2
.- P2_assign_2 (x becomes 2)
- P1_assign_4 (x becomes 4)
- P1_check_cond (4 > 3 is true)
- P1_add_5 (x becomes 4 + 5 = 9)
- P2_mul_2 (P2 now uses the current value of x, which is 9, so x becomes 9 * 2 = 18)
- Final x = 18
Scenario 8: P1 assigns
x=4
. P2 executesx=2
. P1 checks condition (x is 2, 2>3 is false). P1 doesx=x*3
. Then P2 doesx=x*2
.- P1_assign_4 (x becomes 4)
- P2_assign_2 (x becomes 2)
- P1_check_cond (current x is 2; 2 > 3 is false)
- P1_mul_3 (x becomes 2 * 3 = 6)
- P2_mul_2 (x becomes 6 * 2 = 12)
- Final x = 12
Scenario 9: P2 assigns
x=2
. P1 assignsx=4
. P2 doesx=x*2
(x becomes 4*2=8). P1 checks condition (x is 8, 8>3 is true). P1 doesx=x+5
.- P2_assign_2 (x becomes 2)
- P1_assign_4 (x becomes 4)
- P2_mul_2 (current x is 4, so x becomes 4 * 2 = 8)
- P1_check_cond (current x is 8, 8 > 3 is true)
- P1_add_5 (x becomes 8 + 5 = 13)
- Final x = 13
Let’s re-evaluate systematically by considering the point where P1 reads x for its condition and P1 writes its final result, relative to P2’s operations.
P1’s operations:
x = 4
(Write_P1_init)- Read
x
forif (x > 3)
(Read_P1_cond) - If true:
x = x + 5
(Write_P1_true) - If false:
x = x * 3
(Write_P1_false)
P2’s operations:
x = 2
(Write_P2_init)x = x * 2
(Write_P2_final)
Consider the value of x
when P1 executes Read_P1_cond
:
Case A: P1’s
Write_P1_init
is the last write tox
beforeRead_P1_cond
.x
is 4. Condition (4 > 3) is true. P1 will executex = x + 5
.- Subcase A1: P1 executes
Write_P1_true
(to 9). Then P2 runs.- P2_Write_P2_init (x becomes 2)
- P2_Write_P2_final (x becomes 2 * 2 = 4). Final x = 4.
- Sequence: P1_init(x=4) -> P1_read(x=4, true) -> P1_true(x=9) -> P2_init(x=2) -> P2_final(x=4)
- Subcase A2: P2 runs. Then P1 executes
Write_P1_true
.- P2_Write_P2_init (x becomes 2)
- P2_Write_P2_final (x becomes 2 * 2 = 4)
- P1_Write_P1_true (using its original read of x=4 for
x+5
, sox
becomes4+5=9
, or if it re-reads,x
is 4,x
becomes4+5=9
. Let’s assume it uses value from when condition was evaluated for the calculation, or current value if not stored locally. The codex=x+5
implies currentx
. So if P2 ran completely in between P1’s check and P1’s update,x
would be 4. So P1 setsx = 4+5=9
). Final x = 9. - Sequence: P1_init(x=4) -> P1_read(x=4, true) -> P2_init(x=2) -> P2_final(x=4) -> P1_true(x=4+5=9)
- Subcase A3: P2’s
Write_P2_init
happens, thenWrite_P2_final
, then P1’sWrite_P1_true
. (This means P1 did P1_init, P1_read, then P2 ran, then P1_true).- P1_init (x=4) -> P1_read (x=4, cond is true).
- P2_init (x=2) -> P2_final (x=4).
- P1_true (x=x+5, current x is 4, so x=4+5=9). Final x = 9.
- Sequence: P1_init(x=4) -> P1_read(x=4, true) -> P2_init(x=2) -> P2_final(x=4) -> P1_true(x=4+5=9)
- Subcase A4: P1_init, P1_read. Then P2_init. Then P1_true. Then P2_final.
- P1_init (x=4) -> P1_read (x=4, cond is true).
- P2_init (x=2).
- P1_true (x=x+5, current x is 2, so x=2+5=7).
- P2_final (x=x2, current x is 7, so x=72=14). Final x = 14.
- Sequence: P1_init(x=4) -> P1_read(x=4, true) -> P2_init(x=2) -> P1_true(x=2+5=7) -> P2_final(x=72=14)*
- Subcase A5: P1_init, P1_read. Then P1_true. Then P2_init. Then P2_final. (Covered by A1, x=4)
- Subcase A6: P2_init. Then P1_init, P1_read. Then P2_final. Then P1_true.
- P2_init (x=2).
- P1_init (x=4) -> P1_read (x=4, cond is true).
- P2_final (x=x2, current x is 4, so x=42=8).
- P1_true (x=x+5, current x is 8, so x=8+5=13). Final x = 13.
- Sequence: P2_init(x=2) -> P1_init(x=4) -> P1_read(x=4, true) -> P2_final(x=42=8) -> P1_true(x=8+5=13)*
Case B: P2’s
Write_P2_init
is the last write tox
beforeRead_P1_cond
.x
is 2. Condition (2 > 3) is false. P1 will executex = x * 3
.- Subcase B1: P1 executes
Write_P1_false
. Then P2’sWrite_P2_final
executes.- P1_Write_P1_false (x becomes 2 * 3 = 6).
- P2_Write_P2_final (x becomes 6 * 2 = 12). Final x = 12.
- Sequence: P1_init(x=4) (or P2_init first, doesn’t matter for P1_read if P2_init is the most recent) -> P2_init(x=2) -> P1_read(x=2, false) -> P1_false(x=23=6) -> P2_final(x=62=12)
- Subcase B2: P2’s
Write_P2_final
executes. Then P1 executesWrite_P1_false
.- P2_Write_P2_final (x becomes 2 * 2 = 4).
- P1_Write_P1_false (current x is 4, so x becomes 4 * 3 = 12). Final x = 12.
- Sequence: P1_init(x=4) -> P2_init(x=2) -> P1_read(x=2, false) -> P2_final(x=22=4) -> P1_false(x=43=12)
Case C: P2’s
Write_P2_final
is the last write tox
beforeRead_P1_cond
.- This implies P2 ran completely.
x
is 4. Condition (4 > 3) is true. P1 will executex = x + 5
. - P1_Write_P1_true (x becomes 4 + 5 = 9). Final x = 9.
- Sequence: P2_init(x=2) -> P2_final(x=4) -> P1_init(x=4, no effect) -> P1_read(x=4, true) -> P1_true(x=4+5=9)
- This implies P2 ran completely.
Let’s list the distinct values found so far: 4, 9, 18 (from earlier manual trace), 12, 13, 14.
Let’s ensure all operations are atomic for simplicity (each line is one atomic step).
Possible values and one sequence for each:
- x = 4
- Sequence: P1_assign_4 (x=4) -> P1_check_cond (true) -> P1_add_5 (x=9) -> P2_assign_2 (x=2) -> P2_mul_2 (x=4).
- x = 9
- Sequence: P2_assign_2 (x=2) -> P2_mul_2 (x=4) -> P1_assign_4 (x=4) -> P1_check_cond (true) -> P1_add_5 (x=9).
- x = 12
- Sequence: P1_assign_4 (x=4) -> P2_assign_2 (x=2) -> P1_check_cond (x=2, false) -> P1_mul_3 (x=23=6) -> P2_mul_2 (x=62=12).
- x = 13
- Sequence: P2_assign_2 (x=2) -> P1_assign_4 (x=4) -> P2_mul_2 (x=4*2=8) -> P1_check_cond (x=8, true) -> P1_add_5 (x=8+5=13).
- x = 14
- Sequence: P1_assign_4 (x=4) -> P1_check_cond (x=4, true) -> P2_assign_2 (x=2) -> P1_add_5 (x=2+5=7) -> P2_mul_2 (x=7*2=14).
- x = 18 (This was: P2_assign_2 (x=2) -> P1_assign_4 (x=4) -> P1_check_cond (true) -> P1_add_5 (x=9) -> P2_mul_2 (x=9*2=18) )
- Sequence: P2_assign_2 (x=2) -> P1_assign_4 (x=4) -> P1_check_cond (x=4, true) -> P1_add_5 (x=4+5=9) -> P2_mul_2 (x=9*2=18).
The distinct values for x are: 4, 9, 12, 13, 14, 18.
There are 6 different possible values.
Question 2: Theory Questions (25 marks)
(a) In the context of Inter-Process Communication, explain how the sleep and wakeup primitives work. Give one advantage of semaphores over these primitives.
Sleep and Wakeup Primitives:sleep
and wakeup
are system calls used for process synchronization, often in the context of producer-consumer problems or other situations where one process needs to wait for an event triggered by another.
sleep()
: When a process callssleep()
, it voluntarily blocks itself (i.e., suspends its execution) until another process wakes it up. Thesleep()
call typically takes an address (or an event identifier) as a parameter, which specifies the event for which the process is waiting. The process is moved from the running state to a blocked/waiting state.wakeup(event)
: When a process callswakeup(event)
, it signals that the event (identified by theevent
parameter) has occurred. The operating system then searches its list of blocked processes for any process that is sleeping on this specificevent
. If one or more such processes are found, one or all of them (depending on the implementation) are moved from the blocked state to the ready state, making them eligible to be scheduled by the CPU.
A Problem with Sleep/Wakeup: The Lost Wakeup Signal
A critical issue with basic sleep/wakeup is the “lost wakeup signal.” This can occur if:
- A process (consumer) checks a condition (e.g., buffer is empty) and decides to go to sleep.
- Before the consumer actually calls
sleep()
, it is preempted. - Another process (producer) produces an item, sees the consumer is (about to be) waiting, and calls
wakeup()
for the consumer. - This
wakeup()
signal is lost because the consumer is not yet asleep (it was preempted just before callingsleep()
). - The consumer resumes, calls
sleep()
, and may sleep forever if the producer doesn’t produce again.
Advantage of Semaphores over Sleep/Wakeup:
One significant advantage of semaphores over basic sleep/wakeup primitives is that semaphores remember wakeup signals.
- A semaphore has an internal counter. When a
signal
(orV
orup
) operation is performed on a semaphore, its counter is incremented. If there are processes blocked on that semaphore (due to a previouswait
orP
ordown
operation finding the counter at 0), one of them is awakened. - Crucially, if
signal
is called and no process is currently waiting, the semaphore “remembers” this by keeping its counter incremented. A subsequentwait
operation by another process will find the counter positive, decrement it, and proceed without blocking. - In contrast, with raw
sleep
/wakeup
, ifwakeup
is called beforesleep
, the wakeup signal is lost. Semaphores solve this lost wakeup problem by storing the signals (as a count). This makes them a more robust mechanism for synchronization.
(b) In the context of deadlock avoidance algorithms, consider the following states, assuming there is only one type of resource: … For each of the states (X and Y), indicate whether it is safe or unsafe, and justify your classification.
Total resources in the system = 10.
To determine if a state is safe, we use an algorithm similar to the Banker’s Algorithm. We need to find if there’s a sequence of process completions such that all processes can finish. A process can finish if its remaining Need
(Max - Has) is less than or equal to the Available
resources. Once a process finishes, it releases its Has
resources, increasing the Available
resources.
State X:
Total Resources = 10
Process | Has | Max | Need (Max - Has) |
---|---|---|---|
A | 1 | 8 | 7 |
B | 3 | 9 | 6 |
C | 1 | 2 | 1 |
D | 1 | 5 | 4 |
Total Has | 6 |
Available Resources = Total Resources - Total Has = 10 - 6 = 4.
Let’s try to find a safe sequence:
- Initial Available = 4.
- Process A needs 7 (7 > 4, cannot run).
- Process B needs 6 (6 > 4, cannot run).
- Process C needs 1 (1 <= 4, can run). Assume C runs and finishes.
- New Available = Available + C.Has = 4 + 1 = 5.
- Processes remaining: A, B, D. Sequence: <C>
- Available = 5.
- Process A needs 7 (7 > 5, cannot run).
- Process B needs 6 (6 > 5, cannot run).
- Process D needs 4 (4 <= 5, can run). Assume D runs and finishes.
- New Available = Available + D.Has = 5 + 1 = 6.
- Processes remaining: A, B. Sequence: <C, D>
- Available = 6.
- Process A needs 7 (7 > 6, cannot run).
- Process B needs 6 (6 <= 6, can run). Assume B runs and finishes.
- New Available = Available + B.Has = 6 + 3 = 9.
- Processes remaining: A. Sequence: <C, D, B>
- Available = 9.
- Process A needs 7 (7 <= 9, can run). Assume A runs and finishes.
- New Available = Available + A.Has = 9 + 1 = 10.
- Processes remaining: None. Sequence: <C, D, B, A>
- Process A needs 7 (7 <= 9, can run). Assume A runs and finishes.
Since we found a safe sequence (<C, D, B, A>), State X is SAFE.
State Y:
Total Resources = 10
Process | Has | Max | Need (Max - Has) |
---|---|---|---|
A | 2 | 9 | 7 |
B | 2 | 10 | 8 |
C | 3 | 5 | 2 |
D | 1 | 6 | 5 |
Total Has | 8 |
Available Resources = Total Resources - Total Has = 10 - 8 = 2.
Let’s try to find a safe sequence:
- Initial Available = 2.
- Process A needs 7 (7 > 2, cannot run).
- Process B needs 8 (8 > 2, cannot run).
- Process C needs 2 (2 <= 2, can run). Assume C runs and finishes.
- New Available = Available + C.Has = 2 + 3 = 5.
- Processes remaining: A, B, D. Sequence: <C>
- Available = 5.
- Process A needs 7 (7 > 5, cannot run).
- Process B needs 8 (8 > 5, cannot run).
- Process D needs 5 (5 <= 5, can run). Assume D runs and finishes.
- New Available = Available + D.Has = 5 + 1 = 6.
- Processes remaining: A, B. Sequence: <C, D>
- Available = 6.
- Process A needs 7 (7 > 6, cannot run).
- Process B needs 8 (8 > 6, cannot run).
At this point, neither Process A nor Process B can acquire the resources they need to complete (A needs 7, B needs 8, but only 6 are available). There is no process that can run to completion and release its resources.
Therefore, we cannot find a safe sequence.
State Y is UNSAFE.
(c) Ext4 i-nodes are 256 bytes in size. Suppose there are 2,000 files on the disk, 200 of which are currently open by processes. What is the total main memory usage of i-nodes in this scenario? Justify your answer.
Justification and Calculation:
- An i-node (index node) stores metadata about a file (e.g., permissions, ownership, timestamps, disk block locations).
- When a file is open, its i-node is typically read from the disk into main memory to allow for faster access to its metadata and data blocks.
- I-nodes for files that are not open generally reside on the disk and are not continuously kept in main memory just because they exist. They are loaded on demand when the file is accessed.
Given:
- Size of one Ext4 i-node = 256 bytes.
- Number of currently open files = 200.
The main memory usage for i-nodes will primarily be for the i-nodes of the files that are currently open. The i-nodes for the other 1,800 (2000 - 200) closed files remain on disk.
Total main memory usage for i-nodes = (Number of open files) * (Size of one i-node)
Total main memory usage = 200 files * 256 bytes/file
Total main memory usage = 51,200 bytes
This can also be expressed in kilobytes:
51,200 bytes / 1024 bytes/KB = 50 KB.
Therefore, the total main memory usage of i-nodes in this scenario is 51,200 bytes (or 50 KB). This is because only the i-nodes of open files are generally required to be in main memory for active use.
(d) This question is about the Working Set page replacement algorithm. The following table shows the time of last use, and the R and M bits for each memory page (the times are in ms).
Assuming a page fault occurs at virtual time 200ms, and the Working Set algorithm is used with $\tau=100ms$.
Page | Time of Last Use (ms) | R M |
---|---|---|
1 | 90 | 10 |
2 | 100 | 00 |
3 | 30 | 11 |
4 | 50 | 01 |
Current virtual time (VT) = 200ms.
Working set window ($\tau$) = 100ms.
i. For each page, indicate whether it is in the working set or not, and justify your conclusion.
A page is in the working set if it has been referenced within the last $\tau$ time units.
The age of a page is VT - Time of Last Use.
A page is in the working set if its R bit is 1 OR if its age $\le \tau$. (Some definitions strictly use age, others consider the R bit which indicates recent use even if Time of Last Use
hasn’t been updated for this exact tick but was used within the interval where R bits are periodically cleared).
Let’s primarily use age: (VT - Time of Last Use) $\le \tau$.
Page 1:
- Time of Last Use = 90ms.
- Age = 200ms - 90ms = 110ms.
- Is age (110ms) $\le \tau$ (100ms)? No (110 > 100).
- R bit is 1. This indicates it was referenced recently, potentially within the current or previous $\tau$ interval before R bits were last cleared.
- Strictly by “Time of Last Use” and age: Not in working set.
- If “R=1” means “referenced in current window $\tau$”: In working set.
- Standard interpretation: If R=1, it implies the page has been used since the R bit was last cleared. The Working Set algorithm typically scans pages. If R=1, the page is considered part of the working set for the current window, and its “Time of Last Use” is updated to the current virtual time, and R is cleared. If R=0, then its age is checked.
- Let’s assume if R=1, it’s considered in the working set for this scan.
- Page 1: IN WORKING SET. Justification: R bit is 1, indicating recent use. (Alternatively, if strictly by age: Age (110ms) > $\tau$ (100ms), so if R bit was ignored or just cleared, it would be out. But R=1 usually keeps it in for one more round). The problem phrasing “Time of Last Use” combined with R bits suggests R bits are important.
Page 2:
- Time of Last Use = 100ms.
- Age = 200ms - 100ms = 100ms.
- Is age (100ms) $\le \tau$ (100ms)? Yes (100 <= 100).
- R bit is 0.
- Page 2: IN WORKING SET. Justification: Age (100ms) is equal to $\tau$ (100ms).
Page 3:
- Time of Last Use = 30ms.
- Age = 200ms - 30ms = 170ms.
- Is age (170ms) $\le \tau$ (100ms)? No (170 > 100).
- R bit is 1.
- Page 3: IN WORKING SET. Justification: R bit is 1, indicating recent use.
Page 4:
- Time of Last Use = 50ms.
- Age = 200ms - 50ms = 150ms.
- Is age (150ms) $\le \tau$ (100ms)? No (150 > 100).
- R bit is 0.
- Page 4: NOT IN WORKING SET. Justification: R bit is 0 AND Age (150ms) > $\tau$ (100ms).
Summary for i:
- Page 1: In working set (due to R=1).
- Page 2: In working set (due to age $\le \tau$).
- Page 3: In working set (due to R=1).
- Page 4: Not in working set (R=0 and age > $\tau$).
ii. Indicate which page is selected for eviction, and describe how the information in the table above is updated by the algorithm.
The Working Set algorithm evicts pages that are NOT in the working set.
From part (i), Page 4 is the only page not in the working set.
Page selected for eviction: Page 4.
How information is updated by the algorithm (typically during a pass, often triggered by a page fault or periodically):
- Scan Pages: The algorithm scans through all resident pages.
- Check R Bit:
- If R=1 for a page:
- The page is considered part of the working set for this window.
- Its “Time of Last Use” is updated to the current virtual time (200ms in this case).
- The R bit for this page is cleared (set to 0). This is to detect if it’s used in the next interval.
- If R=0 for a page:
- Calculate its age (Current VT - Time of Last Use).
- If age > $\tau$: The page is not in the working set. It is a candidate for eviction. If a page fault has occurred and a page needs to be replaced, this page (or one of these pages if multiple) would be chosen.
- If age $\le \tau$: The page is in the working set. It is kept in memory.
- If R=1 for a page:
- Eviction: If a page fault occurred (as stated), and Page 4 was identified as not in the working set, it would be evicted to make space for the new page.
- M Bit: The M bit (Modified/Dirty bit) is used by the OS. If an evicted page has M=1, it means the page has been modified since it was loaded into memory, and its contents must be written back to the disk before the page frame can be reused. If M=0, the page frame can be reused immediately as its disk copy is still valid. Page 4 has M=1, so it would need to be written to disk.
Updated Table Information (after the scan at VT=200ms, and assuming Page 4 is evicted):
- For pages that were R=1 (and thus in the working set):
- Page 1: Time of Last Use becomes 200ms, R M becomes 00 (R is cleared, M was 0).
- Page 3: Time of Last Use becomes 200ms, R M becomes 01 (R is cleared, M was 1).
- For pages that were R=0 but in the working set (due to age):
- Page 2: Time of Last Use remains 100ms (as R was 0, it wasn’t just referenced this exact tick, but its age kept it in. Some implementations might update its time if it’s kept, but typically only R=1 pages get their time updated aggressively). R M remains 00.
- For the evicted page:
- Page 4: This page is removed from memory. If it were to be brought back in later due to another fault, its R and M bits would be reset (likely to 00 initially).
So, the updated state for pages remaining in memory:
Page | Time of Last Use (ms) | R M | Notes |
---|---|---|---|
1 | 200 | 00 | R bit cleared, Time updated |
2 | 100 | 00 | Kept due to age, R was already 0 |
3 | 200 | 01 | R bit cleared, Time updated, M was 1 |
(4) | (Evicted) | Was 50, 01. Would be written to disk. |
The new page brought in by the page fault would have its own entry, its Time of Last Use set to 200ms, and R bit likely set to 1 (or 0 depending on policy), M bit to 0.
Question 3: Swap two characters (25 marks)
**(a) Write a function, void swapChar (char c1, char c2), that swaps the values of two character variables.
1 |
|
Explanation:
- The function takes two pointers to characters,
char *c1
andchar *c2
. These pointers hold the memory addresses of the characters to be swapped. char temp = *c1;
: We dereferencec1
(i.e., get the character value at the addressc1
) and store it in a temporary character variabletemp
. This is necessary because if we directly assigned*c1 = *c2;
, the original value of*c1
would be lost.*c1 = *c2;
: We dereferencec2
and assign its value to the location pointed to byc1
. Now, the character variable thatc1
points to holds the original value of the characterc2
pointed to.*c2 = temp;
: We assign the value stored intemp
(which was the original value of the characterc1
pointed to) to the location pointed to byc2
.
(b) Write a function, char* initializeString(char *buf), that
- takes a string constant as input,
- dynamically allocates a string of the same length as the input,
- copies the content of the input into the dynamic string,
- returns a pointer to the dynamic string.
Hint: the function should consist of two while loops, one for determining the length of the input and the other for copying the content of the input into the newly allocated string. Do not forget to null-terminate the string at the end.
1 |
|
Explanation:
- Input Validation: Checks if the input buffer
buf
isNULL
. - Determine Length:
- A
length
variable is initialized to 0. - A temporary pointer
temp_ptr
is set tobuf
. - The first
while
loop iterates through the input stringbuf
usingtemp_ptr
until the null terminator (\0
) is encountered. In each iteration,length
is incremented.
- A
- Dynamic Allocation:
malloc((length + 1) * sizeof(char))
allocates memory forlength + 1
characters. We needlength
characters for the string content and one extra character for the null terminator.sizeof(char)
is typically 1, but it’s good practice to include it.- The result of
malloc
is cast to(char *)
. - Error checking is performed: if
malloc
fails (returnsNULL
), the function returnsNULL
.
- Copy Content:
- The second
while
loop iterates fromi = 0
up to (but not including)length
. dynamic_string[i] = buf[i];
copies each character from the input stringbuf
to thedynamic_string
.
- The second
- Null Termination:
dynamic_string[length] = '\0';
adds the null terminator at the end of the copied content in thedynamic_string
. This is crucial for it to be treated as a valid C string.
- Return Value: The function returns the pointer
dynamic_string
which points to the newly allocated and initialized string in the heap memory.
(c) Complete the following main function,
The program should print s=SC2850 and it should not leak memory when it runs.
You can assume the function definitions and the #include statements are appended on top of the file.
1 |
|
Explanation of main
completion:
char *s = initializeString(buf);
:- The
initializeString
function is called withbuf
(which is “CS2850”). s
will now point to a dynamically allocated copy of “CS2850”.
- The
- Error Check:
- Added a check
if (s == NULL)
to handle potential memory allocation failure frominitializeString
.
- Added a check
swapChar(&s[0], &s[1]);
:- The desired output is “s=SC2850”. The original string in
s
is “CS2850”. - To achieve “SC2850”, we need to swap the first character
s[0]
(‘C’) with the second characters[1]
(‘S’). swapChar
expects pointers to characters. So, we pass the addresses ofs[0]
ands[1]
using the address-of operator&
.- A check
if (s[0] != '\0' && s[1] != '\0')
is added for robustness, ensuring the string is long enough to perform the swap, though for the givenbuf
it is.
- The desired output is “s=SC2850”. The original string in
printf("s=%s\n", s);
:- This prints the string pointed to by
s
. After the swap,s
will be “SC2850”. - The output format matches “s=…” as required.
- This prints the string pointed to by
free(s);
:- Since
s
points to memory allocated bymalloc
(insideinitializeString
), this memory must be deallocated usingfree(s)
when it’s no longer needed. This prevents memory leaks, as required.
- Since
s = NULL;
:- Setting
s
toNULL
after freeing is a good defensive programming practice to prevent dangling pointer issues ifs
were accidentally used later (though not strictly required for this problem’s constraints).
- Setting
Question 4: A linked list of integers (25 marks)
The provided code snippet:
1 |
|
Corrections made to the provided snippet for analysis:
struct node head = NULL;
changed tostruct node *head = NULL;
(head should be a pointer).char c = '0';
(or'\0'
) for initialization. The snippet hadchar c=^{,10}
which is unusual. Assumechar c;
orchar c = '\0';
struct node *new = malloc...
changed tostruct node *new_node = ...
becausenew
is a keyword in C++.- Added error check for
malloc
. - Corrected
head->next; head
tohead = head->next;
for list traversal. - The line
i=i+temp->value;
in the second loop is problematic asi
is anint
andtemp->value
is afloat
. This will cause truncation. The behavior for part (e) will follow this truncation.
(a) What is the size of the structure? Why is it always better to use sizeof
instead of computing the sum of the member sizes?
Size of the structure struct node
:
The structure has three members:
int label;
(typically 4 bytes on most systems)float value;
(typically 4 bytes for single-precision float)struct node *next;
(a pointer, size depends on architecture: 4 bytes on 32-bit, 8 bytes on 64-bit systems)
So, the sum of member sizes could be:
- On a 32-bit system: 4 (int) + 4 (float) + 4 (pointer) = 12 bytes.
- On a 64-bit system: 4 (int) + 4 (float) + 8 (pointer) = 16 bytes.
However, the actual size determined by sizeof(struct node)
might be larger due to padding. Compilers may add padding bytes between members or after the last member to ensure that members are aligned to certain memory boundaries (e.g., a 4-byte integer might need to start at an address divisible by 4). This alignment can improve access speed.
Why it is always better to use sizeof
:
- Portability: The sizes of basic data types (like
int
, pointers) and the rules for padding/alignment can vary across different compilers, operating systems, and hardware architectures. Manually summing sizes makes the code non-portable.sizeof
queries the compiler, which knows the exact size on the current target system, ensuring the code works correctly everywhere. - Padding and Alignment:
sizeof
automatically accounts for any padding bytes inserted by the compiler for alignment purposes. Manual calculation would likely miss this, leading to incorrect size determination, which can cause buffer overflows or other memory corruption issues when allocating memory or performing pointer arithmetic. - Maintainability: If the structure definition changes (e.g., a member is added, removed, or its type changes), code using
sizeof
will automatically adapt. Code with manually calculated sizes would need to be found and updated, which is error-prone. - Readability and Correctness: Using
sizeof
clearly expresses the intent of getting the actual size of the type or variable and is less prone to calculation errors than manual summing.
(b) What is the difference between \0
and 0
? Why do you need to subtract 0
in new->value = c - '0'
?
Difference between \0
and '0'
:
\0
(Null Character):- This is a character literal representing the null character (also called null terminator).
- Its ASCII (or UTF-8) value is 0.
- It is primarily used to mark the end of strings in C. Functions that operate on strings (like
strcpy
,strlen
,printf %s
) rely on this null character to know where the string data ends. - In terms of integer value,
\0
is equivalent to the integer0
.
'0'
(Character Zero):- This is a character literal representing the digit character ‘0’.
- Its ASCII value is typically 48 (this can vary on non-ASCII systems, but is standard on most).
- It is a printable character.
Why subtract '0'
in new->value = c - '0'
:
The line if (c <= '9' && c >= '0')
checks if the character c
(read by getchar()
) is a digit character (from ‘0’ through ‘9’).
- Character variables in C store ASCII values (or values from the system’s character set). For example, if
c
is ‘1’, its ASCII value is 49. Ifc
is ‘7’, its ASCII value is 55. - The goal is to convert the digit character into its corresponding integer value. For instance, if the user types ‘1’, we want to store the integer 1.0 in
new->value
. - In ASCII (and compatible character sets), the digit characters ‘0’ through ‘9’ are represented by consecutive integer codes.
- ‘0’ has ASCII value X
- ‘1’ has ASCII value X + 1
- ‘2’ has ASCII value X + 2
- …
- ‘9’ has ASCII value X + 9
(For ASCII, X=48).
- So,
c - '0'
performs arithmetic on the character codes.- If
c
is ‘1’ (ASCII 49) and'0'
is ASCII 48, then'1' - '0'
is49 - 48 = 1
. - If
c
is ‘7’ (ASCII 55) and'0'
is ASCII 48, then'7' - '0'
is55 - 48 = 7
.
- If
- This subtraction effectively converts the ASCII representation of a digit character into its numerical integer equivalent. This integer is then assigned to the
float
variablenew->value
(which will be, e.g., 1.0, 7.0, etc.).
(c) Why are the nodes printed from the last to the first? Why does the second while loop end after the program prints the node with the label 0?
Why nodes are printed from last to first:
The nodes are printed from the last input digit to the first input digit because of how the linked list is constructed:
new_node->next = head;
head = new_node;
This sequence of operations adds each new node to the front of the list.
- Initially,
head
isNULL
. - When the first digit is read (e.g., ‘1’ in “1423”), a node is created for it.
new_node->next
becomesNULL
, andhead
points to this new node. List: Node(1) -> NULL. - When the second digit is read (e.g., ‘4’), a new node is created.
new_node->next
is set to the currenthead
(which points to Node(1)). Then,head
is updated to point to this newest node. List: Node(4) -> Node(1) -> NULL. - This continues for all input digits. If the input is “1423”, the list becomes: Node(3) -> Node(2) -> Node(4) -> Node(1) -> NULL. (Node(3) was the last char entered).
The printing loop (while (head) { ... head = head->next; }
) traverses the list starting from the head
and following the next
pointers. Since the head
always points to the most recently added node (which corresponds to the last character entered by the user before newline/EOF), the list is printed in reverse order of input.
Why the second while
loop ends after printing the node with label 0:
The label
field is assigned sequentially starting from 0 and incrementing (new_node->label = i; i++;
).
- The first node created (corresponding to the first digit entered by the user) gets
label = 0
. - The second node created gets
label = 1
, and so on.
Since new nodes are added to the front of the list, the node with label = 0
will be the last node in the linked list (the one whose next
pointer is NULL
, assuming at least one digit was entered).
The printing loop is while (head)
. This loop continues as long as head
is not NULL
.
Inside the loop:
printf("label=%d, ...", head->label);
prints the current node’s data.struct node *temp = head;
head = head->next;
advanceshead
to the next node in the list.free(temp);
frees the node that was just printed.
When head
points to the node with label = 0
(which is the original first node entered, now at the end of the list being traversed):
- Its data is printed.
temp
is set to this node.head
is updated tohead->next
. Since this is the last node, itsnext
field isNULL
. So,head
becomesNULL
.- The node (pointed to by
temp
) is freed.
After this iteration, the conditionwhile (head)
(which is nowwhile (NULL)
) becomes false, and the loop terminates. Thus, the loop ends after processing and printing the node that hadlabel = 0
.
(d) How many dynamic allocations does the program perform? How many times does it call free?
Dynamic Allocations (
malloc
):
A dynamic allocation (malloc(sizeof(struct node))
) is performed once for each valid digit character entered by the user before a newline or EOF. Theif (c <= '9' && c >= '0')
condition ensures this.
So, if the user entersN
digit characters,malloc
is calledN
times.free
Calls:
The secondwhile (head)
loop iterates through each node in the list and callsfree(temp)
for each node. This loop continues untilhead
becomesNULL
, meaning all nodes that were added to the list are freed.
So, ifN
nodes were allocated and added to the list,free
will be calledN
times.
Therefore, the program performs N
dynamic allocations and N
calls to free
, where N
is the number of digit characters entered by the user. This means that, assuming malloc
always succeeds, the program should not leak memory from the linked list nodes themselves.
(e) Can you predict the output of the last call of printf if you assume the user enters 1423\n
on the terminal after the program has started? Is the argument of printf correct?
User input: 1423\n
Execution Trace:
First
while
loop (reading input and building list):i
starts at 0.head
isNULL
.c = '1'
:new_node
allocated.new_node->label = 0
,new_node->value = '1' - '0' = 1.0
.i
becomes 1.new_node->next = NULL
.head
points to this node.- List: Node(label=0, value=1.0) -> NULL
c = '4'
:new_node
allocated.new_node->label = 1
,new_node->value = '4' - '0' = 4.0
.i
becomes 2.new_node->next
points to Node(label=0).head
points to this new node.- List: Node(label=1, value=4.0) -> Node(label=0, value=1.0) -> NULL
c = '2'
:new_node
allocated.new_node->label = 2
,new_node->value = '2' - '0' = 2.0
.i
becomes 3.new_node->next
points to Node(label=1).head
points to this new node.- List: Node(label=2, value=2.0) -> Node(label=1, value=4.0) -> Node(label=0, value=1.0) -> NULL
c = '3'
:new_node
allocated.new_node->label = 3
,new_node->value = '3' - '0' = 3.0
.i
becomes 4.new_node->next
points to Node(label=2).head
points to this new node.- List: Node(label=3, value=3.0) -> Node(label=2, value=2.0) -> Node(label=1, value=4.0) -> Node(label=0, value=1.0) -> NULL
c = '\n'
: First loop terminates. At this point,i = 4
.
Second
while
loop (printing, summing intoi
, and freeing):- Current
head
points to Node(label=3, value=3.0).i
is currently 4. - Iteration 1:
- Prints “label=3, value=3.000000”
temp
points to Node(label=3).head
moves to Node(label=2).i = i + temp->value;
=>i = 4 + 3.0;
(float 3.0 is converted to int 3 for addition, or result truncated) =>i = 4 + 3 = 7
.free(temp)
.
- Iteration 2:
head
points to Node(label=2, value=2.0).i
is 7.- Prints “label=2, value=2.000000”
temp
points to Node(label=2).head
moves to Node(label=1).i = i + temp->value;
=>i = 7 + 2.0;
=>i = 7 + 2 = 9
.free(temp)
.
- Iteration 3:
head
points to Node(label=1, value=4.0).i
is 9.- Prints “label=1, value=4.000000”
temp
points to Node(label=1).head
moves to Node(label=0).i = i + temp->value;
=>i = 9 + 4.0;
=>i = 9 + 4 = 13
.free(temp)
.
- Iteration 4:
head
points to Node(label=0, value=1.0).i
is 13.- Prints “label=0, value=1.000000”
temp
points to Node(label=0).head
moves toNULL
.i = i + temp->value;
=>i = 13 + 1.0;
=>i = 13 + 1 = 14
.free(temp)
.
head
is nowNULL
. Second loop terminates.i
is 14.
- Current
Last
printf
:printf("i=%d\n", i);
- This will print the final value of
i
, which is 14.
Predicted output of the last call of printf
:i=14
Is the argument of printf
correct?
The last printf
is printf("i=%d\n", i);
.
- The format specifier is
%d
, which expects an argument of typeint
. - The variable
i
is declared asint i;
. - Throughout the code,
i
is used as an integer. The operationi = i + temp->value;
involves adding afloat
(temp->value
) to anint
(i
). In C, when afloat
is added to anint
and the result is assigned back to anint
, thefloat
value is typically truncated to its integer part before the addition (or the sum is truncated if the addition happens as float then assigned to int). For example,i = (int)( (float)i + temp->value )
. Giventemp->value
are whole numbers like 1.0, 2.0, etc.,(int)temp->value
will be 1, 2, etc. So, the calculationi = i + (int)temp->value
is effectively what happens.
Yes, the argument i
is of type int
, which matches the %d
format specifier. So, in terms of type matching, the argument is correct.
The meaning of i
at the end is the sum of its initial value after the first loop (which was the count of digits, 4) and the integer parts of the digits entered (1+4+2+3 = 10). So, 4 + 10 = 14. If the intent was to print the simple sum of the digits entered (1+4+2+3=10), then the logic for updating i
is a bit convoluted as it incorporates its previous value (the count). If the intent was just to print the count, it should have been printed before the second loop or stored in another variable. However, given the code structure, i=14
is the correct output.
This concludes the detailed answers to the exam paper.