Memra

And that is the whole trick behind dup

◈ 10 cards

State the lowest-free-slot rule, follow the descriptor triangle from the fd table through the open-file table to the inode, and redirect a program output with dup and dup2.

Three tables, not one

A descriptor is a small integer, and the temptation is to imagine it is the file. It is not. Between your int and the bytes on disk sit three structures, and knowing which one holds what is the most examinable fact in this whole module.

The per-process file descriptor table. One array per process, indexed by descriptor number. Every process starts with slots 0, 1 and 2 already filled by the kernel — standard input, standard output, standard error — which is why your first open() normally comes back as 3. Each slot is a pointer into the next table.

The system-wide open-file table. One entry per opening of a file, shared by every process on the machine. This is where the file offset lives, along with the status flags the file was opened with (O_APPEND, the access mode) and a reference count.

The inode table. One entry per file. Type, permission bits, owner, link count, size, timestamps, and the pointers to the disk blocks.

Why the middle table has to exist

Because a single file can be open several times at once, and each opening needs its own place in the file. The inode is shared — there is exactly one per file — so it cannot hold a per-opening offset. That offset has to live somewhere in between. That somewhere is the open-file table.

Everything about descriptor sharing falls out of that one design decision:

  • Two open() calls on the same file — even in the same process — produce two open-file-table entries pointing at one inode. Two descriptors, two independent offsets. Reading 10 bytes through the first leaves the second still at 0.
  • fork() copies the descriptor table, so the child gets its own slots that point at the same open-file-table entries. Two descriptors in two processes, one offset. If parent and child both read, they divide the file between them rather than each reading all of it.
  • dup() does the same thing inside one process: a second slot pointing at the same entry. One offset, seen through two numbers.

So the question "do these two descriptors share an offset?" is really "do they point at the same open-file-table entry?", and the answer is yes for fork and dup, no for two opens.

The lowest-free-slot rule

When a call needs a new descriptor, the kernel scans the process descriptor table from slot 0 upward and takes the first free slot. Not the next one after the highest used; the lowest free one. That applies to open(), to dup(), to pipe() and to socket() alike.

You can demonstrate it in three lines. Open a file in a fresh program and it gets 3. Insert close(0) first and the same open() returns 0, because slot 0 is now the lowest free one and the kernel does not care that it used to be standard input.

Worked example — redirecting your own output

That rule is not trivia. It is the entire mechanism behind the shell's >.

Open a file: it lands in slot 3. Now close(1) — standard output is gone, and slot 1 is the lowest free slot in the table. Call dup(fd), which duplicates a descriptor into the lowest free slot. It must land in slot 1. And slot 1 is where printf() writes.

int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
close(1);
dup(fd);                       /* necessarily lands in slot 1 */
printf("this goes into out.txt\n");

Nothing about printf() changed. It still writes to descriptor 1; descriptor 1 now points at the file's open-file-table entry.

Why dup2 replaces that pair

The close/dup sequence has a window. Between the close(1) and the dup(fd), slot 1 is free — and if a signal handler runs in that window and opens anything, it takes slot 1, and your dup() lands in slot 4 pointing at nothing useful. In a threaded program the window is worse still.

dup2(fd, 1) closes the target and duplicates into it atomically: no window, and the target is named explicitly instead of being inferred from the state of the table.

int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dup2(fd, 1) == -1) { perror("dup2"); return 1; }
close(fd);                     /* slot 3 is now redundant */
printf("this goes into out.txt\n");

Three details of dup2 are examinable. If fd is valid and equals the target, it is a no-op and nothing is closed. If fd is valid and differs, the target is closed first, then duplicated into. If fd is invalid, the call fails with EBADF and — importantly — the target is not closed. And close(fd) afterwards is right, not optional tidying: the file stays open through slot 1, because the open-file-table entry is only released when its reference count reaches zero.

Redirecting input is the mirror image: dup2(in, 0) and every subsequent scanf or fgets reads the file.

a slot points at an entryan entry points at an inodefd tableper process: 0, 1, 2, 3 ...open-file tablesystem-wide: OFFSET + status flagsinodeone per file: type, perms, links, blocksTwo open() calls -> two entries -> twoindependent offsets. fork() or dup() -> twoslots -> ONE entry -> ONE shared offset.
The descriptor triangle. The offset lives in the middle table — which is the only reason two openings of one file can be in two different places.
0123after openttyttyttyout.txtfdclose(1)ttyttyout.txtlowest freedup(fd)ttyout.txtttyout.txtstdoutfdSlots 1 and 3 now point at ONE open-file-table entry, so they share one offset. printf() writes to thefile.
After `close(1)`, slot 1 is the lowest free slot — so `dup(fd)` has nowhere else to go.
NORMAL ~/memra/learn/comp-325/descriptors-are-handed-out-lowest-first utf-8 LF