Writing inetd-Invoked Servers Iterative vs Concurrent Servers · Server Design Patterns · Interview Q&A

 

Part 2: Writing inetd-Invoked Servers
Chapter 60 · Iterative vs Concurrent Servers · Server Design Patterns · Interview Q&A
3
Code Examples
2
Server Designs
6+
Interview Questions

What We Cover in Part 2

In Part 1, we learned how inetd works. Now we look at the actual code. We compare a traditional standalone TCP server with an inetd-invoked server and see how dramatically simpler the inetd version is. We also look at the two main server design patterns — iterative and concurrent — and when to use each.

1. A Traditional Standalone TCP Echo Server

A standalone TCP server must handle everything itself. It creates a socket, binds to a port, listens, accepts connections, forks children, and handles client communication. This is a lot of boilerplate. Here is what a typical standalone concurrent TCP echo server looks like in simplified form:

/* Simplified standalone TCP echo server (is_echo_sv.c) */
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define PORT    7       /* Echo service port */
#define BUF_SIZE 4096

/* SIGCHLD handler to reap zombie children */
static void sigchld_handler(int sig)
{
    /* Use WNOHANG so we don't block if no child has exited */
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

int main(void)
{
    int lfd, cfd;
    struct sockaddr_in servaddr;
    char buf[BUF_SIZE];
    ssize_t numRead;

    /* Step 1: Set up SIGCHLD handler to avoid zombies */
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);

    /* Step 2: Create TCP socket */
    lfd = socket(AF_INET, SOCK_STREAM, 0);

    /* Step 3: Bind to port 7 on all interfaces */
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(PORT);
    bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    /* Step 4: Mark socket as passive (listening) */
    listen(lfd, 5);

    /* Step 5: Accept loop */
    for (;;) {
        cfd = accept(lfd, NULL, NULL);

        switch (fork()) {
        case 0:  /* Child: handle client */
            close(lfd);  /* Child doesn't need listening socket */
            while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
                write(cfd, buf, numRead);
            close(cfd);
            _exit(EXIT_SUCCESS);

        default: /* Parent: go back and accept next connection */
            close(cfd);  /* Parent doesn't need connected socket */
            break;
        }
    }
}
Observation: This server has many steps — socket creation, bind, listen, accept, fork, SIGCHLD handler, closing file descriptors. The actual echo logic is only 2 lines: read(cfd, buf, BUF_SIZE) and write(cfd, buf, numRead). Everything else is setup. inetd handles all the setup for us.

2. The inetd-Invoked Echo Server

When inetd manages the connection, all the setup code disappears. Remember that inetd has already done everything: created the socket, bound it, listened, accepted the connection, forked a child, and mapped the socket to stdin/stdout. Your server only needs to read from stdin and write to stdout.

/* TCP echo server invoked by inetd (is_echo_inetd_sv.c) */
/* TLPI Listing 60-6 */
#include <syslog.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define BUF_SIZE 4096

