Memra

From a toy to something that stays up

◈ 12 cards

The steps that turn a program into a daemon and why each one is there, then the design of a multi-process TCP application where every philosopher and every fork is its own process.

What is still wrong with the server you just wrote

The concurrent server from L14.3 works and would not survive an afternoon. Log out and it dies with your shell. Press Ctrl-C in the wrong window and it dies. Start it twice by accident and two copies fight over the port. It holds your home directory open, so that filesystem cannot be unmounted. And it writes its errors to a terminal that will not exist tomorrow.

A daemon is the fix: a long-running background process with no controlling terminal. The recipe is short, and every step earns its place.

The recipe, one reason at a time

1. fork(), and let the parent _exit(0). The child is inherited by init and no longer belongs to the shell's job control, so it survives the shell. This step also has a second purpose that step 6 depends on.

2. Set signal dispositions. A server should not die of SIGHUP when the session that started it goes away, and should not die of SIGINT. Ignore both. Install the SIGCHLD reaper here too. SIGHUP has a convention worth knowing: many daemons use it to mean re-read your configuration, which is how inetd is reconfigured without restarting.

3. umask(027). Set it explicitly rather than inheriting whatever the starting shell had, so that files and logs the daemon creates get the permissions you intended regardless of the mode argument passed to open().

4. Open a lock file and take an exclusive lock on it. This is how you enforce a single instance: the second copy fails to take the lock and exits with a message instead of quietly competing for the port. flock() is simpler and its locks survive fork(), but it does not exist on Solaris; fcntl() with F_SETLK is portable but its locks are not inherited across fork().

5. Write the PID into the lock file. Now anyone — an init script, an administrator, you — can find the running daemon and signal it without guessing at ps output.

6. setsid(). This creates a new session with the caller as leader and, crucially, with no controlling terminal. It fails with EPERM if the caller is already a process-group leader — which is exactly why step 1 had to fork. The child of a fork is never a group leader, so setsid() here always succeeds. The ordering is the point, and it is a favourite exam question.

7. chdir("/"). Two reasons, both practical. A daemon sitting in your home directory keeps that filesystem busy and it cannot be unmounted. And if the daemon dumps core, you want the core file somewhere findable rather than in whatever directory the daemon happened to inherit.

8. Close every inherited descriptor, including 0, 1 and 2. Whatever the starting shell had open is not the daemon's business, and holding them costs descriptors it will need for clients.

9. Reopen 0, 1 and 2 on /dev/null. Do not skip this. A great deal of library code assumes the standard descriptors are open; if they are not, the first file the daemon opens becomes descriptor 0 and some library's stray printf writes into it. /dev/null returns EOF on read and swallows writes, which is exactly the benign behaviour wanted.

10. Run the service loop, with reaping. acceptfork → back to accept, and the SIGCHLD handler from step 2 draining with waitpid(-1, NULL, WNOHANG) in a loop. Without it every step above is wasted: the daemon stays up precisely long enough to fill the process table.

Designing a multi-process TCP application: dining philosophers

The heaviest design question on this material asks for something unusual, and is worth 20 marks. Five philosophers sit around a table; between each pair is one fork; a philosopher needs both adjacent forks to eat. The unusual part is the implementation constraint: each philosopher and each fork is a separate process, and they communicate over TCP/IP.

That constraint decides the architecture before you write a line. A fork is a resource that must be held by at most one philosopher at a time, and the only thing in this design that can hold state exclusively is a process. So:

Ten processes. Five fork servers and five philosopher clients. Each fork server owns exactly one resource and one boolean: held, or free, and if held, by whom.

Who listens and who connects. The fork servers listen; the philosophers connect. Fork server i does socketbindlistenaccept on its own well-known port — say 9000 + i, so philosopher p reaches its left fork at 9000 + p and its right fork at 9000 + ((p+1) mod 5) without any discovery mechanism. Ports are the naming scheme, and saying so is worth a mark.

The message protocol, in words before code. Three messages, one line each, terminated by a newline:

  • REQUEST — philosopher to fork server, meaning may I hold you?
  • GRANT or DENY — the reply. GRANT if the fork is free; DENY if some other philosopher holds it.
  • RELEASE — philosopher to fork server, after eating.

Use a newline terminator and read the reply in a loop until you see it, because TCP has no message boundaries and a five-byte GRANT\n can arrive in two reads. This is the L14.2 discipline applied to a protocol you designed yourself.

Each fork server is iterative and stateful. It serves one request at a time, to completion, before looking at the next — and that is exactly what makes it a lock. If a fork server were concurrent, two forked slaves could both read a free flag and both answer GRANT, and the resource would be held twice. Serialisation is the feature.

A philosopher's cycle is: think → REQUEST the first fork → REQUEST the second → eat → RELEASE both → repeat. Say explicitly what happens on a DENY: the simplest correct choice is to release anything already held, wait, and retry — holding one fork while blocking on the other is exactly the deadlock the classic problem is famous for.

Check every return value and reap every child. Every socket, bind, listen, accept, connect, read and write returns a value that can be −1, and an assignment that ignores them is marked down. If any process forks, it installs the SIGCHLD reaper. Close the sockets and shut the servers down cleanly at the end.

About deadlock, and how much of your effort it deserves

Run the symmetric version — every philosopher reaching left first — and it deadlocks: each holds its left fork, each waits forever for its right. The classic fix is asymmetry: make one philosopher reach right first, or equivalently impose a global ordering and require every philosopher to take the lower-numbered fork first. One sentence of design, and the cycle in the wait-for graph is broken.

Give it that one sentence and move on. The assignment states that marks are not deducted if your solution leads to starvation, which tells you where the marks actually are: command of process communication over TCP/IP. Spend your time on the socket plumbing, the port map, the message protocol, the read loops and the error checking — and on the documentation of your thought process, which the assignment asks for by name.

in backgroundpolicy setexclusivedetachedclean slatefork; parent _exit(0)child re-parented, off job controlsignals; umask(027)ignore HUP/INT; install SIGCHLD reaperlock file; write PIDsingle instance, and findablesetsid(); chdir("/")no controlling terminalclose fds; reopen 0,1,2on /dev/null — libraries assume them openaccept, fork, reapthe service loopSkip the last box and every other box waswasted: the process table fills.
The order is load-bearing in one place: setsid() must follow the fork, because it fails if the caller is already a process-group leader and the child of a fork never is.
P0F09000P1F19001P2F29002P3F39003P4F49004P = philosopher client, connects. F = fork server, listens. Edge = a TCPconnection.
Ten processes and no shared memory. A philosopher addresses its left and right forks by port number alone, which is why the port map is part of the design and not an implementation detail.

source Stevens & Rago, APUE 3e ch16, §10.7

NORMAL ~/memra/learn/comp-325/production-servers-and-the-dining-philosophers utf-8 LF