A stateless server needs each request to be self-describing โ yet clients still
want the ordinary open/read/write/close API. How do we reconcile the two?
The Crux: How To Define A Stateless File Protocol
Stateful calls likeopen() canโt be part of the wire protocol (theyโd force
the server to track open files). So how do we define a protocol that is both
stateless and supports the POSIX file API?49.5 The NFSv2 Protocol
The linchpin is the file handle file handle NFS's unique identifier for the file or directory an operation targets, included in most protocol requests. It has three parts: a volume identifier (which exported file system), an inode number (which file within it), and a generation number (to guard against stale handles). Together they let a stateless server locate the exact file with no other context. defined in ch. 49 โ open in glossary โ a unique id for the file or directory an operation targets, carried in most requests. It has three parts:
The generation number generation number The component of an NFS file handle that is incremented each time an inode number is reused. Because a stale file handle carries the old generation number, it will no longer match the (incremented) current one, preventing a client holding an outdated handle from accidentally accessing the different file that has since reused that inode. defined in ch. 49 โ open in glossary is the subtle part: the server bumps it whenever it reuses an inode number, so a client holding an old handle canโt accidentally read the new file that reused that inode. Here are the key protocol operations:
| arguments | returns | |
|---|---|---|
| LOOKUP | directory handle, name to look up | file handle + attributes |
| GETATTR | file handle | attributes |
| SETATTR | file handle, attributes | โ |
| READ | file handle, offset, count | data, attributes |
| WRITE | file handle, offset, count, data | attributes |
| CREATE | directory handle, file name, attributes | โ |
| REMOVE | directory handle, file name | โ |
| MKDIR | directory handle, dir name, attributes | file handle |
| RMDIR | directory handle, dir name | โ |
| READDIR | directory handle, count, cookie | directory entries, cookie |
LOOKUP is how you obtain a handle: pass a directory handle plus a name, get back
the handle (and attributes) for that entry. The very first handle โ for the root
/ โ comes from the separate mount protocol. Attributes are just the
metadata stat() reports (size, owner, permissions, and the last-modified time,
which will matter for caching). With a handle in hand, READ and WRITE carry
the handle plus an explicit offset and count.
49.6 From Protocol to Distributed File System
Now watch the two halves cooperate on a real open โ read โ read โ read
โ close sequence. The key thing to notice: the client holds all the state
(the fd โ handle map and the current offset), turning each read() into a
self-contained READ(handle, offset, count):
The client already has the root handle (from the mount protocol).
Three things stand out. First, the client โ not the server โ tracks the fd โ
handle mapping and the file position, which is what lets a stateless server work.
Second, server contact happens only where needed: the first open triggers a
LOOKUP (and a long path like /home/remzi/foo.txt triggers three โ one per
component: home in /, remzi in home, foo.txt in remzi), while close
is purely local. Third, every server request carries everything needed to
complete it โ the design point that makes graceful crash recovery possible, which
we tackle next.
Check yourself: the NFSv2 protocol
1.An NFS file handle has three parts. What is each for?
2.Why does the file handle include a generation number?
3.What does the LOOKUP protocol operation do, and how does a client get its very first handle?
4.In turning the protocol into a file system, where is the per-open-file state (like the current offset) kept?
5.Why does close(fd) require no message to the server?
6.How does the client resolve a long pathname like /home/remzi/foo.txt?