int main(int argc, char *argv[])
{
    char buf[BUF_SIZE];
    ssize_t numRead;

    /*
     * inetd has already:
     *   - Created the socket
     *   - Accepted the connection
     *   - forked() this child
     *   - Mapped the socket to STDIN_FILENO (fd 0) and STDOUT_FILENO (fd 1)
     *
     * So we just read from stdin (the client) and write back to stdout.
     */
    while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
        if (write(STDOUT_FILENO, buf, numRead) != numRead) {
            syslog(LOG_ERR, "write() failed: %s", strerror(errno));
            exit(EXIT_FAILURE);
        }
    }

    if (numRead == -1) {
        syslog(LOG_ERR, "Error from read(): %s", strerror(errno));
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

Standalone vs inetd Server — Side by Side

Task Standalone Server inetd Server
Create socket ✅ You write it inetd does it
bind() + listen() ✅ You write it inetd does it
accept() loop ✅ You write it inetd does it
fork() child ✅ You write it inetd does it
SIGCHLD handler ✅ You write it inetd does it
Daemon setup ✅ You write it inetd does it
Actual echo logic ~2 lines ~2 lines
Conclusion: The inetd echo server is about 15 lines of code vs 60+ for standalone. The business logic (the echo part) is identical — only the setup is eliminated by inetd.

3. Registering the Server with inetd

To tell inetd to use your server for the echo service, add this line to /etc/inetd.conf:

echo  stream  tcp  nowait  root  /bin/is_echo_inetd_sv  is_echo_inetd_sv

Breaking down the fields:

echo                  # Service name (port 7, from /etc/services)
stream                # Socket type: stream = TCP
tcp                   # Protocol: TCP
nowait                # Don't wait for child to finish before accepting more
root                  # Run server as root user
/bin/is_echo_inetd_sv # Full path to your server binary
is_echo_inetd_sv      # argv[0] for the server program

After editing /etc/inetd.conf, send inetd a SIGHUP signal to make it re-read its configuration:

kill -HUP $(pidof inetd)
# or on modern systems using xinetd:
# systemctl reload xinetd

4. Writing a Server That Works Both Standalone and via inetd

Sometimes you want a server that can run either way — started directly from the command line (for testing), or invoked by inetd (in production). You can do this by checking for a command-line flag like -i. This is the concept behind exercise 60-2 in TLPI.

/* Dual-mode echo server: can run standalone or via inetd */
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <syslog.h>

#define PORT     7
#define BUF_SIZE 4096

/* Handle a single connected client on fd cfd */
static void handle_client(int cfd)
{
    char buf[BUF_SIZE];
    ssize_t numRead;

    while ((numRead = read(cfd, buf, BUF_SIZE)) > 0) {
        if (write(cfd, buf, numRead) != numRead) {
            syslog(LOG_ERR, "write() failed: %s", strerror(errno));
            return;
        }
    }
}

int main(int argc, char *argv[])
{
    int inetd_mode = 0;

    /* Check for -i flag: means we were invoked by inetd */
    if (argc > 1 && strcmp(argv[1], "-i") == 0)
        inetd_mode = 1;

    if (inetd_mode) {
        /*
         * inetd has already accepted the connection and mapped the
         * socket to STDIN_FILENO. Just handle the one client.
         */
        handle_client(STDIN_FILENO);
        exit(EXIT_SUCCESS);
    }

    /*
     * Standalone mode: do all the setup ourselves.
     * Create socket, bind, listen, accept loop with fork.
     */
    int lfd, cfd;
    struct sockaddr_in servaddr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(PORT);

    bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(lfd, 5);

    printf("Standalone mode: listening on port %d\n", PORT);

    for (;;) {
        cfd = accept(lfd, NULL, NULL);
        if (fork() == 0) {
            close(lfd);
            handle_client(cfd);
            close(cfd);
            _exit(EXIT_SUCCESS);
        }
        close(cfd);
    }
}
How it works:

  • ./echo_sv -i — inetd mode: reads from stdin (fd 0, which is the accepted socket)
  • ./echo_sv — standalone mode: does its own socket setup and accept loop

The handle_client() function is shared between both modes — one function, two execution paths.

5. Iterative vs Concurrent Server Design

Every TCP server falls into one of two design categories. Choosing the right one matters a lot for performance and reliability.

Iterative Server

Handles one client completely before moving to the next. Client A must wait for Client B to finish. Simple but can cause delays if requests are long.

Accept Client A
Handle Client A fully
Client B waits…
Accept Client B
Handle Client B fully
Best for: Short, quick requests (e.g., a simple time server that returns the current time and disconnects immediately).

Concurrent Server

Handles multiple clients simultaneously. Uses fork() or threads. Client B does not wait for Client A. More complex but necessary for long-running requests.

Accept Client A → fork()
Child: Handle A
Parent: Accept B
A still running
B also starts
Best for: Long or variable-length requests like file transfers, database queries, or any service where clients might stay connected a long time.

6. High-Load Server Design Challenges

The standard concurrent server — one new process per client via fork() — works well for moderate loads. But when thousands of clients connect simultaneously, creating a new process for each one becomes expensive. Here are some alternative approaches used in production:

Approach How It Works Used By
Process per client (fork) fork() on each connection Traditional servers, inetd
Thread per client pthread_create() per connection, cheaper than fork Apache httpd (worker MPM)
Pre-forked processes Fork a pool of workers at startup, each accepts clients Apache httpd (prefork MPM)
Event-driven / I/O multiplexing Single process uses select()/poll()/epoll() to handle many clients nginx, Node.js, Redis
Thread pool Fixed number of threads, each picks up new clients from a queue Tomcat, modern HTTP servers

7. Limiting the Number of Child Processes

A simple concurrent server forks one child per client with no limit. Under a flood of connections (intentional or accidental), this can fork hundreds or thousands of processes, exhausting system memory and causing the server to crash. This is the concept behind exercise 60-1 in TLPI.

/* Concurrent server with a child process limit */
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT         7
#define BUF_SIZE     4096
#define MAX_CHILDREN 10   /* Maximum simultaneous client handlers */

static volatile int num_children = 0;  /* Current count of live children */

static void sigchld_handler(int sig)
{
    /* Reap all finished children and decrement counter */
    while (waitpid(-1, NULL, WNOHANG) > 0)
        num_children--;
}

int main(void)
{
    int lfd, cfd;
    struct sockaddr_in servaddr;
    char buf[BUF_SIZE];
    ssize_t numRead;

    /* Set up SIGCHLD handler */
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_handler = sigchld_handler;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);

    /* Create, bind, listen */
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(PORT);
    bind(lfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    listen(lfd, 50);

    for (;;) {
        cfd = accept(lfd, NULL, NULL);

        /* Wait until we are below the child limit */
        while (num_children >= MAX_CHILDREN) {
            printf("Max children reached (%d), waiting...\n", MAX_CHILDREN);
            pause();  /* Wait for SIGCHLD to reduce the count */
        }

        num_children++;

        switch (fork()) {
        case 0:  /* Child */
            close(lfd);
            while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)
                write(cfd, buf, numRead);
            close(cfd);
            _exit(EXIT_SUCCESS);

        default: /* Parent */
            close(cfd);
            break;
        }
    }
}
Key points:

  • num_children tracks how many child processes are currently running.
  • The SIGCHLD handler decrements the count when a child exits.
  • If at the maximum, the parent calls pause() to sleep until a SIGCHLD arrives.
  • This prevents the server from being overwhelmed by too many simultaneous clients.

