Concurrent Server Design Linux Socket Programming

 

Concurrent Server Design
Chapter 60 โ€” Part 1 of 4 โ€ข Linux Socket Programming
๐Ÿ“– Theory + Code
๐ŸŽ“ Beginner Friendly
๐Ÿ” Interview Q&A

What You Will Learn

In this part you will understand why a server needs to be concurrent, how the traditional fork-based concurrent server works step by step, and what the child and parent processes each do. You will also see the difference between an iterative (single-client) server and a concurrent (multi-client) server.

Key Terms

Concurrent Server Iterative Server fork() accept() Listening Socket Connected Socket SIGCHLD Zombie Process Daemon File Descriptor Duplication

๐Ÿ’ก Why Do Servers Need to Be Concurrent?

Imagine a bank with only one teller. Every customer must wait for the person in front to finish completely before being served. That is exactly how an iterative server works โ€” it handles one client at a time, fully, before moving on to the next.

Now imagine a bank with multiple tellers. Several customers are served at the same time. That is a concurrent server.

For services like TCP echo (where a client may send unlimited data and the server must keep reading and writing), serving one client can take an unlimited amount of time. During that time, all other clients are blocked waiting. This is completely unacceptable in real systems.

โŒ Iterative Server
Client 1 โ†’ Server (handling…)
Client 2 โ†’ WAITING ๐Ÿ˜ฎ
Client 3 โ†’ WAITING ๐Ÿ˜ฎ
Only 1 client served at a time

โœ” Concurrent Server
Client 1 โ†’ Child Process 1 โ–ถ
Client 2 โ†’ Child Process 2 โ–ถ
Client 3 โ†’ Child Process 3 โ–ถ
All clients served simultaneously

The rule is simple: if servicing one client may take an indefinite amount of time, use a concurrent server design.

๐Ÿ”ง How the Traditional Fork-Based Concurrent Server Works

The classic Linux approach creates a new child process using fork() for every incoming client connection. Here is the step-by-step flow:

Fork-Based Server Flow

Server Starts
Create Listening Socket
call accept() โ€” wait for client
Client connects โ†’ accept() returns connected socket (cfd)
Call fork()

CHILD (case 0)
close(lfd)
handleRequest(cfd)
_exit()
PARENT (default)
close(cfd)
loop back to accept()

Key insight: After fork(), both parent and child have copies of both file descriptors (lfd and cfd). Each closes the one it does not need:

  • The child closes lfd (listening socket) because it does not accept new connections.
  • The parent closes cfd (connected socket) because it does not talk to that client.

If the parent does NOT close cfd, that socket will never actually close (the reference count stays above zero), and eventually the parent runs out of file descriptors.

๐Ÿ“„ What Happens to File Descriptors After fork()

This is one of the most important things to understand. When fork() is called, the child inherits a copy of all the parent’s open file descriptors. Both point to the same underlying open file descriptions in the kernel.

Process FD 3 (lfd) FD 4 (cfd) Action Needed
Parent KEEP (accept loop) CLOSE (not needed) close(cfd)
Child CLOSE (not needed) KEEP (talk to client) close(lfd)

๐Ÿ’€ The Zombie Problem and SIGCHLD Handler

When a child process finishes, it does not immediately disappear. It becomes a zombie โ€” it has exited but its entry is still in the process table, waiting for the parent to collect its exit status using wait() or waitpid().

In a concurrent server that creates many child processes, if we never call waitpid(), the process table fills up with zombies. This can eventually exhaust system resources.

The solution is to install a SIGCHLD signal handler. The kernel sends SIGCHLD to the parent whenever a child exits. In the handler we call waitpid() in a loop (because multiple children may exit between signal deliveries):

static void grimReaper(int sig)
{
    int savedErrno;
    savedErrno = errno;   /* Save errno - waitpid() may change it */

    while (waitpid(-1, NULL, WNOHANG) > 0)
        continue;         /* Reap all dead children */

    errno = savedErrno;   /* Restore errno */
}

Important details:

  • waitpid(-1, ...) means “wait for any child”.
  • WNOHANG means “do not block if no child has exited yet”.
  • We use a while loop because multiple children can exit before the handler runs.
  • We save and restore errno because signal handlers can interrupt system calls that set errno.

๐Ÿ’€
Without SIGCHLD handler
Dead children pile up as zombies. Process table fills. System degrades.
โœ”๏ธ
With SIGCHLD + waitpid()
Dead children are reaped immediately. Process table stays clean.

๐Ÿ”Œ Why the Server Becomes a Daemon

A daemon is a process that runs in the background, detached from any terminal, and typically started at system boot. Network servers are usually daemons because:

  • They should run indefinitely without an attached terminal.
  • They should not be killed when the user logs out.
  • They use syslog for logging instead of printing to a terminal.

The server calls becomeDaemon() (covered in Chapter 37) which: forks, makes the child the session leader via setsid(), changes the working directory to /, closes standard file descriptors, and redirects them to /dev/null.

๐Ÿ›ก Preventing Fork Bombs: Limiting Child Processes

A malicious attacker can connect to your server thousands of times, causing it to create thousands of child processes. This is called a remote fork bomb and it can make the system unusable.

In production servers you should track the number of currently running children:

static int childCount = 0;
#define MAX_CHILDREN 100

/* In the SIGCHLD handler: */
childCount--;   /* decrement when a child dies */

/* After fork(): */
childCount++;

