Lab 4 - Signals

Class: CSCE-313


Notes:

Instructions

In this lab, you will enhance the banking system from previous labs by adding signal handling capabilities. Signals are software interrupts that allow processes to handle asynchronous events. You'll implement handlers for various signals to make the system more robust and responsive to events like user interrupts (Ctrl+C), timeouts, and child process termination.

What are Signals?

Signals are notifications sent to a process to notify it of a particular event. These events might be:

When a signal arrives, the process can:

  1. Handle the signal with a custom handler
  2. Ignore the signal
  3. Allow the default action to occur

Starter Code

The provided code builds upon the banking application:

This system consists of a Client (parent) that manages three Servers (children) via Named Pipes (FIFOs).

  1. Process Logic (client.cpp)

    • Startup: The client uses fork() and execvp() to launch ./finance, ./logging, and ./file.
    • Communication: It creates three RequestChannel objects. Every time a user picks a menu option, the client sends a Request and waits for a Response.
    • Shutdown: It sends a QUIT request to each server and then uses wait() to clean up the child processes.
  2. The Communication Layer (channel.cpp)

    • This file handles the actual "speaking" between processes.
    • Crucial for Lab 4: The functions send_request and receive_request both call wait_with_timeout(). This is where your SIGALRM logic will be triggered. If your signal handler sets the timeout_occurred flag correctly, these functions will stop waiting and return an error to the user.
  3. The Signal Controller (signals.cpp)
    This is your primary workspace. It acts as the "Event Listener" for the entire system:

    • shutdown_requested: Set by SIGINT. Tells the while loop in client.cpp to stop and start the cleanup.
    • timeout_occurred: Set by SIGALRM. Tells the RequestChannel to stop waiting for a server that is taking too long.
    • server_processes Vector: A registry of the PIDs of the servers. When SIGCHLD fires, you use this list to see which specific server (finance, file, or logging) has stopped.
  4. The Server Files (finance.cpp, file.cpp, logging.cpp)

    • These are infinite while(true) loops.
    • They wait for a Request, process it, and send back a Response.
    • They exit only when they receive a Request of type QUIT.

How it all connects:

Event Signal Handler Action System Result
User hits Ctrl+C SIGINT Set shutdown_requested Client finishes current loop and exits gracefully.
The server is slow SIGALRM Set timeout_occurred channel.cpp stops waiting and returns an error.
Server process dies SIGCHLD Update Registry Server Status menu option shows "TERMINATED".

Code Structure

lab3/
├── signals.h              # Signal handling declarations 
├── signals.cpp            # Signal handler implementations 
├── finance.cpp            # Financial transaction server 
├── file.cpp               # File operations server 
├── logging.cpp            # Logging server 
├── client.cpp             # Main client program 
├── common.h               # Common declarations 
├── common.cpp             # Common implementations 
├── channel.h              # IPC channel declarations 
├── channel.cpp            # IPC channel implementations 
├── test_signals.cpp       # Signal handling tests 
└── Makefile               # Build system

Objectives

**After completing this lab, you will understand:

In signals.cpp:

Here, you can now get an idea of implementing signal handlers and other functions used to block/unblock signals, etc., by implementing them yourself. The detailed information about each of these functions can be found in the code comments. The following TODOs are to be implemented in signals.cpp file :

Atomic Flags Declaration

Declare atomic variables for shutdown_requested (boolean), timeout_occurred (boolean), and child_exited (integer) to track the system state. These atomic variables ensure thread-safe operations when dealing with asynchronous signals.

Refer to this manuahttps://en.cppreference.com/w/cpp/atomic/atomicl page to understand more about them.

Signal Handler Setup

Implement a robust signal handling mechanism using the sigaction structure. Signal handlers are crucial for managing asynchronous events in your application. Begin by carefully initializing the sigaction structure for three specific signals: SIGINT, SIGALRM, and SIGCHLD.

For the SIGINT and SIGALRM handlers, use sigaction with default settings, meaning you'll set no specific flags. These handlers are typically used to manage interrupt signals (like Ctrl+C) and timer-related events.

The SIGCHLD handler requires special attention. Use the SA_RESTART flag for this handler, which is designed to automatically restart system calls interrupted by the child process signal. This flag is particularly useful when dealing with child processes, as it prevents system calls from failing due to signal interruptions. The SIGCHLD signal is sent to the parent process when a child process terminates, stops, or is resumed, making it crucial for process management.

