A backend service opens its configuration file and receives the integer 3. It creates a listening socket and receives 4. It opens a log and receives 5.
The numbers are file descriptors. They are small, nonnegative integers through which a Unix process refers to open files and other I/O objects.
The number itself contains no file data, pathname, or hardware address. It is an index into a kernel-managed table associated with the process.
A file descriptor is a process-local handle that identifies one entry in the process's table of open resources.
That definition explains why descriptor 3 can mean a log file in one process and a network socket in another, why two descriptors can share a file offset, and why closing a descriptor does not necessarily close the underlying object for everyone.
An application often begins with a pathname:
The kernel can use that name to locate a file and check whether the requested access is allowed. Repeating that work for every byte read or written would be inefficient and semantically awkward. Names can change, and the operation also needs state that is not part of the pathname, such as the access mode and current position.
Opening separates two phases:
The object is located and opened once, the process receives a file descriptor, and that descriptor is used for later operations.
On success, a call such as open() installs an entry in the calling process's descriptor table and returns the entry's index:
Subsequent operations supply fd rather than resolving the pathname again. The kernel validates the integer, finds the corresponding table entry, and follows it to the open object.
This creates a stable reference. Renaming the file after it has been opened does not rewrite the integer or force the process to locate the file again. The descriptor continues to refer to the object that was opened until the descriptor is closed or replaced.
The detailed behavior of opening, reading, writing, and seeking is separate from the descriptor structure. Here, open() matters because it shows how a descriptor enters the table.
Each process has a descriptor table. A descriptor number has meaning only relative to that table.
Suppose two processes both use descriptor 3:
There is no conflict. The integer 3 is not a system-wide object ID. The kernel interprets it together with the calling process.
This is the same general idea as a virtual address. Two processes can use the same numeric virtual address for different memory because each has its own address-space mappings. Similarly, two processes can use the same descriptor number for different resources because each has its own descriptor table.
A successful open() normally returns the lowest-numbered descriptor not currently in use by the process. If descriptors 0, 1, and 2 are occupied, the next result is commonly 3. If descriptor 1 has been closed, however, a later open can return 1.
Correct code therefore treats the returned number as an opaque result:
It should not assume that a newly opened object will always receive descriptor 3 or any other particular value.
Ordinary threads in the same process share the descriptor table. If one thread opens an object, the other threads can use the returned descriptor. If one thread closes that entry, it disappears from the shared table for all of them.
The phrase “descriptor points to a file” is a useful shortcut, but it hides a middle layer. For regular files, the accurate model has three levels:
In this example:
3 and 4 refer to the same open file description.5 refers to a different open file description.The distinction determines which state is shared and which state is independent.
Loading simulation...
The descriptor table entry is intentionally small. Conceptually, it contains:
The open file description holds state associated with one opening. For a seekable file, that state includes the current file offset. It also includes the access mode and file status flags such as append or non-blocking behavior when those flags apply.
The underlying file-system object holds the file's data and persistent metadata. Multiple open file descriptions can refer to the same object while maintaining separate per-opening state.
Operating systems use different internal structures and names, but POSIX uses open file description for this middle-layer concept. On Linux, it corresponds conceptually to a system-wide open-file structure maintained by the kernel.
The middle layer is the key to reasoning about sharing. Two integers do not share an offset merely because they refer to the same file. They share an offset only when they lead to the same open file description.
Suppose a file contains:
A process opens it twice:
Each successful open() normally creates a new open file description:
Two separate open descriptions means two separate offsets. Reading through one does not move the other.
Reading one byte through descriptor 3 returns A and advances open description A's offset to 1. Open description B remains at offset 0, so reading through descriptor 4 also returns A.
The file object is shared, but the current positions are independent.
This behavior is useful when two parts of a program need to scan the same file independently. Merely opening the same pathname does not force the two users to coordinate one cursor.
The exact data returned by concurrent access can still depend on changes to the underlying file. Independent offsets do not create independent copies of the file's bytes.
dup()The dup() system call takes an existing descriptor and installs another descriptor-table entry that refers to the same open file description:
The result looks like:
Here the two descriptors share one open description, so they share the offset. A read through fd 3 advances what fd 4 will see next.
Now a read through descriptor 3 advances the offset observed through descriptor 4 as well:
The two table entries are distinct, so closing descriptor 3 does not remove descriptor 4. The remaining descriptor still reaches the shared open file description.
This is different from copying an integer variable:
That statement creates no new table entry. alias and original are merely two user-space variables containing the same descriptor number. Closing through either variable removes the one shared table entry, leaving both integers stale.
The difference is:
Keeping those three cases separate prevents many descriptor-lifetime bugs.
dup2()dup() chooses the lowest available descriptor number. Sometimes a program needs the duplicate to have a specific number. dup2(oldfd, newfd) makes newfd refer to the same open file description as oldfd.
If newfd was already open, dup2() replaces it as part of the operation. This atomic replacement is important because a separate close(newfd) followed by dup(oldfd) would leave a window in which another thread or signal handler could acquire the desired number.
Shell output redirection is the classic use:
After step 2:
After step 3, descriptor 1 remains. The program can write to standard output without knowing that the shell connected it to a regular file.
Input redirection and pipelines use the same principle. They arrange the standard descriptor numbers before the target program begins executing.
Unix programs conventionally begin with three descriptors:
These are conventions, not special integer types built into the CPU. The process's descriptor table determines what each number currently references.
In an interactive shell, all three often lead to a terminal. With redirection, they can lead elsewhere:
The service still reads descriptor 0 and writes descriptors 1 and 2. The shell changes the table entries before starting the service:
Standard error is separate from standard output so diagnostics can go to a different destination from ordinary results.
A process is allowed to close any of these descriptors. If descriptor 0 is closed, the next operation that allocates the lowest available descriptor may reuse 0 for an unrelated object. Robust programs do not assume that descriptors below 3 are always present or retain their usual meaning.
fork()When a process calls fork(), the child receives a copy of the parent's descriptor table. Corresponding entries in the two tables refer to the same open file descriptions.
Assume the parent has descriptor 3 open at offset 20:
If the parent performs an operation that advances the offset to 30, the child observes offset 30 through its corresponding descriptor. The offset belongs to the shared open file description, not to either descriptor table.
The tables themselves are normally distinct. If the child closes its descriptor 3, the parent's entry remains installed. If the parent later opens another file, that new table entry does not appear automatically in the child.
This combination is subtle:
It lets a shell create a child, rearrange the child's descriptors, and leave the parent's own table intact. It also means a parent and child can interfere with each other's file position if both use an inherited regular-file description without coordination.
exec()A successful exec() replaces the program code and process memory, but it does not create a new process. Open descriptors normally remain in the process's descriptor table.
That behavior is essential for shell redirection:
output.log, makes fd 1 refer to it, and calls exec().fd 1 and writes ordinary standard output.output.log receives the bytes.The new program does nothing unusual. Redirection works because the descriptor was rearranged before it started.
Descriptor inheritance is useful when deliberate and dangerous when accidental. A server might unintentionally expose a client connection, secret file, or internal control pipe to a program it launches. An inherited pipe endpoint can also keep a pipe alive and prevent another process from observing the expected end condition.
The close-on-exec descriptor flag solves this problem. When set on a descriptor-table entry, the kernel closes that descriptor if exec() succeeds.
Many descriptor-creating interfaces support an atomic close-on-exec option such as O_CLOEXEC. Setting the property during creation avoids a race in a multithreaded process:
Close-on-exec belongs to the descriptor entry, not the shared open file description. Two duplicated descriptors can therefore reach the same open description while having different close-on-exec settings.
close(fd) tells the kernel to remove one entry from the calling process's descriptor table:
If other descriptors still refer to the same open file description, they remain valid:
Closing one descriptor removes only that table entry. The open description survives because another descriptor still refers to it.
The kernel keeps reference counts internally. When the final reference to an open file description disappears, the kernel can release that open state. The underlying persistent file can continue to exist independently in the file-system namespace.
Process termination closes the process's remaining descriptors. Relying on termination for routine cleanup is still poor resource ownership. A long-running service that continually opens resources without closing them retains kernel state and eventually reaches a limit.
Closing also makes the integer available for reuse. This can turn a stale descriptor into a dangerous bug:
The failure is not guaranteed to appear as “bad descriptor.” Reuse can make the stale integer valid for the wrong object. Multithreaded programs therefore need clear descriptor ownership and synchronization around close.
The three-level model separates two commonly confused flag categories.
A descriptor flag belongs to one descriptor-table entry. Close-on-exec is the main example. Changing it for descriptor 3 does not automatically change it for descriptor 4, even when both entries refer to the same open file description.
A file status flag belongs to the open file description. Examples include append and non-blocking status. Descriptors created by dup() share these flags because they share the middle-layer object.
The close-on-exec flag belongs to each descriptor, while the append mode belongs to the shared open description. Two descriptors can therefore differ in one and agree on the other.
Changing the shared open-description status through one descriptor can affect operations performed through the other. Changing a per-descriptor flag affects only that table entry.
The access mode, such as read-only or write-only, is also established for the open file description. Duplicating a read-only descriptor does not create a writable one.
The name file descriptor is historical and broader than “descriptor for a regular file.” Unix uses descriptors for many open I/O resources:
The descriptor-table lookup is common. What happens after that lookup depends on the underlying object and operation.
One call reaches four different implementations. The descriptor decides which, which is why the same read works on a file, a pipe, a terminal, and a socket.
Not every object has a meaningful byte offset. A pipe or stream socket delivers a stream of bytes but cannot generally seek to “byte 500.” A listening socket accepts connections rather than supplying stored file contents. The shared descriptor interface unifies access and lifetime management without pretending that every object has regular-file semantics.
A descriptor table cannot grow without bound. The operating system enforces a per-process limit, and the system also has finite kernel memory for open objects.
When a process has no available descriptor under its limit, an operation that needs a new one can fail with EMFILE. A system-wide shortage can produce ENFILE on systems that use that distinction.
These failures matter to backend services because accepting a socket, opening a log, creating a pipe, and connecting to another service can all require descriptors. A leak in any of those paths can eventually prevent unrelated I/O.
The important rule here is lifecycle ownership:
Every successfully acquired descriptor must either be deliberately transferred to another owner or eventually closed.
Resource-limit configuration and production diagnosis add further concerns, but they do not change the descriptor model.
/procLinux exposes a process's descriptor table through /proc/<pid>/fd. Each entry is named by a descriptor number and appears as a symbolic link describing the referenced object.
In a shell, inspect its standard descriptors:
The targets depend on how the shell was started. In a terminal, they may refer to a pseudoterminal. In an IDE, container, or redirected shell, they may point to pipes or other objects.
Open a file on descriptor 7 in the current shell:
The output commonly identifies /etc/hostname. The exact display can vary with mounts and namespaces, but descriptor 7 now appears in the shell's table.
Close it:
The final command reports that the entry no longer exists. /etc/hostname itself has not been deleted; only this process's open table entry was removed.
/proc/<pid>/fdinfo/<fd> exposes additional information for a descriptor. For the open descriptor, this command can show fields including its current position and flags:
The numeric flag representation is kernel-facing and not necessary to memorize. The useful observation is that the kernel maintains state beyond the integer stored in the shell.
Access to another process's /proc entries can be restricted by credentials, mount options, or security policy. A descriptor may grant access to sensitive data, so exposing descriptor information is itself security-relevant.
The following program opens one file twice and also duplicates one of those descriptors. Single-byte reads make the offset relationships visible without depending on internal kernel structures:
Create the input, compile, and run:
A typical run prints:
The exact descriptor numbers can differ. The characters reveal the structure:
This experiment distinguishes shared file data from shared open state. All three descriptors ultimately reach the same file object, but only two share an open file description.
A file descriptor is a nonnegative, process-local integer that indexes a descriptor-table entry. The entry refers to an open file description, which holds per-opening state such as the current offset, access mode, and file status flags. That open description then refers to the underlying file or I/O object.
Separate calls to open() normally create independent open file descriptions, even for the same file. dup() creates another table entry for the same description, so the duplicates share offset and status. Copying the integer alone creates no new kernel reference.
After fork(), parent and child have separate descriptor tables whose corresponding entries share open file descriptions. A successful exec() normally preserves descriptors unless their close-on-exec flag is set. close() removes one entry, while other references remain valid.
Descriptors 0, 1, and 2 conventionally represent standard input, output, and error. Redirection works by changing what those entries reference. Regular files, pipes, sockets, terminals, and devices can all use the descriptor interface while retaining different semantics.
The central mental model is:
Descriptor number → process table entry → open file description → underlying object.
5 quizzes