8. Using syslog in inetd Servers

A standalone server can print errors with fprintf(stderr, ...). But an inetd-invoked server has its stderr remapped to the socket. Any message written to stderr would be sent to the client — which is wrong! Instead, inetd servers use syslog() to log errors to the system log (/var/log/syslog or /var/log/messages).

#include <syslog.h>

/* Log an error message to the system log */
syslog(LOG_ERR, "write() failed: %s", strerror(errno));

/* Log priority levels available:
   LOG_EMERG   - system is unusable
   LOG_ALERT   - action must be taken immediately
   LOG_CRIT    - critical conditions
   LOG_ERR     - error conditions         (most common for servers)
   LOG_WARNING - warning conditions
   LOG_NOTICE  - normal but significant
   LOG_INFO    - informational
   LOG_DEBUG   - debug-level messages
*/

/* View syslog output on Linux: */
/* tail -f /var/log/syslog           (Debian/Ubuntu) */
/* tail -f /var/log/messages         (RHEL/CentOS)   */
/* journalctl -f                     (systemd-based) */

Interview Questions — Part 2

Q1: What is the difference between an iterative and a concurrent server?

An iterative server handles one client at a time — it processes a client’s complete request before accepting the next client. A concurrent server handles multiple clients simultaneously, typically by forking a child process or creating a thread for each new client. Iterative servers are simpler but become a bottleneck when requests take long. Concurrent servers scale better but are more complex to write correctly.