Refer to this simple signal handler code in C++ to understand more about the implementation.

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/file.h>

// signal handler
void signal_handler(int s)
{
    std::cout << "Caught signal: " << s << std::endl;
    exit(1);
}

// entry
int main(int argc, char* argv[])
{
    // setup signal handler
    struct sigaction sigIntHandler;

    sigIntHandler.sa_handler = signal_handler;
    sigemptyset(&sigIntHandler.sa_mask);
    sigIntHandler.sa_flags = 0;
    sigaction(SIGINT, &sigIntHandler, NULL);

    std::cout << "Press Ctrl+C to exit." << std::endl;

    for (;;) {
        sleep(1);
    }

    return 0;
}

SIGINT Handler Implementation

The SIGINT handler is responsible for managing the application's graceful shutdown mechanism when a user interrupts the program (typically via Ctrl+C). Implement a two-stage shutdown process that provides users with control over the termination sequence. On the first SIGINT signal, initiate a graceful shutdown by setting a predefined shutdown flag. Use only signal-safe functions for logging and communication to prevent potential race conditions or undefined behavior.

When the first SIGINT is received, set the shutdown flag, which should prevent new operations from starting and prepare the application for a clean exit. If a second SIGINT is received before the graceful shutdown completes, immediately force the application to exit. Ensure all print statements are done using signal-safe functions like write() instead of printf() or cout.

SIGALRM Handler Implementation

The SIGALRM handler is crucial for managing operation timeouts in the application. When a SIGALRM signal is received, immediately set a timeout flag to indicate that an operation has exceeded its allocated time. Use signal-safe functions to record the event for the user.

This handler should focus on marking the timeout state, allowing the main application logic to respond appropriately. The timeout flag serves as a communication mechanism between the signal handler and the main application thread, signaling that a time-sensitive operation has failed to complete within the expected timeframe.

SIGCHLD Handler Implementation

The SIGCHLD handler is responsible for managing child process termination. It is a robust handler that uses waitpid() with the WNOHANG flag to non-blockingly retrieve information about terminated child processes. This approach prevents the handler from blocking and allows multiple child process status checks in a single call.

For each terminated child process, the critical task is updating the server registry when a child process terminates. Iterate through the server processes vector to find the matching process, update its status, and log the termination event as per the comments given in the code.

Signal Blocking Implementation

Signal blocking is a critical mechanism for protecting critical sections of code from interruption. The block_signals() function should create a signal set using sigset_t and carefully block specific signals to prevent unexpected interruptions during sensitive operations. Focus on blocking only the SIGINT signal, which can potentially disrupt critical processes.

When implementing signal blocking, initialize the signal set using sigemptyset() to clear any existing signals and use the sigaddset() function to add signals to the block set, and then apply the block using sigprocmask(). This ensures that the specified signals are temporarily prevented from interrupting the current execution context.

The blocking mechanism should be used sparingly and for short durations to prevent prolonged signal suppression, which could make the application less responsive. Ensure that signals are unblocked as soon as the critical section is complete to maintain the application's responsiveness and ability to handle asynchronous events.

Signal Unblocking Implementation

The unblock_signals() function serves as the counterpart to signal blocking, restoring the normal signal handling behavior. Create a signal set similar to the blocking process, but use sigprocmask() to remove the previous signal blocks. This function is crucial for returning the application to a state where it can receive and process signals normally.

Timeout Mechanism Implementation

The wait_with_timeout() function introduces a robust timeout mechanism for operations that might potentially hang or take too long. Use the alarm() system call to set a timer for the specified number of seconds. This creates a race condition handler that prevents indefinite waiting on potentially stuck operations.

Timeout Cancellation Implementation

The cancel_timeout() function provides a way to cancel any pending alarm explicitly. Again use the alarm() system call to cancel any existing alarm without setting a new one. This is particularly useful when an operation completes successfully before the timeout, preventing unnecessary signal generation.

Error Checking Requirement:

Wherever required for system calls, check the return values. If the return value is -1, use perror() to print the specific error message and handle the failure appropriately. Proper error checking is crucial for creating robust and reliable system-level applications.

In the client.cpp

There are only a few changes in the client file, i.e., to register child processes after their creation with the signal handlers, and blocking signals before entering the critical section, and immediately unblocking them after the critical section transaction is completed. Watch out for the code comments in the file to understand more about the changes to be done.

The other files in the system (servers, common.h, channel code) are complete and provide the infrastructure your signal handling code will interact with. Focus your efforts on implementing the TODOs in signals.cpp and client.cpp while maintaining the existing functionality of the banking system.

