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;
}
}
}
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 |
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);
}
}
./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.
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.
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;
}
}
}
num_childrentracks 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
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.
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.
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.
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.
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.
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.
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.