Q2: Why is fork() expensive at high load, and what are the alternatives?

Creating a new process with fork() copies the process’s address space and requires kernel bookkeeping. Under heavy load (hundreds of clients/second), the cost of fork() adds up. Alternatives include: pre-forking a pool of worker processes at startup (each worker accepts clients independently), using threads instead of processes (cheaper context switch), or using a single-process event-driven model with epoll() to handle many connections without any extra processes at all.

Q3: In the inetd echo server, why do we use syslog() instead of perror() or fprintf(stderr)?

In an inetd-invoked server, file descriptor 2 (stderr) has been redirected to the connected socket. Writing an error message to stderr would send that message to the client, which is incorrect behavior. syslog() writes to the system log daemon instead of any file descriptor, so it works correctly regardless of how the process’s standard streams are set up.

Q4: How would you design a server that can work both as a standalone daemon and as an inetd-invoked server?

Use a command-line flag (such as -i) to switch between modes. When the -i flag is present, assume the socket is already connected and available on STDIN_FILENO (fd 0) — just handle the client and exit. When no flag is given, run in standalone mode: create a socket, bind, listen, and accept in a loop with fork(). The actual client-handling logic is identical in both modes — factor it into a shared function that accepts a file descriptor.

Q5: What happens if a concurrent server does not limit the number of child processes?

Without a limit, a flood of incoming connections (from clients or an attacker) causes the server to fork() continuously. Each process consumes memory (stack, page tables, kernel structures). If the number grows too large, the system runs out of memory and process table slots, causing the server to crash or the whole system to become unresponsive. This is a classic resource exhaustion (denial of service) vulnerability. Limiting children with a counter and SIGCHLD handler prevents this.

Q6: In a concurrent TCP server using fork(), why does the parent close the connected socket (cfd) after fork()?

After fork(), both parent and child have their own copy of the connected socket file descriptor. The socket has a reference count of 2. If the parent keeps its copy open, the socket will not actually be closed when the child calls close() — the connection stays open until both parent and child close their copies. By calling close(cfd) in the parent immediately after fork(), the parent’s reference is dropped and only the child holds the connection, which is the correct behavior.

Q7: What is xinetd and how does it relate to inetd?

xinetd (Extended Internet Services Daemon) is a modern replacement for the original inetd. It provides the same core functionality (listen on multiple ports, start servers on demand) but adds important security features: per-service access control (allow/deny by IP address), rate limiting (prevent fork bombs), logging, and resource limits per service. On modern Linux systems, xinetd or systemd socket activation have largely replaced the original inetd. The server programming model is the same — stdin/stdout mapped to the socket.

Chapter Exercises (from TLPI)

Exercise 60-1

Add code to the standalone concurrent TCP echo server (is_echo_sv.c) to place a limit on the number of simultaneously executing children. Think about: where to keep the count, how to increment it, how to decrement it when a child exits, and what to do when the limit is reached.

Hint: Use a global volatile int num_children counter, increment before fork(), decrement in the SIGCHLD handler, and call pause() in the accept loop when at the limit.

Exercise 60-2

Modify the standalone echo server so it can be invoked either directly from the command line or by inetd. If given a -i option, assume it is being invoked by inetd: handle one client on STDIN_FILENO and exit. Without -i, operate in the usual standalone mode. Also modify /etc/inetd.conf to invoke this server for the echo service.

Hint: Check argc/argv[1] for the -i flag early in main(). The client-handling logic should be in a shared function so neither mode duplicates it.

Chapter 60 Key Takeaways

inetd eliminates server boilerplate Socket → stdin/stdout via dup2() nowait=TCP, wait=UDP syslog() not stderr in inetd servers Iterative: one client at a time Concurrent: fork/thread per client Limit children to prevent exhaustion SIGCHLD reaps zombie children -i flag for dual-mode servers epoll for high-load single-process

Leave a Reply

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