Important System Limitations

Server Operation Behavior

The current implementation has some important limitations that you should be aware of:

  1. Non-blocking Timeouts:
    • While the client implements timeouts for operations, the server processes continue executing their operations even after a timeout occurs
    • For example, if a deposit operation is timed out after 30 seconds but the server takes 40 seconds to process it, the deposit will still occur
    • There is no rollback mechanism implemented in the servers
  2. Transaction Consistency:
    • Due to the above behavior, you might observe that:
    • A "timed out" deposit might still appear in your balance
    • A "failed" operation might actually succeed on the server side
    • The log file might show operations that the client reported as timed out
  3. Why This Happens:
    • The servers are implemented to process requests fully once received
    • Timeout signals only affect the client's waiting period
    • Server operations are not designed to be interruptible
    • Implementing true distributed transaction rollback would require significant additional complexity

Tasks

Implement Signal Handlers (50 points)

In signals.cpp, implement the following:

Signal Blocking/Unblocking in Client (20 points)

In signals.cpp, implement the following:

Implement Timeout Management (15 points)

In signals.cpp, implement the following functions:

Server Registration with Signal Handlers (5 points)

In the client.cpp, implement the following:

Signal Blocking Around Transactions (10 points)

In the client.cpp, implement the following:

Implementation

In signals.cpp

Atomic flags

std::atomic<bool> shutdown_requested(false);
std::atomic<bool> timeout_occurred(false);
std::atomic<int> child_exited(0);

So that section becomes:

namespace SignalHandling {

    std::atomic<bool> shutdown_requested(false);
    std::atomic<bool> timeout_occurred(false);
    std::atomic<int> child_exited(0);
    
    // Server process registry
    std::vector<ServerProcess> server_processes;

What this is doing:

Why we write them here in signals.cpp:

signals.h

#ifndef _SIGNALS_H_
#define _SIGNALS_H_

#include <signal.h>
#include <atomic>
#include <string>
#include <vector>
#include <sys/types.h>
#include <iostream>

namespace SignalHandling {
    // Signal flags (using std::atomic for thread safety)
    extern std::atomic<bool> shutdown_requested;
    extern std::atomic<bool> timeout_occurred;
    extern std::atomic<int> child_exited;

    // Server process tracking
    struct ServerProcess {
        pid_t pid;
        std::string name;
        bool active;
    };

    extern std::vector<ServerProcess> server_processes;

    // Signal handlers
    void setup_handlers();
    void sigint_handler(int sig);
    void sigalrm_handler(int sig);
    void sigchld_handler(int sig);

    // Signal operations
    void block_signals();
    void unblock_signals();
    bool wait_with_timeout(int seconds);
    void cancel_timeout();

    // Server management
    void register_server(pid_t pid, const std::string& name);
    bool is_server_active(const std::string& name);
    void print_server_status();

    // Logging
    void log_signal_event(const std::string& message);
}

// Helper template for executing functions with timeout
template<typename Func>
bool execute_with_timeout(Func operation, int timeout_seconds) {
    SignalHandling::timeout_occurred = false;

    // Set alarm
    alarm(timeout_seconds);

    bool result = operation();

    // Cancel alarm
    alarm(0);
    return result && !SignalHandling::timeout_occurred;
}

#endif

Why std::atomic:

Why the initial values are these:

One important distinction:

Signal Handler Setup

The lab specifically says to use sigaction for SIGINT, SIGALRM, and SIGCHLD, with SA_RESTART only for SIGCHLD

void setup_handlers() {

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);

    sa.sa_handler = sigint_handler;
    sa.sa_flags = 0;
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction SIGINT");
    }

    sa.sa_handler = sigalrm_handler;
    sa.sa_flags = 0;
    if (sigaction(SIGALRM, &sa, NULL) == -1) {
        perror("sigaction SIGALRM");
    }

    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction SIGCHLD");
    }

    log_signal_event("Signal handlers initialized");
}

What sigaction is doing?

So this line pattern:

sigaction(SIGINT, &sa, NULL);

means:

So setup_handlers() is basically saying:

Why we reuse the same struct sigaction sa?

You start with:

struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);

So you start from a clean base configuration.

Then for each signal, you update the two important fields:

and call sigaction(...).

Handlers

First handler: SIGINT

sa.sa_handler = sigint_handler;
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
    perror("sigaction SIGINT");
}