/* Before calling fork(): */
if (childCount >= MAX_CHILDREN) {
    /* Either refuse the connection or temporarily stop accepting */
    close(cfd);
    continue;
}

Another approach: accept the connection but immediately close it, so the client gets a clean “connection refused” style response instead of hanging forever.

๐Ÿ’ป Minimal Concurrent Server Skeleton (Annotated)

Below is the cleanest possible skeleton of a fork-based concurrent TCP server. Every line is explained in comments so you understand the role of each part:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <errno.h>

#define PORT 9000
#define BACKLOG 10

/* SIGCHLD handler - reap zombie child processes */
static void sigchld_handler(int sig)
{
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0)  /* reap all dead children */
        continue;
    errno = saved_errno;
}

/* Per-client work done by the child process */
static void handle_client(int cfd)
{
    char buf[1024];
    ssize_t n;

    /* Echo everything back to the client */
    while ((n = read(cfd, buf, sizeof(buf))) > 0)
        write(cfd, buf, n);

    /* n == 0: client disconnected; n == -1: error */
}

int main(void)
{
    int lfd, cfd;                  /* listening fd, connected fd */
    struct sockaddr_in addr;
    struct sigaction sa;

    /* 1. Install SIGCHLD handler to prevent zombies */
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;      /* restart interrupted system calls */
    sigaction(SIGCHLD, &sa, NULL);

    /* 2. Create the listening socket */
    lfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&addr, 0, sizeof(addr));
    addr.sin_family      = AF_INET;
    addr.sin_port        = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);  /* listen on all interfaces */

    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(lfd, BACKLOG);

    printf("Server listening on port %d\n", PORT);

    /* 3. Accept loop */
    for (;;) {
        cfd = accept(lfd, NULL, NULL);   /* block until a client connects */
        if (cfd == -1) {
            if (errno == EINTR) continue; /* interrupted by signal - retry */
            perror("accept");
            break;
        }

        /* 4. Fork a child to handle this client */
        switch (fork()) {
        case -1:
            perror("fork");
            close(cfd);         /* cannot create child; skip this client */
            break;

        case 0:                 /* CHILD PROCESS */
            close(lfd);         /* child doesn't need listening socket */
            handle_client(cfd);
            close(cfd);
            _exit(EXIT_SUCCESS);  /* use _exit() not exit() in child */

        default:                /* PARENT PROCESS */
            close(cfd);         /* parent doesn't need connected socket */
            break;              /* loop back to accept() */
        }
    }

    close(lfd);
    return 0;
}

Why _exit() in the child? Because exit() flushes stdio buffers and calls atexit() handlers. In the child those are copies inherited from the parent and flushing them twice causes double output. _exit() exits immediately without any cleanup.

๐ŸŽ“ Interview Questions & Answers
Q1. What is the difference between an iterative server and a concurrent server?

An iterative server handles one client completely before accepting the next. A concurrent server handles multiple clients at the same time, typically by creating a new process (or thread) for each client.

Q2. After fork() in a concurrent server, why must the parent close cfd and the child close lfd?

After fork(), both file descriptors exist in both processes. The parent no longer needs cfd (the child handles that client), and the child never accepts new connections so it does not need lfd. Failing to close them causes file descriptor leaks. The parent would eventually exhaust its file descriptor limit, and the connected socket would never actually be closed because the reference count remains above zero.

Q3. What is a zombie process? How does a concurrent server prevent zombies?

A zombie is a process that has exited but whose entry is still in the process table because the parent has not yet called wait()/waitpid() to collect its exit status. A concurrent server prevents this by installing a SIGCHLD signal handler that calls waitpid(-1, NULL, WNOHANG) in a loop to reap all dead children.

Q4. Why do we use a while loop inside the SIGCHLD handler instead of a single waitpid()?

Multiple children may exit between signal deliveries. Linux may deliver only one SIGCHLD even if several children exit. A single waitpid() would reap only one child, leaving others as zombies. The while loop keeps calling waitpid() until it returns 0 (no more dead children), reaping all of them.

Q5. Why should the child call _exit() instead of exit()?

exit() calls stdio flush and atexit() handlers that were registered in the parent. The child has copies of these, so calling exit() in the child causes double flushing of output buffers and runs cleanup functions that were meant for the parent. _exit() terminates immediately without any cleanup.

Q6. What is a remote fork bomb and how can you protect against it?

A remote fork bomb is when an attacker makes thousands of simultaneous connections, forcing the server to create thousands of child processes, exhausting system resources. Protection: maintain a counter of active children, incremented after fork() and decremented in the SIGCHLD handler, and refuse connections when the count reaches a configured maximum.

Q7. Why does a network server become a daemon?

A daemon runs in the background, detached from any controlling terminal, so it is not killed when a user logs out. It uses syslog instead of terminal output. Most long-running system services (web servers, SSH daemons, etc.) are implemented as daemons.

Q8. What is the role of SA_RESTART in the signal action flags for SIGCHLD?

SA_RESTART causes system calls (like accept()) that are interrupted by the signal to be automatically restarted by the kernel, rather than returning -1 with errno set to EINTR. Without this, the accept() in the server loop would fail every time a child exits, and the server would need to explicitly check for EINTR and retry.

Next: TCP Echo Server โ€” Full Implementation Walkthrough
See every line of the TCP echo server code explained in plain language with flow diagrams.

Part 2 โ†’ TCP Echo Server โ† Back to Index

Leave a Reply

Your email address will not be published. Required fields are marked *