The Problem: Too Many Idle Daemons
If you look at /etc/services on a Linux system, you will see hundreds of different network services listed — FTP, Telnet, SMTP, POP3, echo, discard, daytime, and many more.
In theory, a system could be running a separate server process for each one of these. But think about what that means:
- Most of these services are rarely used — they sit idle 99% of the time, just waiting for a connection that may never come
- Every idle process still occupies a slot in the kernel’s process table
- Every idle process still consumes memory and swap space
- The total resource drain from hundreds of idle server processes is significant
inetd (pronounced “inet-dee”) stands for Internet Daemon. Because it oversees a wide range of services and acts as their supervisor, it is also known as the Internet Superserver.
inetd provides two main benefits:
One inetd process monitors all service ports. Individual servers are started only when a connection/datagram actually arrives. No more hundreds of idle daemons wasting memory.
inetd handles all the boilerplate socket setup (socket, bind, listen, accept) on behalf of every service. The server program can go straight to reading from stdin/writing to stdout.
An extended version of inetd called xinetd is provided in some Linux distributions. xinetd adds security enhancements like access control lists, rate limiting, and better logging. For xinetd details, see: http://www.xinetd.org/
inetd is normally started during system boot. After going through the normal daemon startup process (closing file descriptors, detaching from terminal, etc.), inetd performs the following sequence of steps:
For each service listed, create a socket of the right type (stream for TCP, dgram for UDP) and bind it to the specified port. TCP sockets are additionally put into listen mode.
Call select() to monitor all created sockets simultaneously. inetd blocks here, consuming almost no CPU, waiting for any activity on any of the sockets.
When a UDP datagram arrives or a TCP connection request comes in, select() returns. For TCP connections, inetd also calls accept() to accept the connection before continuing.
inetd calls fork() to create a child process. The child performs setup steps (see below) and then calls exec() to replace itself with the actual server program.
For TCP, inetd (the parent) closes the connected socket file descriptor — it is only needed in the child (the execed server).
inetd goes back to calling select() to wait for the next connection or datagram on any of its sockets.
Child Process Setup Steps (Step 4 detail)
Before the child process calls exec() to run the actual server, it does the following:
- Close all inherited file descriptors except the one socket that has activity (the connected TCP socket or the UDP socket with data)
- Duplicate the socket onto fds 0, 1, and 2 (stdin, stdout, stderr) using dup2(). This means the server program can simply use read()/write() or even fgets()/printf() on standard file descriptors.
- Close the original socket fd (since it is now duplicated on 0, 1, 2 and no longer needed at its original number)
- Optionally change UID/GID to values specified in /etc/inetd.conf, so the server runs as a less-privileged user rather than as root
Code: What the Child Does Before exec()
/* Simplified pseudocode showing what inetd's child process does
before exec()-ing the actual server */
int conn_fd = /* the accepted TCP socket or UDP socket with data */;
/* Step a: Close all other file descriptors */
int max_fd = sysconf(_SC_OPEN_MAX);
for (int fd = 0; fd < max_fd; fd++) {
if (fd != conn_fd) {
close(fd); /* ignore errors — fd may not be open */
}
}
/* Step b: Duplicate socket onto stdin(0), stdout(1), stderr(2) */
if (conn_fd != STDIN_FILENO) {
dup2(conn_fd, STDIN_FILENO); /* fd 0 = socket */
dup2(conn_fd, STDOUT_FILENO); /* fd 1 = socket */
dup2(conn_fd, STDERR_FILENO); /* fd 2 = socket */
close(conn_fd); /* original no longer needed */
}
/* Step c: Optionally drop privileges */
setgid(server_gid); /* set group ID first */
setuid(server_uid); /* then set user ID */
/* Step d: exec the actual server — it reads/writes via fds 0,1,2 */
execv("/usr/sbin/in.ftpd", argv);
/* If execv returns, it failed */
_exit(127);
read(0, buf, n) reads from the network, write(1, buf, n) writes to the network. Many simple services like echo or daytime are written this way for maximum simplicity.inetd’s behavior is controlled entirely by its configuration file at /etc/inetd.conf. Each line in this file describes one service. Lines beginning with # are comments.
Example Lines from /etc/inetd.conf
# echo stream tcp nowait root internal
# echo dgram udp wait root internal
ftp stream tcp nowait root /usr/sbin/tcpd in.ftpd
telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd
login stream tcp nowait root /usr/sbin/tcpd in.rlogind
The first two lines (commented out) are the TCP and UDP echo services implemented internally by inetd itself. The remaining three are real services that invoke the tcpd TCP wrapper.
Field-by-Field Explanation
Each line has the following whitespace-separated fields:
Service
Name
Socket
Type
Protocol
Flags
(wait/nowait)
Login
Name
Server
Program
Server
Arguments
| Field | Meaning |
|---|---|
| Service name | Name from /etc/services (e.g., ftp, telnet, echo). Combined with the protocol field to look up the port number. |
| Socket type | stream for TCP (connection-oriented), dgram for UDP (datagram). |
| Protocol | tcp or udp. Must match an entry in /etc/protocols. |
| Flags | wait or nowait. Controls whether inetd temporarily hands off socket management to the execed server. Critical for correct TCP vs UDP behavior — covered in Part 3. |
| Login name | Username (and optionally group) from /etc/passwd under which the execed server will run. Can optionally include a group: user.group |
| Server program | Full pathname of the server executable to run when this service is triggered. Specify internal for services handled by inetd itself. |
| Server arguments | argv[0], argv[1], … for the server. First argument is typically the program basename (argv[0]). Omitted for internal services. |
Reloading inetd Configuration
When you modify /etc/inetd.conf, inetd is not restarted — you simply send it a SIGHUP signal to ask it to re-read the configuration file:
# Send SIGHUP to reload /etc/inetd.conf
killall -HUP inetd
# Alternatively, find inetd's PID and signal it directly
kill -HUP $(pidof inetd)
select() system call (see Section 63.2.1 in TLPI). select() allows monitoring multiple file descriptors simultaneously and blocks until any of them becomes ready. This way, one process can watch all service sockets without spinning or polling.killall -HUP inetd or kill -HUP $(pidof inetd). inetd has a SIGHUP handler that closes all existing sockets, re-reads /etc/inetd.conf, and creates new sockets for the updated configuration.