Second handler: SIGALRM

sa.sa_handler = sigalrm_handler;
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL) == -1) {
    perror("sigaction SIGALRM");
}

Third handler: SIGCHLD

sa.sa_handler = sigchld_handler;
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
    perror("sigaction SIGCHLD");
}

After setup_handlers() runs, the program is now prepared for asynchronous events:

Implement SIGINT handler

The lab wants a two-stage shutdown:

The lab also explicitly says:

Use this implementation:

void sigint_handler(int sig) {
    (void)sig;  // suppress unused parameter warning

    const char first_msg[] =
        "SIGINT received - initiating graceful shutdown\n";
    const char second_msg[] =
        "Second SIGINT received - forcing exit\n";

    if (!shutdown_requested) {
        // First SIGINT - Graceful termination
        shutdown_requested = true;

        write(STDOUT_FILENO, first_msg, sizeof(first_msg) - 1);
        log_signal_event("SIGINT received - initiating graceful shutdown");
    } else {
        // Second SIGINT - force exit
        write(STDOUT_FILENO, second_msg, sizeof(second_msg) - 1);
        log_signal_event("Second SIGINT received - forcing exit");
        _exit(1);
    }
}

Why the handler has this form

The function is:

void sigint_handler(int sig)

Why we do (void)sig;

(void)sig;

Why the messages are stored as const char[]

const char first_msg[] =
    "SIGINT received - initiating graceful shutdown\n";
const char second_msg[] =
    "Second SIGINT received - forcing exit\n";

The key condition: first Ctrl+C or second Ctrl+C

if (!shutdown_requested) {

Why we print with write() instead of cout or printf

write(STDOUT_FILENO, first_msg, sizeof(first_msg) - 1);

Why not cout or printf?

Breaking that line down:

That last part matters because write() needs the exact byte count.

Logging the event

log_signal_event("SIGINT received - initiating graceful shutdown");

Why use _exit(1) instead of exit(1)

Inside a signal handler, _exit() is safer than exit().

exit()

_exit()

Since this is happening inside a signal handler, _exit(1) is the better choice.

Implement SIGALRM handler

The purpose of SIGALRM here is just:

The lab explicitly says the handler should set timeout_occurred and log "SIGALRM received - operation timed out". It also explains that this flag is how the main application knows an operation took too long.

Use this implementation:

void sigalrm_handler(int sig) {
    (void)sig;  // suppress unused parameter warning
	
    const char msg[] = "SIGALRM received - operation timed out\n";
	
    timeout_occurred.store(true);
	
    write(STDOUT_FILENO, msg, sizeof(msg) - 1);
    log_signal_event("SIGALRM received - operation timed out");
}

What this handler is for

SIGALRM is sent when an alarm timer expires.

So somewhere else in your code, there will be logic like:

alarm(seconds);

If the operation does not finish before that timer expires, the OS sends SIGALRM, and this handler runs.

This handler does not directly cancel the operation or stop the server. It just records:

That matches the lab’s design: the signal handler communicates the timeout through the shared atomic flag, and the normal program logic reacts afterward.

Setting the timeout flag

timeout_occurred = true;

This is the core action of the handler.

You are telling the rest of the program:

This is exactly what the lab wants. The timeout flag is the communication bridge between the asynchronous signal handler and the regular application logic.

Why this handler is intentionally small

A signal handler should do as little as possible.

That is especially true here, because SIGALRM is just meant to notify the application that time is up.

So the good design is:

Then the normal program flow can check timeout_occurred and decide what to do next.

Implement SIGCHLD handler

It keeps your server registry accurate when one of the child server processes dies.

Use this implementation inside the while loop:

void sigchld_handler(int sig) {
    (void)sig;  // suppress unused parameter warning
	
    int status;
    pid_t pid;
    
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        child_exited++;
        
        // Update server registry
        for (auto& server : server_processes) {
            if (server.pid == pid) {
                server.active = false;
				
                std::stringstream ss;
                ss << "Child process terminated: " << server.name
                   << " (PID: " << pid << ")";
                log_signal_event(ss.str());
				
                break;
            }
        }
    }
}

What SIGCHLD means

SIGCHLD is sent to the parent process whenever one of its child processes changes state, especially when it terminates.

In your lab, the parent is the client, and the children are the three servers:

The lab explains that when a server dies, the SIGCHLD handler should update the registry so the Server Status menu can show that server as "TERMINATED".

So this handler’s job is:

Why we use waitpid(-1, &status, WNOHANG)

This line is the heart of the handler:

while ((pid = waitpid(-1, &status, WNOHANG)) > 0)

Let’s break it apart.

waitpid(...)

Why the while loop is needed

You might wonder: why not just call waitpid() once?

Because one SIGCHLD signal can correspond to more than one child being ready to reap.

So the loop:

while ((pid = waitpid(-1, &status, WNOHANG)) > 0)

means:

This is what the lab means when it says the handler should allow multiple child process status checks in a single call. 

Why child_exited++

This increments your atomic counter every time you successfully reap one terminated child.

So if:

This gives the program a running count of how many child processes have terminated.

Why we loop through server_processes

Your server_processes vector stores records like: (this is in signals.h)

struct ServerProcess {
    pid_t pid;
    std::string name;
    bool active;
};

So each server has:

When waitpid() returns a PID, you need to determine:

That is why you do:

for (auto& server : server_processes) {

and then compare:

if (server.pid == pid)

That is the match.

Marking the server as terminated

Once you find the matching server:

server.active = false;

This is the actual registry update.

Then later, when print_server_status() runs, it will print:

So this one assignment is what makes the server-status feature work correctly.

Logging the event message

The lab says the log must use exactly this format:

Child process terminated: <server-name> (PID: <pid>)

So we build it like this:

std::stringstream ss;
ss << "Child process terminated: " << server.name
   << " (PID: " << pid << ")";
log_signal_event(ss.str());

If the finance server with PID 12345 dies, the message becomes:

Child process terminated: finance (PID: 12345)

Why break; is important

After you find the matching server and update it:

break;

You should stop the for loop because:

This makes the code cleaner and avoids accidental repeated work.

What happens if no server matches the PID?

In normal lab behavior, every child server should be registered with:

register_server(pid, name);

So there should usually be a match.

If there is no match:

That is okay.
It just means the process was not in your registry.

One important system note

Strictly speaking, inside a real production-grade signal handler, using things like:

is not considered fully async-signal-safe.

But for this lab, the starter design clearly expects you to:

So for the assignment, this is the correct implementation style.

Implement Signal Blocking

The lab is very specific here: block_signals() should build a sigset_t, add only SIGINT, and then apply the block with sigprocmask(). It also says to log the exact message "Signals blocked for critical section".

Use this:

void block_signals() {
    sigset_t set;

    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return;
    }

    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return;
    }

    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask SIG_BLOCK");
        return;
    }

    log_signal_event("Signals blocked for critical section");
}

What signal blocking means

Normally, if the user presses Ctrl+C, your process can receive SIGINT immediately.

But sometimes the program is in a critical section, meaning:

So block_signals() temporarily says:

The lab explicitly says this function is for protecting critical sections from interruption, and that you should focus on blocking only SIGINT.

Why we create sigset_t

sigset_t set;

A sigset_t is just a set of signals.

Think of it like a container that can hold:

In this function, we only want the set to contain SIGINT.

Why sigemptyset(&set) comes first

if (sigemptyset(&set) == -1) {
    perror("sigemptyset");
    return;
}

This initializes the signal set so it starts empty.

That is important because you do not want garbage or old signal values inside it.

After this line, the set contains:

Then you explicitly add the signals you want.

Why sigaddset(&set, SIGINT)

if (sigaddset(&set, SIGINT) == -1) {
    perror("sigaddset");
    return;
}

This adds SIGINT to the set.

So now the set contains exactly:

Why sigprocmask(SIG_BLOCK, &set, NULL)

if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
    perror("sigprocmask SIG_BLOCK");
    return;
}

This is the system call that actually changes the process signal mask.

Why this protects the critical section

Suppose the program is doing something delicate, like:

If SIGINT were delivered right in the middle, your graceful shutdown could begin at an awkward moment.

By blocking SIGINT during that short window, you reduce the chance of leaving the operation in an inconsistent state.

That is exactly why the lab says this mechanism should be used around critical sections and only for short durations

Why we check for -1 and use perror()

The lab says that whenever system calls require error checking, you should test for -1 and use perror().

That is why each step is guarded:

If one fails, perror() prints the OS error reason, and return; exits the function early.

Implement Signal Unblocking

Use the mirror-image of block_signals():

