Distinguish descriptors from shared open-file state
Track descriptor entries, shared offsets, status flags, and inheritance without confusing a pathname with a live open file.
On this page
The question for today
What you’ll understand
- Draw the distinction between a descriptor, an open file description, and the underlying file.
- Predict offset sharing after dup and fork and distinguish descriptor flags from file status flags.
- Choose explicit inheritance and positional I/O when shared mutable state would otherwise cause mistakes.
A descriptor is a small integer, yet treating it as the file itself leads to surprisingly large bugs. A parent and child can have separate descriptor tables while still advancing one shared offset. Two opens of the same pathname can instead have independent offsets. The useful question is which kernel object owns each piece of state. We will build that object model and use it to explain duplication, inheritance, position changes, and descriptor lifetime.
Follow references through three different layers
A process uses a file descriptor as an index into its descriptor table. That table entry references an open file description, represented in Linux by a struct file, which in turn connects to the appropriate underlying object and operations. The underlying object might be a regular file, pipe, socket, or something else. For a seekable regular file, the open file description holds the current offset and file status flags. A pathname is a means of locating an object during open; it is not the lasting identity of every subsequent I/O operation. Renaming or unlinking a name does not simply redirect an already open reference to whatever later appears at that name.
Each ordinary successful open creates a new open file description, even when another open names the same underlying file. Dup creates a new descriptor referring to an existing description instead. Therefore, descriptor equality and object identity answer different questions: different integers can alias one description, and the same integer in two processes need not identify the same object. An integer can also be reused after close. When investigating a stale descriptor bug, track ownership and lifetime rather than assuming that seeing descriptor seven twice proves continuity. The reference diagram should show table entries separately from the descriptions they reference, with offset and status state attached to the latter.
Duplication shares some state and copies other state
After dup, the descriptors share an offset and file status flags because both reference one open file description. Fork copies the parent’s descriptor entries into the child, with corresponding entries still referencing the same descriptions. A read in the child can consequently change the position used by a later parent read. This sharing does not mean their descriptor tables are one table: closing the child’s entry does not close the parent’s entry. Ordinary threads in one process normally share the descriptor table, which makes a concurrent close and numeric reuse especially dangerous for code that assumes an integer remains associated with its original resource.
Keep file descriptor flags separate from file status flags. FD_CLOEXEC belongs to one descriptor entry; an ordinary dup does not copy it as an enabled flag on the duplicate. Status flags such as O_APPEND or O_NONBLOCK belong to the shared description, so a supported change through one alias affects the other aliases. Some status properties are not changeable with F_SETFL; consult the actual interface instead of treating it as an arbitrary bit assignment. Set close-on-exec atomically when creating or duplicating a descriptor if another thread may exec concurrently. Opening first and setting FD_CLOEXEC later creates an avoidable inheritance race between those two operations.
Make position and lifetime explicit in the design
If workers should read independent regions, a seek followed by a read is not one indivisible positional operation merely because those calls appear next to each other in source. Another user of the shared description can change its offset between them. On a seekable file, pread supplies an explicit byte offset without modifying the shared current offset. It solves the implicit-position race for that operation, but it does not freeze file contents, guarantee a full-length read, or create a transaction with a concurrent writer. Check the returned byte count and define what concurrent mutation means for your application. Opening separately can provide independent offsets while still leaving shared file-content concerns.
Resource cleanup needs the same object-level reasoning. Closing one descriptor removes that reference; aliases can keep the description alive. A child that inherits an unwanted pipe end can prevent another process from observing the expected end-of-file condition even after the intended writer closes its own end. Build a deliberate inheritance list for a launched program, close unused ends on each side, and test failure paths as well as normal exit. Descriptor leaks are often visible as a gradual count increase rather than a single dramatic error. Measure counts under repeated lifecycle operations, distinguish expected long-lived resources from leaks, and investigate which ownership transition failed instead of raising limits as the first response.
Work it through
Two tables, one position
A regular file’s open description has offset 100 bytes. Descriptor 7 is duplicated to 12, then the process forks. Assume no other I/O or seeks. The parent reads 40 bytes through 7; the child then reads 25 bytes through its inherited 12. All requested bytes are returned. Where is the shared offset?
Dup and fork create additional descriptor references to the same open file description. They do not create independent current offsets for these aliases.
The parent’s completed read advances the shared offset from 100 to 140 bytes. The child’s subsequent read begins at 140, not 100.
The child’s 25 returned bytes advance the same offset to 165 bytes. A later parent read through either alias begins there, assuming no intervening offset operation.
The shared current offset is 165 bytes; closing one child alias would not reset it or remove the parent’s references.
The living notebook
Explore the model
Interactive lab · probability
What repeated inheritance checks can establish
Use Success probability p as a hypothetical probability that one independent launch check detects a descriptor-inheritance defect, and Trials n as the number of checks. Predict how the zero-detection bar changes before increasing n. Inspect the exact binomial bars and mean np.
- Mean · np
- 5
- Variance · np(1−p)
- 2.5
- Total probability
- 1
These are exact binomial probabilities for independent trials with one fixed success probability. Playback tours the distribution; it does not resample outcomes. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forThe diagram models independent checks with constant detection probability; it does not inspect descriptors, simulate dup or fork, or prove a program leak-free. Correlated tests violate independence, while unexercised failure paths can make the assumed detection probability unsupported.
Pause & reason
Check your understanding
Your turn
Try it yourself
A shared description is currently at byte 165. A successful pread requests and returns 16 bytes at explicit offset 4096. No other operation occurs. What byte interval was read, and what current offset remains?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Positional I/O supplies its own position and leaves the description’s current offset unchanged.
Compare with the solution
The read covers bytes 4096 through 4111 inclusive, equivalently the half-open interval [4096,4112). The shared current offset remains 165. This does not guarantee the bytes were a snapshot against concurrent writes.
Keep the idea
Take it with you
- Descriptor entries reference open file descriptions; paths and underlying files are additional distinct layers.
- Dup and fork preserve sharing of offsets and status flags while descriptor-entry flags and table ownership follow their own rules.
- Explicit positional I/O and deliberate inheritance reduce bugs caused by implicit shared state.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.