3.1 The four addressing modes
| Mode | Delivery | Support |
|---|---|---|
| Unicast | one sender → one receiver | everywhere |
| Broadcast | one → all hosts on a subnet | IPv4 only (IPv6 dropped it) |
| Multicast | one → all subscribed hosts | IPv4 optional, IPv6 mandatory (next lesson) |
| Anycast | one → nearest of a set | routing-level (DNS roots) |
3.2 Broadcast addresses
- Limited broadcast: 255.255.255.255 — all hosts on the local link; never forwarded by routers. Used when you don't yet know your own network (DHCP's first cry).
- Subnet-directed broadcast: host bits all-1 for a subnet, e.g. 192.168.1.255 for 192.168.1.0/24. Routers can be configured to forward — but are universally told not to (smurf-attack history).
At the link layer an IPv4 broadcast rides an Ethernet frame to ff:ff:ff:ff:ff:ff — every NIC on the segment passes it up; every host pays the interrupt, which is broadcast's fundamental cost (and why multicast exists).
Who uses broadcast: ARP (the question "who has 192.168.1.7?" is a broadcast), DHCP (client has no address yet), NTP/routing daemons on LANs, service discovery ("is there a server here?").
3.3 Sending: SO_BROADCAST and dg_cli with broadcasting
The kernel refuses to sendto a broadcast address unless the program explicitly declares intent:
const int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
dest.sin_family = AF_INET;
dest.sin_port = htons(13); /* daytime */
inet_pton(AF_INET, "192.168.1.255", &dest.sin_addr);
sendto(sockfd, "", 1, 0, (struct sockaddr *)&dest, sizeof(dest));
(Without the option: EACCES.) One datagram, many replies: broadcast a daytime query and every host's server answers — the client must loop in recvfrom with a timeout, printing each reply and its source address:
/* dg_cli with broadcasting: collect replies until 5 quiet seconds */
sendto(sockfd, sendline, n, 0, (SA *)&dest, sizeof(dest));
for (;;) {
fd_set rset; FD_ZERO(&rset); FD_SET(sockfd, &rset);
struct timeval tv = {5, 0};
if (select(sockfd+1, &rset, NULL, NULL, &tv) == 0) break; /* done */
len = sizeof(from);
n = recvfrom(sockfd, recvline, MAXLINE, 0, (SA *)&from, &len);
printf("from %s: %s", inet_ntop(AF_INET, &from.sin_addr, str, sizeof(str)), recvline);
}
Note what changed from the unicast client: you can't use a connected UDP socket (replies come from many addresses), and "how long do I wait?" has no right answer — a timeout heuristic ends the collection.
3.4 Race conditions — the signal version of this story
The classic alternative timeout (SIGALRM) hides a race condition — and this syllabus point generalises far beyond broadcasting:
/* BUGGY timeout pattern */
signal(SIGALRM, recvfrom_alarm);
alarm(5);
n = recvfrom(sockfd, ...); /* RACE: if the alarm fires BETWEEN */
alarm(0); /* alarm(5) and recvfrom, the signal */
/* is consumed early and recvfrom */
/* blocks FOREVER */
The window is tiny — which makes the bug worse: it strikes once a month in production. The three repairs, in increasing elegance:
- pselect — atomically unblock a signal and wait: block SIGALRM, set the flag handler, then
pselectwith the original mask; the unblock+wait is one atomic kernel operation. (sigsetjmp/siglongjmp is the older equivalent escape.) - select with a timeout instead of signals (the code in §3.3) — no signals, no race.
- Self-pipe trick — handler writes a byte to a pipe; select watches the pipe and the socket: turns signals into descriptors.
Definition for the exam: a race condition exists when the correctness of a program depends on the relative timing of events outside its control (signal delivery vs syscall entry here). Cures are always the same family: make check-and-wait atomic, or funnel the asynchronous event into the synchronous wait set.
3.5 Unicast vs broadcast on the wire — the frame-level comparison
"Compare unicast and broadcast delivery of a UDP datagram" expects the layered trace, not just definitions. Send the same daytime query both ways on a 50-host Ethernet:
| Stage | Unicast (to 192.168.1.7) | Broadcast (to 192.168.1.255) |
|---|---|---|
| frame destination | 7's MAC (via ARP) | ff:ff:ff:ff:ff:ff |
| NICs that pass it up | exactly one — others filter in hardware | all 50 — broadcast bypasses the MAC filter |
| IP layer on each host | n/a (never seen) | all 50 check: am I in 192.168.1.0/24? deliver up |
| UDP layer | one host demultiplexes to port 13 | hosts with port 13 open deliver; the other ~45 drop it — after paying the interrupt + IP processing |
| replies | one | many, staggered |
The bolded cell is the syllabus point "broadcast loads every host": the cost is paid by hosts that don't even run the service — work done in software, above the NIC, for nothing. Multicast (next lesson) pushes the filtering back down into the NIC. Note also that broadcast is inherently UDP/ICMP-only: TCP's 4-tuple names exactly two endpoints, so a TCP broadcast is a contradiction.
3.6 The two canonical broadcast users, traced
ARP (the protocol below your sockets): before the unicast above can even be framed, the sender must learn 7's MAC. It broadcasts "who has 192.168.1.7?" — every host processes it; only 7 replies (unicast) with its MAC; the sender caches it (arp -a shows the cache). ARP is the proof that broadcast is foundational: every unicast conversation on a LAN begins with one broadcast.
DHCP (the bootstrap problem): a booting client has no IP address at all — it cannot unicast, cannot even fill in a sane source. It broadcasts DISCOVER from 0.0.0.0:68 to 255.255.255.255:67; servers OFFER; client REQUESTs (still broadcast — other servers must see the choice); server ACKs. Broadcast is the only possible transport for "I don't know who I am or who you are" — say that sentence and the marks follow.
3.7 pselect, precisely
Since §3.4 names it as the proper fix, give its signature and the two differences from select:
int pselect(int maxfdp1, fd_set *rset, fd_set *wset, fd_set *eset,
const struct timespec *timeout, /* nanoseconds, and const! */
const sigset_t *sigmask); /* THE new argument */
Inside the kernel, pselect atomically performs: install sigmask → wait → restore the old mask. The race-free pattern: block SIGALRM in the main flow; let the handler just set a flag; call pselect with a mask that unblocks SIGALRM only for the duration of the wait. The signal can now arrive only while the process is actually waiting — the gap between "check the flag" and "go to sleep" has been welded shut. That one-sentence mechanism ("unblock and wait are a single atomic kernel operation") is the whole answer to "how does pselect fix the race?".
Exam pointers
- "Distinguish unicast, broadcast, multicast, anycast" — §3.1's table plus one named user of each (TCP / ARP+DHCP / IPTV+MBone / DNS root servers).
- "Why does broadcasting require SO_BROADCAST?" — deliberate friction: a typo'd address or a misconfigured netmask shouldn't accidentally interrupt every host on the subnet; the kernel demands an explicit declaration of intent (EACCES otherwise).
- "Explain the race condition in the timeout version of dg_cli" — the buggy code, the exact window (between alarm() and recvfrom entering the kernel), the consequence (block forever), then the three fixes ranked. State the general definition verbatim at the end.
- Short answer: why can't a broadcast client use a connected UDP socket? (Replies arrive from many source addresses; a connected socket filters all but one.)
Check yourself
- 255.255.255.255 vs 192.168.1.255 — which crosses routers, which needs you to know your subnet, and which does DHCP use first?
- A 200-host LAN runs one broadcast-based discovery query per second from each host. Describe the per-host cost — including hosts that don't participate in the protocol.
- Why does the broadcast daytime client need a select timeout where the unicast one didn't? What ends the reply collection?
- Place the race: between which two lines of the buggy code must the alarm fire to cause the hang? Why does smallness make the bug worse?
- Sketch how the self-pipe trick converts a signal into something select can wait on. Which general cure family does it belong to?