void unblock_signals() {
    sigset_t set;

    if (sigemptyset(&set) == -1) {
        perror("sigemptyset");
        return;
    }

    if (sigaddset(&set, SIGINT) == -1) {
        perror("sigaddset");
        return;
    }

    if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
        perror("sigprocmask SIG_UNBLOCK");
        return;
    }

    log_signal_event("Signals unblocked");
}

What each part is doing:

Why this matters:

Implement Timeout Mechanism

For this TODO, the implementation is very small. You just need to clear the timeout flag and start the alarm.

Use this:

bool wait_with_timeout(int seconds) {
    timeout_occurred = false;
	
    alarm(seconds);
	
    return true;
}

What this is doing

Reset the timeout flag

timeout_occurred.store(false);

Start the timer

alarm(seconds);

Why it still returns true

return true;

Why this is called a timeout mechanism

Without a timeout, an operation might:

By setting an alarm, you create a limit:

Important note

alarm(seconds) sets a process-wide timer:

And if later you want to cancel the alarm after the operation completes successfully, that is usually done with:

alarm(0);

That cancels any pending alarm.

So in practice the pattern is often:

wait_with_timeout(5);
/* do operation */
alarm(0);   // cancel timer if operation finished in time

Implement Timeout Cancellation

You just need to cancel any existing alarm timer.

Use this:

void cancel_timeout() {
    alarm(0);
}

How alarm() works

How to cancel an alarm

The POSIX rule is:

alarm(0);

means:

So if an operation finishes successfully before the timeout:

This is exactly what the lab description is referring to when it says this prevents unnecessary signal generation.

In client.cpp

Register finance/logging/file server with signal handler

Add this right after the if (pid == 0) { ... } block:

SignalHandling::register_server(pid, "finance");

So it becomes:

// Start finance server
pid_t pid = fork();
if (pid < 0) {
    perror("Fork failed");
    exit(1);
}
if (pid == 0) { // Child process
    char* args[] = {(char*)"./finance", (char*)"-m", (char*)to_string(max_account).c_str(), nullptr};
    execvp(args[0], args);
    perror("Execvp failed");
    exit(1);
}

// Register finance server with signal handler
SignalHandling::register_server(pid, "finance");

Why this is needed

The lab says that in client.cpp you must register all servers with the signal handlers after they are created using fork() and exec().

Also, the signal system uses a server_processes vector as a registry of the server PIDs so that when SIGCHLD happens, it can determine which specific server died and update its status.

So this one line is what connects:

What register_server(pid, "finance") does

From signals.cpp, register_server does this:

Conceptually, it adds something like:

{ pid_of_finance_server, "finance", true }

That means the program now knows:

Why it must happen in the parent, not the child

Notice that you place this line after the child if (pid == 0) block.

That means only the parent executes it.

That is correct, because:

The child should not register itself in the parent's registry.

Why we use pid

After fork():

So when execution reaches this line in the parent:

SignalHandling::register_server(pid, "finance");

The next two TODOs for logging and file servers will almost certainly be the exact same pattern:

SignalHandling::register_server(pid, "logging");
SignalHandling::register_server(file_pid, "file");

Implement signal blocking before transaction

The idea is:

So the code should become:

    SignalHandling::block_signals();
	
    retry_operation("login", login_operation);
	
    SignalHandling::unblock_signals();

What this is trying to protect

The lab wants signal blocking around a critical section.

Here, the critical section is the actual login transaction:

retry_operation("login", login_operation);

Why is this sensitive?

If SIGINT arrives right in the middle, the transaction could be interrupted at an awkward time.

So the purpose of blocking is:

Implement signal blocking/unblocking before transaction

In Deposit, the code should become:

    SignalHandling::block_signals();
	
    retry_operation("deposit", deposit_operation);
	
    SignalHandling::unblock_signals();

In Withdraw, the code should be:

    SignalHandling::block_signals();
	
    retry_operation("withdrawal", withdraw_operation);
	
    SignalHandling::unblock_signals();

In View Balance, the code becomes:

    SignalHandling::block_signals();
	
    retry_operation("balance check", balance_operation);
	
    SignalHandling::unblock_signals();

In Upload File, the code becomes:

    SignalHandling::block_signals();
	
    retry_operation("file upload", upload_operation);
	
    SignalHandling::unblock_signals();

In Download File, the code becomes:

    SignalHandling::block_signals();
	
    retry_operation("file download", download_operation);
	
    SignalHandling::unblock_signals();

In Logout, the code becomes:

    SignalHandling::block_signals();
	
    retry_operation("logout", logout_operation);
	
    SignalHandling::unblock_signals();