Skip to main content

What Are UNIX Signals?

Signals are software interrupts that provide a way for processes to communicate with each other in UNIX-like operating systems. In Minitalk, we use two specific signals to transmit binary data:
  • SIGUSR1: User-defined signal 1 (represents binary 0)
  • SIGUSR2: User-defined signal 2 (represents binary 1)
These signals are called “user-defined” because they have no predefined meaning in the operating system. We can assign them any purpose we want, making them perfect for custom IPC protocols.

Signal Handler Implementation

The server registers signal handlers to respond to incoming signals. Here’s the actual implementation from server.c:38-39:
signal(SIGUSR1, handle_sigusr);
signal(SIGUSR2, handle_sigusr);
This tells the operating system: “Whenever this process receives SIGUSR1 or SIGUSR2, call the handle_sigusr function.”

The Signal Handler Function

Here’s the complete signal handler from server.c:16-32:
void	handle_sigusr(int sigsent)
{
	static size_t	c = 0;
	static size_t	i = 0;

	if (sigsent == SIGUSR1)
		c = c << 1;
	else if (sigsent == SIGUSR2)
		c = (c << 1) | 1;
	i++;
	if (i == 8)
	{
		ft_printf("%c", c);
		i = 0;
		c = 0;
	}
}
The static keyword means these variables persist between function calls:
  • c: Accumulates the bits of the current character being received
  • i: Counts how many bits have been received (0-7)
Without static, these would reset to 0 every time the function is called, losing all progress.

How Signals Enable IPC

The key to signal-based IPC is the kill() system call, which sends signals between processes. Despite its intimidating name, kill() is just the standard way to send signals.

Sending Signals from the Client

From client.c:53-62, here’s how the client sends each bit:
if ((str[i] >> bits) & 1)
{
	if (kill(pid, SIGUSR2) == -1)
		return (1);
}
else
{
	if (kill(pid, SIGUSR1) == -1)
		return (1);
}
The kill() function takes two parameters:
  1. pid: The process ID of the target process (the server)
  2. signal: Which signal to send (SIGUSR1 or SIGUSR2)
It returns -1 if the signal couldn’t be sent (e.g., invalid PID or insufficient permissions).

The Complete IPC Flow

  1. Server starts and registers signal handlers (server.c:38-39)
  2. Server enters pause loop (server.c:40-43), waiting for signals
  3. Client sends signal using kill(pid, SIGUSR1) or kill(pid, SIGUSR2)
  4. Operating system delivers signal to the server process
  5. Signal handler executes (handle_sigusr function)
  6. Server returns to pause, waiting for the next signal
Signals are asynchronous! The server doesn’t poll or check for signals - the operating system interrupts the server’s execution when a signal arrives. This is why we use pause() to efficiently wait.

Why Signals for Minitalk?

Advantages

  • Lightweight: No need for pipes, sockets, or shared memory
  • Simple: Just two signal types make the protocol easy to understand
  • Educational: Teaches fundamental UNIX IPC concepts

Limitations

  • Slow: Each signal requires a system call and context switch
  • No data payload: Signals carry no data, only their type
  • Unreliable: Signals can be lost if sent too quickly (hence the usleep())

Key Takeaways

SIGUSR1 = Bit 0

When the client needs to send a 0 bit, it uses kill(pid, SIGUSR1)

SIGUSR2 = Bit 1

When the client needs to send a 1 bit, it uses kill(pid, SIGUSR2)
By mapping two signal types to two binary values, Minitalk creates a simple but effective communication protocol that can transmit any data, one bit at a time.

Build docs developers (and LLMs) love