Learn › Bash Redirection and File Descriptors › A descriptor is not a filename
A descriptor is not a filename. Try the examples, inspect their output, and explain the result. Practice feedback does not award XP or mastery.
Two readers use the same file. Sometimes both start at the first byte; sometimes the second starts where the first stopped. The pathname alone cannot explain the difference.
A file descriptor is a numbered entry owned by a process. An open file description is the kernel object that entry references. For a regular file, it holds the current offset, the position of the next read or write. Opening a pathname creates an open file description. Duplicating a descriptor shares an existing one. Linux open documentation.
This optional chapter requires Linux, Bash, and Python 3. /proc examples describe the Linux kernel, not a simulated terminal. Work in a private practice directory. Python makes byte-sized reads explicit; you do not need to write Python from memory.
In your managed Bash practice terminal
Run camp-redirection 04 to enter this chapter's private directory. While this lab machine is running, the same command returns to it after reconnecting; camp-redirection 04 --fresh creates a new one without deleting earlier files. Run the code blocks there and use cd to leave.
We create a three-byte file. Bash's read -N 1 reads exactly one character here, where the fixture uses simple ASCII. -r disables backslash interpretation, and -u selects the descriptor.
The fixture starts as ABC. Predict the two characters read through descriptors 3 and 4 when descriptor 4 duplicates 3. Then match the result to the offsets in the table.
printf ABC > letters.txt
(
exec 3< letters.txt
exec 4<&3
IFS= read -r -N 1 -u 3 first
IFS= read -r -N 1 -u 4 second
printf 'fd3=%s fd4=%s\n' "$first" "$second"
exec 3<&- 4<&-
)
Expected output
fd3=A fd4=B
| Reader subshell after each action | fd 3 | fd 4 | Offset |
|---|---|---|---|
| Open file | Open A | Closed | A: 0 |
| Duplicate fd 3 | A | Same A | A: 0 |
| Read via fd 3 | A | A | A: 1 |
| Read via fd 4 | A | A | A: 2 |
The duplicate does not receive an independent copy of the position. Both entries refer to A. Closing either descriptor removes that reference; the other can still be used.
Now replace duplication with another input open. Everything else stays the same.
The fixture starts as ABC; both readers should get A because each pathname open has its own offset. Compare that with the duplicate-reader result.
printf ABC > independent.txt
(
exec 3< independent.txt
exec 4< independent.txt
IFS= read -r -N 1 -u 3 first
IFS= read -r -N 1 -u 4 second
printf 'fd3=%s fd4=%s\n' "$first" "$second"
exec 3<&- 4<&-
)
Expected output
fd3=A fd4=A
Here fd 3 refers to open A and fd 4 to open B. After both reads, A and B each have offset 1. Both objects refer to the same underlying file, but their positions are independent.
Do not replace a controlled one-byte reader with an arbitrary buffered program and expect the same experiment. A program may read ahead. Distinguish how much it fetched from the kernel from how much text it displayed.
An inherited descriptor can refer to the same open file description across a process boundary. This child consumes one byte using a raw operating-system read. The parent then reads the next byte.
The child reads A through inherited fd 3; the parent should then read B from the same advancing open-file position.
printf ABC > inherited.txt
(
exec 3< inherited.txt
python3 -c 'import os; print("child=" + os.read(3, 1).decode("ascii"))'
IFS= read -r -N 1 -u 3 next
printf 'parent=%s\n' "$next"
exec 3<&-
)
Expected output
child=A
parent=B
The parent waits for this foreground command, so there is no scheduling race. The child's table and the parent's table contain separate descriptor entries, both pointing to open A. They are not sharing the same process-local table.
Linux exposes descriptor information under /proc/PID/fd and /proc/PID/fdinfo, where PID is a process identifier. self selects the inspecting process. A descriptor link shows what it refers to, while fdinfo includes the file position.
This Python example opens and duplicates a file, then reports stable facts. It does not print variable process IDs or absolute paths.
printf ABC > proc-example.txt
python3 - <<'PY'
import os
fd = os.open("proc-example.txt", os.O_RDONLY)
copy = os.dup(fd)
try:
os.read(fd, 1)
def position(number):
with open(f"/proc/self/fdinfo/{number}") as info:
return next(line.split()[1] for line in info if line.startswith("pos:"))
print("positions=" + position(fd) + "," + position(copy))
print("same target=" + str(os.readlink(f"/proc/self/fd/{fd}") == os.readlink(f"/proc/self/fd/{copy}")))
finally:
os.close(copy)
os.close(fd)
PY
The summary is positions=1,1 and same target=True. Equal paths and positions alone do not prove sharing; the controlled duplication and read establish it. Two independent opens could coincidentally show matching values.
An open file description also stores file status flags, such as append mode. Duplicates share those flags. A descriptor's close-on-exec flag belongs to its individual table entry. It controls whether that entry survives successful program replacement. Duplication and inheritance APIs have their own defaults. Linux duplication documentation.
Python deliberately creates non-inheritable descriptors by default. This Linux example shows that changing inheritance on one duplicate does not change the other. It demonstrates the distinction without teaching Python defaults as Bash defaults.
python3 - <<'PY'
import os
fd = os.open("flag-example.txt", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
copy = os.dup(fd)
try:
os.set_inheritable(fd, True)
os.set_inheritable(copy, False)
print("inheritable=" + str(os.get_inheritable(fd)) + "," + str(os.get_inheritable(copy)))
finally:
os.close(copy)
os.close(fd)
PY
The output is inheritable=True,False. A shared offset does not imply every descriptor property is shared. Duplication also does not turn a read-only open into a writable one.
Try this reasoning task before reading the explanation. A parent opens a file, passes its descriptor to a child, and waits. The child reads two bytes. The parent then finds its next read begins at byte three. Would reopening the pathname in the child preserve the parent's position?
Yes, for this regular-file example. Reopening creates open B with an independent position. Duplication would keep sharing open A and reproduce the symptom. If your repair still skips bytes, check whether the child received a duplicate rather than opening the path.
For additional Linux inspection, strace can show opens, descriptor duplication, reads, and closes. Trace output includes process IDs, paths, and implementation-dependent descriptor numbers. Follow the process labels and returned descriptor numbers; do not compare the whole trace to a memorized transcript. A trace is optional here, and no mandatory example requires strace to be installed.
Change the three-byte fixture to WXYZ. Have one process read two bytes before another reads one. Demonstrate both shared and independent opens, then draw the two process tables and the open file descriptions they reference.
Keep your evidence bounded: the bytes read, open versus duplicate operations, and final positions. This model applies to regular files. Pipes do not have a seekable file position, so do not extend the offset experiment to a pipe.
Next, investigate a different boundary: when Bash treats a here-document body as literal text and when it expands that text before a command reads it.
Practice Chapter 4. A descriptor is not a filename in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.