Calling dup() does not create an independent stream position. The returned descriptor refers to the same open file description as the source descriptor, so a seek through either descriptor changes the offset observed through both. At the same time, descriptor-local state such as the close-on-exec flag remains attached to each descriptor separately.
That split is easy to miss because both kinds of state are manipulated through integer file descriptors. The integer is only a process-local reference. Several descriptors can point at one open file description, and that shared object carries state with consequences for reads, writes, seeks, and status-flag changes.
The descriptor number is not the open state
On POSIX systems, a successful open() creates an open file description and returns a file descriptor referring to it. The open file description carries the current file offset and file status flags. The descriptor is an entry in a process descriptor table.
A second open() of the same pathname normally creates another open file description. Those two opens can therefore have independent offsets even though both ultimately refer to the same file.
Duplication has different semantics:
int a = open("events.log", O_RDONLY);
int b = dup(a);After a successful dup(a), a and b are distinct descriptor numbers referring to one open file description. If a initially has offset zero, a read through a advances the shared offset. A later read through b starts from that advanced position.
This is not a property of the pathname or inode alone. Two independent calls to open("events.log", O_RDONLY) create separate open file descriptions and therefore separate offsets. Sharing arises from the reference relationship created by duplication, inheritance, or another interface that preserves the same open file description.
A seek through one alias moves the other
The file offset belongs to the open file description. lseek() changes that offset rather than storing a position inside the descriptor-table entry.
A compact sequence makes the coupling visible:
int a = open("data.bin", O_RDONLY);
int b = dup(a);
lseek(b, 4096, SEEK_SET);
char buf[128];
read(a, buf, sizeof buf);Subject to successful calls and a seekable file, the read(a, ...) begins at offset 4096. It then advances the shared offset by the number of bytes read.
This coupling matters when code treats a duplicated descriptor as an isolation boundary. Passing a duplicate to another component does not give that component an independent cursor. A seek or ordinary position-changing I/O operation can alter the position used by the original holder.
An independent cursor requires an independent open file description, commonly produced by another open() when the target and access model permit it. Even then, pathname races and replacement semantics can matter if reopening by name is used after the original open. The choice between duplication and reopening is therefore also a choice about object identity and namespace lookup.
File status flags follow the shared description
The shared state is broader than the offset. File status flags retrieved with fcntl(fd, F_GETFL) belong to the open file description. Flags that can be changed with F_SETFL, such as O_APPEND or O_NONBLOCK where applicable, are consequently visible through descriptors that reference that same description.
If code enables O_APPEND through one duplicate, writes through another duplicate use the same append status. The change is not scoped to the integer descriptor used in the fcntl() call.
That behavior can cross abstraction boundaries. A library handed a duplicate might reasonably appear to have its own handle, yet changing a mutable file status flag can affect I/O performed elsewhere through another alias. Descriptor duplication isolates lifetime references more readily than it isolates I/O state.
The exact effect of a status flag still depends on the object type and platform contract. O_NONBLOCK, for example, has meaningful behavior for interfaces where nonblocking operation is defined. Sharing the flag does not imply that every underlying object reacts to it in the same manner.
Descriptor flags stay local to each descriptor
File descriptor flags occupy a different layer. FD_CLOEXEC, manipulated through F_GETFD and F_SETFD, is associated with a particular descriptor rather than the shared open file description.
That distinction permits two aliases to have different inheritance behavior across exec while retaining the same offset and status flags.
A plain dup() returns a duplicate whose close-on-exec flag is clear. Interfaces such as F_DUPFD_CLOEXEC and, on Linux, dup3() with O_CLOEXEC can create a duplicate with close-on-exec set atomically.
The atomic form matters in multithreaded programs that create descriptors while another thread can execute a new program image. Creating a descriptor and setting FD_CLOEXEC in a later operation leaves an interval in which the descriptor exists without the intended flag. An interface that applies close-on-exec during descriptor creation removes that particular interval.
The separation can be summarized as two layers:
descriptor a ----+
+---- open file description ---- file
descriptor b ----+ |
+-- file offset
+-- file status flags
descriptor a: descriptor flags
descriptor b: descriptor flagsThe descriptor-local flags do not become shared merely because both arrows reach the same open file description.
Fork preserves the same sharing relation
Descriptor sharing is not limited to dup(). After fork(), the child inherits descriptors that refer to the same open file descriptions as the corresponding descriptors in the parent.
For a regular seekable file, parent and child can therefore affect a shared offset. If one process seeks, the other process observes the resulting position through its inherited alias. Reads and writes that advance the offset also operate against that common open-file-description state.
This does not mean the parent and child share their descriptor tables. Each process has its own descriptors, and either process can close or alter descriptor-local flags without directly changing the other’s descriptor-table entry. The shared object sits one layer below those tables.
The lifetime of the open file description also follows references rather than a single descriptor number. Closing one alias removes that reference. The underlying open file description remains active while another descriptor still refers to it.
This reference model is visible in interfaces beyond ordinary file I/O. On Linux, epoll registrations are tied to the combination of a descriptor and its underlying open file description in ways that can make duplicate descriptors relevant to event-registration lifetime. Closing one descriptor does not necessarily eliminate the underlying open description while aliases remain.
Closing an alias does not revoke the others
A successful close(a) invalidates descriptor a in that process. It does not invalidate b when b is a duplicate referring to the same open file description.
This is useful for deliberate ownership transfer. A process can duplicate a descriptor, pass or retain one reference, then close another. The shared open file description persists until its reference lifetime ends according to the operating-system rules.
It also limits the meaning of close as a cancellation mechanism. If another part of the process, a child process, or a recipient of descriptor passing holds an alias, closing the local descriptor does not revoke that alias. Resource ownership therefore cannot be inferred solely from one descriptor number.
The same point applies to pathname removal. An open file description refers to an opened object independently of later pathname changes. Removing or replacing the pathname does not retarget an already open descriptor to a new file. Descriptor duplication preserves that existing open-object reference rather than repeating pathname resolution.
Shared offsets are coordination state
A shared offset can be useful when multiple actors intentionally consume one sequential stream position. It can also become hidden coordination when those actors were expected to operate independently.
Position-independent I/O avoids changing the shared file offset. Interfaces such as POSIX pread() and pwrite() operate at an explicit offset and do not update the open file description’s current offset. They can therefore reduce cursor interference when the object and operation fit those interfaces.
That does not turn duplicated descriptors into fully independent handles. File status flags and the underlying open object remain shared at the open-file-description layer, and concurrent I/O has additional ordering and atomicity rules that depend on the operation and platform.
The practical boundary is structural: duplication creates another name for existing open state. A separate open() creates new open state. Those operations can point toward the same file while carrying different sharing semantics, and code that treats them as interchangeable can accidentally couple cursor position, append mode, nonblocking mode, lifetime, or process inheritance behavior.
The integer descriptor exposes none of that topology by itself. Correct reasoning depends on the layer that owns each piece of state: descriptor-table entry, open file description, or underlying file object.