Two processes, one protocol.
The director owns the checkout policy; the supermarket process owns customers, cashiers, queues and their state. In the simplest launch the director starts the supermarket itself with fork and exec. They talk through a Unix-domain stream socket with integer-coded requests, responses and acknowledgments, converted to network byte order, and coordinate orders with signals.
Customers who change their mind.
- Admission. At opening the store admits C customers; when occupancy falls to C − E, it refills back to C.
- Customers are threads: each picks a shopping time and a basket size. With an empty basket they ask the director for permission to leave; otherwise they join an open checkout and, every S milliseconds, compare the people ahead of them with the other queues, moving to a shorter one.
- Cashiers are threads too, each with a random base service time between 20 and 80 ms plus a fixed time per product. A reporting thread sends each checkout's customer count to the director at regular intervals.
- Synchronization. Shared state is protected by mutexes, and threads wait on condition variables instead of spinning.
| Parameter | Meaning | config1 | config2 |
|---|---|---|---|
| K | maximum open checkouts | 2 | 6 |
| C | customers in the store at once | 20 | 50 |
| E | departures before the store refills | 5 | 3 |
| S1 | nearly empty checkouts that trigger a closing | 1 | 2 |
| S2 | queue length that triggers an opening | 4 | 5 |
The demo at the top simulates the policy with config2, slowed down about eight times.
A policy under load.
On every report the director scans the checkouts in order and acts on the first condition it meets: a queue of S2 or more customers opens a closed checkout; S1 checkouts with zero or one customer close the current one, always keeping at least one open. Every order goes through a small handshake: a signal to warn the supermarket, a signal back, then the order and the checkout number over the socket.
static void decision() { bool closing = false, opening = false; int at_most_one_customer = 0, i = 0, checkout = -1; // open or close a checkout, according to the thresholds in the configuration file while (i < dir_args.max_open_checkouts && !closing && !opening) { if (customers_per_checkout[i] >= dir_args.s_open_checkout) { opening = true; } else if (customers_per_checkout[i] == 0 || customers_per_checkout[i] == 1) { at_most_one_customer++; if (at_most_one_customer >= dir_args.s_close_checkout) { checkout = i; closing = true; } } i++; } if (opening) { // pick the first closed checkout (-1 marks a closed one) i = 0; while (i < dir_args.max_open_checkouts && checkout == -1) { if (customers_per_checkout[i] == -1) checkout = i; else i++; } if (checkout != -1) { // SIGPROF warns the supermarket that an order is coming, then wait for its SIGPROF back syscall(kill(mrkt_pid, SIGPROF), "kill") while (!SIGPROF_arrived) ec_nzero(pthread_cond_wait(&alrm_cond, &mtx)) SIGPROF_arrived--; write_on_socket(supermarket_fd, ORDER_OPEN_CHECKOUT); } else return; } /* ... the closing order follows the same handshake, only if more than one checkout is open */}
Closing time, done right.
The day ends through signals sent to the director. SIGHUP closes admission and lets the customers inside finish their shopping, as at the end of the demo's cycle. SIGQUIT requests an emergency closing through a separate coordination path. Internally, SIGPROF carries the director's checkout orders.
After closing, the supermarket writes a structured log, and a Bash script turns it into tables: total customers and products, then per customer (products, time inside, time at the checkout, queues joined) and per checkout (products, customers served, open time, average service time, closures).
Operating systems, made tangible.
The C sources, the Makefile with its test targets, the analysis script and the report that explains the architecture, protocol and synchronization are on GitHub.