Skip to main content

Architecture Overview

Minitalk implements a classic client-server architecture where:
  • Server: A persistent process that waits for incoming messages
  • Client: A transient process that sends a message and terminates
  • Addressing: Uses Process IDs (PIDs) to route signals between processes
Unlike network client-server applications that use IP addresses and ports, Minitalk uses PIDs for addressing. Each running process has a unique PID assigned by the operating system.

Server Implementation

Server Startup and PID Display

The server starts up and immediately displays its PID so clients know where to send messages. From server.c:34-37:
int	main(void)
{
	ft_printf("SUCCESS!, Server is ready :D! The PID: %d\n", getpid());
	ft_printf("Waiting fot the string...\n");
Example output:
SUCCESS!, Server is ready :D! The PID: 12345
Waiting fot the string...
The getpid() system call returns the current process’s PID. This is the number clients will use to address this server.

Signal Handler Registration

After displaying its PID, the server registers handlers for incoming signals (server.c:38-39):
signal(SIGUSR1, handle_sigusr);
signal(SIGUSR2, handle_sigusr);
This tells the operating system:
  • “When SIGUSR1 arrives, call handle_sigusr
  • “When SIGUSR2 arrives, call handle_sigusr
Both signals use the same handler because the handler function receives the signal type as a parameter (int sigsent) and can distinguish between them.

The Pause Loop

The heart of the server is an infinite loop with pause() from server.c:40-43:
while (1)
{
	pause();
}
pause() suspends the process until a signal is received. This is extremely efficient because:
  1. No CPU usage: The process doesn’t busy-wait or poll
  2. Instant wake-up: The OS immediately resumes the process when a signal arrives
  3. Automatic return: After the signal handler finishes, pause() returns and is called again
Without pause():
// BAD: Burns CPU constantly checking
while (1)
{
    // Nothing - just wastes CPU cycles
}
With pause():
// GOOD: Sleeps until needed
while (1)
{
    pause(); // CPU usage = 0% while waiting
}

Client Implementation

PID Validation

Before sending any signals, the client validates the provided PID to ensure it’s a valid number. From client.c:77-85:
while (argv[1][j] != '\0')
{
	if (argv[1][j] < '0' || argv[1][j] > '9')
	{
		ft_printf("Wrong PID\n");
		return (1);
	}
	j++;
}
This prevents errors like:
./client abc "Hello"        # ❌ Invalid PID
./client 12.5 "Hello"       # ❌ Invalid PID
./client -100 "Hello"       # ❌ Invalid PID
./client 12345 "Hello"      # ✅ Valid PID
This validation only checks if the PID is a number. It doesn’t verify that a process with this PID actually exists. If you provide a non-existent PID, the kill() function will fail with error -1.

Usage and Argument Validation

The client enforces correct usage from client.c:75-76:
if (argc != 3 || !argv[2])
	return (ft_printf("Usage : ./client <PID> <string to send>\n"), 1);
Valid usage:
./client 12345 "Hello, World!"
Invalid usage:
./client 12345              # ❌ Missing message
./client "Hello"            # ❌ Missing PID
./client                    # ❌ Missing both

Sending the Message

After validation, the client calls send_message() from client.c:86-90:
if (send_message(ft_atoi(argv[1]), (unsigned char *)argv[2]))
{
	ft_printf("Client failed sending signal\n");
	return (1);
}
The send_message function handles:
  1. Converting the PID string to an integer with ft_atoi()
  2. Iterating through each character in the message
  3. Sending 8 signals per character (one per bit)
  4. Error handling if kill() fails

Communication Flow

Here’s the complete step-by-step flow of a message transmission:

Phase 1: Setup

Phase 2: Client Transmission

Phase 3: Error Handling

// From client.c:55-61
if (kill(pid, SIGUSR2) == -1)
	return (1);
// ...
if (kill(pid, SIGUSR1) == -1)
	return (1);
When does kill() return -1?
  1. Invalid PID: The process doesn’t exist
    ./client 99999 "Hello"  # If PID 99999 doesn't exist
    
  2. Permission denied: You don’t own the target process
    ./client 1 "Hello"      # PID 1 is init, requires root
    
  3. Invalid signal: Signal number is wrong (shouldn’t happen with SIGUSR1/2)
Error handling flow:
kill() fails → return 1 → main() checks return value → print error → exit
This ensures the client doesn’t silently fail if the server isn’t responding.

PID-Based Addressing

How PIDs Work

// Server discovers its own PID
int server_pid = getpid();  // e.g., 12345

// Client targets that PID
kill(12345, SIGUSR1);  // Send to process 12345
Properties:
  • Unique: Each running process has a unique PID
  • System-wide: PIDs are unique across the entire system
  • Temporary: PIDs are reassigned after a process terminates
  • Sequential: Usually assigned sequentially (but can wrap around)
Limitations:
  • Local only: PIDs only work on the same machine (not across networks)
  • No authentication: Any process owned by you can signal any other process you own
  • Reusable: If server restarts, it gets a new PID

Multiple Clients

Can multiple clients send to the same server?Yes! Multiple clients can all send signals to the same server PID. However:
  1. No queuing: If two clients send simultaneously, signals may be lost
  2. No isolation: The server can’t distinguish which client sent which signal
  3. Message interleaving: Messages from different clients will be mixed together
Example:
# Terminal 1
./server
# PID: 12345

# Terminal 2
./client 12345 "Hello" &

# Terminal 3
./client 12345 "World" &

# Server might print: "HeWlolrold" or "HWeolrllod" or any interleaving
For production use, you’d need to add:
  • Client identification in the protocol
  • Message framing (start/end markers)
  • Acknowledgment signals back to clients

Real-World Example

Let’s trace a complete session:

Terminal 1: Start Server

$ ./server
SUCCESS!, Server is ready :D! The PID: 42837
Waiting fot the string...

Terminal 2: Send Message

$ ./client 42837 "Hi"
$
The client:
  1. Validates 42837 is a valid number ✅
  2. Calls send_message(42837, "Hi")
  3. Sends 16 signals total (8 for ‘H’, 8 for ‘i’)
  4. Each signal is separated by 700 microseconds
  5. Total time: ~16 × 0.7ms = 11.2ms
  6. Exits successfully

Back to Terminal 1: Server Output

SUCCESS!, Server is ready :D! The PID: 42837
Waiting fot the string...
Hi
The server:
  1. Was sleeping in pause()
  2. Received 16 signals
  3. After every 8 signals, printed one character
  4. Continues waiting for more messages

Key Architectural Decisions

Server is Persistent

Runs continuously, handling multiple messages from multiple clients over its lifetime

Client is Transient

Starts, sends one message, and terminates immediately

Unidirectional

Communication flows only from client to server (no acknowledgments in basic version)

PID-Based

Uses process IDs for addressing instead of network addresses

Summary

The Minitalk architecture demonstrates:
  1. Server initialization: Get PID, register handlers, enter pause loop
  2. Client addressing: Validate and use PID to target the server
  3. Signal-based IPC: Use kill() to send, signal handlers to receive
  4. Efficient waiting: pause() provides zero-CPU waiting
  5. Error handling: Check kill() return values for transmission errors
This simple architecture teaches fundamental UNIX concepts:
  • Process identification (PIDs)
  • Signal handling
  • Inter-process communication
  • Asynchronous event handling
While not suitable for high-performance applications, it’s an excellent educational tool for understanding how processes communicate at the system level.

Build docs developers (and LLMs) love