Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Docket, a gopher, stands at a wall of pigeonholes numbered 0 to 5. Holes 0, 1 and 2 hold tickets for the terminal, and a string runs from the ticket in hole 3 to a filing cabinet.

File Descriptors

Every read, write and close in a program takes a small integer as its first argument. That integer is a file descriptor. It is the only handle a process has on a file, a pipe, a terminal or a network connection, and the kernel translates it on every call into the object the process means.

In this chapter, we will follow that translation from the integer to the bytes on disk. We will meet the kernel structures on the way, one at a time and in the order a read visits them: the descriptor table of the process, the open file, the directory entry, and the inode. For each structure we will look at its definition in the kernel source, say what each field is for, and confirm its behaviour with an experiment. Then we will write fdfacts, which measures all of this on a running kernel, and fdtable, a model of the same structures that gives the same results.

This chapter covers

  • why a process refers to a file by a number
  • the path of one read through the kernel
  • the descriptor table: struct fdtable, the lowest-free-number rule, and the limit on descriptors
  • the open file: struct file, the offset, and the status flags
  • names and files: directory entries, struct inode, and hard links
  • reference counts: when an open file and an inode are released
  • what dup, fork and exec do to descriptors
  • shell redirection, including 2>&1
  • how to inspect the descriptors of a running process

Why a number

A program runs in user mode, in which the processor lets it touch only its own memory. The kernel runs in kernel mode and keeps its data in memory that user-mode code cannot read or write. A system call is the controlled way from one to the other: the program puts a call number and arguments in registers and executes an instruction that switches the processor to kernel mode at a fixed entry point.

The objects that describe an open file live in kernel memory. The kernel cannot hand a process a pointer to them, because the process could not use the pointer and must not be able to forge one. It keeps a table for each process and gives the process an index into that table. The process can pass any integer it likes. The kernel looks the integer up in the caller’s own table, so a process can only ever name the files that the kernel has put there.

By convention the first 3 entries have fixed roles: descriptor 0 is the standard input, 1 the standard output, and 2 the standard error. A program started from a terminal finds all 3 referring to that terminal.

One read, step by step

Suppose a process has opened a file and received descriptor 3, and now calls read(3, buf, 100). The kernel does the following.

  1. It takes entry 3 of the calling process’s descriptor table. The entry is a pointer to a struct file, the kernel’s record of one open file. An empty entry ends the call with the error EBADF, “bad file descriptor”.
  2. It reads the current position in the file from the struct file. This position is the offset: the number of bytes from the start of the file at which the next read or write takes place.
  3. It calls the read function of whatever the file is. The struct file holds a table of functions, and a disk file, a pipe and a socket each supply their own.
  4. For a disk file, that function finds the file’s inode, the kernel’s record of the file itself, and through it the cached pages of the file. The page cache is the subject of chapter 8.
  5. It copies up to 100 bytes from those pages into buf, in the memory of the process.
  6. It adds the number of bytes copied to the offset and returns that number.
The path of read(3, buf, 100) through the kernel. The process enters the kernel. Entry 3 of the descriptor table gives a struct file. The struct file supplies the offset, f_pos. The read function of the file system is found through f_op. The struct inode gives the size of the file and its page cache. The bytes are copied to buf, and f_pos is increased by the number of bytes copied.
Figure 2.1 The path of a read through the kernel.

The steps name 3 objects: the table entry, the open file, and the inode. The rest of the chapter takes them in that order. The question to keep in mind for each is which state it holds, because that decides what is shared when a descriptor is duplicated or inherited.

The descriptor table

Each process has a struct files_struct, which contains its descriptor table. The table itself is a struct fdtable. These are the definitions from include/linux/fdtable.h in Linux 5.15, with the fields that concern locking left out.

struct fdtable {
        unsigned int max_fds;          /* number of entries in fd[] */
        struct file **fd;              /* the table: one pointer per descriptor */
        unsigned long *close_on_exec;  /* bitmap: descriptors closed by exec */
        unsigned long *open_fds;       /* bitmap: descriptors in use */
        /* ... */
};

struct files_struct {
        atomic_t count;                /* processes and threads sharing this table */
        struct fdtable *fdt;           /* the current table */
        unsigned int next_fd;          /* where the search for a free number starts */
        struct file *fd_array[64];     /* a first table, large enough for most programs */
        /* ... */
};
FieldMeaning
fdAn array of pointers. fd[3] is the open file behind descriptor 3, or NULL.
open_fds1 bit per descriptor, set while the number is in use.
close_on_exec1 bit per descriptor: the close-on-exec flag, described below.
max_fdsThe current length of fd. The table starts with 64 entries and is replaced by a larger one when a higher number is needed.
countHow many tasks use this table. The threads of a process share 1 table.

The table gives rise to 2 rules that every Unix program depends on.

A new descriptor gets the lowest free number. The kernel searches open_fds for the first clear bit, starting at next_fd. After a process closes descriptor 0, the next open returns 0.

The number of descriptors is limited. Each process has a resource limit, RLIMIT_NOFILE, and an open that would need a number at or above it fails with EMFILE, “too many open files”. ulimit -n shows the limit in a shell. On this machine it is 1048576 for a process started from the terminal, and many systems set 1024. The kernel as a whole has a second limit, on the number of open files in all processes together. /proc/sys/fs/file-nr shows the current number first:

$ cat /proc/sys/fs/file-nr
17312	0	9223372036854775807

A server that forgets to close descriptors, a descriptor leak, eventually hits EMFILE and can accept no more connections, although memory and CPU are free.

The open file

The pointers in the table refer to struct file objects. The POSIX standard calls this object an open file description, and the name is worth learning because the manual pages use it. A struct file is created by each successful open, and also by pipe, socket and accept. This is the definition from include/linux/fs.h, reduced to the fields this chapter uses.

struct file {
        struct path f_path;                    /* the directory entry it was opened by */
        struct inode *f_inode;                 /* the file itself */
        const struct file_operations *f_op;    /* read, write, ... for this kind of file */
        atomic_long_t f_count;                 /* references to this struct file */
        unsigned int f_flags;                  /* O_APPEND, O_NONBLOCK, ... */
        fmode_t f_mode;                        /* opened for reading, writing, or both */
        loff_t f_pos;                          /* the offset */
        /* ... */
};
FieldMeaning
f_posThe offset. read and write start here and advance it. lseek sets it.
f_flagsThe status flags given to open, such as O_APPEND (every write goes to the end of the file) and O_NONBLOCK (calls return EAGAIN where they would block). fcntl with F_GETFL and F_SETFL reads and changes them.
f_modeWhether the file was opened for reading, for writing, or both. A write on a file opened read-only fails with EBADF.
f_countThe number of references to this struct file: table entries in any process, plus system calls in progress.
f_inodeThe inode of the file.
f_opA table of function pointers. read calls f_op->read_iter, write calls f_op->write_iter.

f_op is the mechanism behind the phrase “everything is a file”. The system call read contains no code for disks, pipes or sockets. It calls through f_op, and each kind of object installs its own functions when the struct file is created: ext4 for a file on disk, the pipe code of chapter 1 for a pipe, TCP for the socket of chapter 10. A program that reads descriptor 0 therefore works unchanged whether descriptor 0 is a terminal, a file or a pipe.

The important point for the rest of the chapter is where the offset lives. It is in the struct file, and neither in the table entry nor in the inode. Table entries that point to the same struct file share an offset. Separate struct file objects for the same file on disk each have their own.

Names, directory entries and inodes

A struct file refers to an inode. The inode is the file: its type, its owner, its permissions, its size, its timestamps, and the location of its data. It is identified by a number, the inode number, which is unique within a file system.

An inode contains no name. Names are kept in directories. A directory is itself a file whose contents are a list of directory entries, each a pair of a name and an inode number. Opening /tmp/x/report.txt means reading the directory /, finding the entry tmp and its inode, reading that directory, and so on to the last name. The kernel caches each step in a struct dentry, so that a path which has been used recently is resolved without reading directories from disk.

struct dentry {
        struct dentry *d_parent;       /* the directory that contains this entry */
        struct qstr d_name;            /* the name: a single component of a path */
        struct inode *d_inode;         /* the inode the name refers to */
        /* ... */
};

struct inode {
        umode_t i_mode;                /* type (file, directory, ...) and permissions */
        kuid_t i_uid;                  /* owner */
        kgid_t i_gid;                  /* group */
        unsigned long i_ino;           /* the inode number */
        unsigned int i_nlink;          /* directory entries that refer to this inode */
        loff_t i_size;                 /* size in bytes */
        struct timespec64 i_atime;     /* last access */
        struct timespec64 i_mtime;     /* last change of the contents */
        struct timespec64 i_ctime;     /* last change of the inode */
        blkcnt_t i_blocks;             /* disk space used, in 512-byte units */
        atomic_t i_count;              /* references from inside the kernel */
        const struct file_operations *i_fop;   /* copied to f_op at open */
        struct address_space *i_mapping;       /* the cached pages of the file */
        /* ... */
};

stat prints the fields of an inode, and ls -i prints inode numbers. Since the name is outside the inode, several directory entries can hold the same inode number. Such an entry is a hard link, created by ln or the link system call. On this machine:

$ echo hello > report.txt
$ ln report.txt copy.txt
$ ls -li
19927765 -rw-rw-r-- 2 jetson jetson 6 Oct 12 22:56 copy.txt
19927765 -rw-rw-r-- 2 jetson jetson 6 Oct 12 22:56 report.txt
$ stat report.txt
  File: report.txt
  Size: 6         	Blocks: 8          IO Block: 4096   regular file
Device: 10301h/66305d	Inode: 19927765    Links: 2
Access: (0664/-rw-rw-r--)  Uid: ( 1000/  jetson)   Gid: ( 1000/  jetson)
The output of ls -li and stat explained. In each line of ls, 19927765 is the inode number, the 2 after the permissions is the link count, and 6 is the size. Both lines have the same inode number, so they are the same file, and only the name differs. stat prints the same fields with their names, and Blocks counts units of 512 bytes.
Figure 2.2 The same output, with the inode field behind each value.

Both names show inode 19927765. The 2 after the permissions, Links: 2 in the output of stat, is i_nlink. The names are equal in every respect: neither is the original, and a change made through one is visible through the other, because there is a single inode and a single copy of the data.

The directory /tmp/x has the entries report.txt and copy.txt, and both hold the inode number 19927765. That inode has a link count of 2 and a size of 6, the owner, the permissions and the times, and the location of the data, which is the text hello followed by a newline.
Figure 2.3 A directory maps names to inode numbers. Everything else about the file is in the inode.

Several everyday facts follow from this split.

  • Renaming a file within a file system is fast at any size. rename changes a directory entry and does not touch the inode or the data.
  • Permissions belong to the inode, so they are the same through every name.
  • Removing a file is called unlink because it removes a link: a directory entry. What happens to the inode is the subject of the next section.

Figure 2.4 puts the structures together.

The kernel structures behind a descriptor, top to bottom, each pointing to the next. struct fdtable has the array fd with one pointer per descriptor and the bitmaps open_fds and close_on_exec. struct file has f_pos, f_flags, f_count, f_inode and f_path. struct dentry has d_name, d_parent and d_inode. struct inode has i_ino, i_mode, i_nlink, i_size, the owner and times, and i_mapping.
Figure 2.4 From a descriptor to an inode: the structures and the pointers between them.

What is shared

We can now answer the question a programmer has to ask: when a process has 2 descriptors, what do they share? It depends on how the second descriptor came to exist.

The second descriptor comes fromNew table entryNew struct fileThe descriptors share
dup or dup2yesnothe offset, the status flags, and the file
fork, in the childyesnothe offset, the status flags, and the file
a second open of the same nameyesyesonly the file
The descriptor table of a process has entries 0, 1 and 2 for the terminal, entry 3 from an open, entry 4 from a dup of 3, and entry 5 from a second open of the same file. Entries 3 and 4 refer to one open file description with offset 5. Entry 5 refers to another description with offset 0. Both descriptions refer to the same inode, a file of 5 bytes.
Figure 2.5 Descriptors 3 and 4 share a struct file, and therefore an offset. Descriptor 5 has its own.

The reason for sharing the offset through dup and fork is sequential output. When a shell runs (date; ls) > file, both commands inherit the same struct file, and ls continues at the offset where date stopped. If each had its own offset, the second command would overwrite the output of the first. We will measure exactly that below.

The close-on-exec flag is deliberately excluded from sharing. It tells the kernel to close the descriptor when the process executes a new program, and it is stored in the close_on_exec bitmap of the table, outside the struct file. A program can therefore keep a descriptor for itself and still pass a duplicate of it to a child.

Lifetimes

An object in the kernel is released when nothing refers to it any more, and each of the objects of this chapter keeps a count for that purpose.

ObjectCountRaised byLowered byAt 0
struct filef_countopen, dup, forkclose, exit of a processthe open file is closed, and its reference to the inode is dropped
inodei_nlinklink, creating the fileunlink, rename over the namethe file has no name
inodeopen counteach struct file that refers to itclosing a struct filethe file is not open

A struct file is closed when the last descriptor for it is closed. close on a descriptor that was duplicated or inherited only lowers f_count. This is the rule that chapter 1 needed: a pipe reports the end of the stream when the last descriptor for its write end is gone, in whichever process that descriptor was.

An inode and its data are freed when it has no name and is not open. Both conditions are required. A file that is unlinked while a process has it open keeps its inode and its data until that process closes it.

The counts of an inode through 4 calls. After create it has 1 name and is open 0 times, and it exists. After open it has 1 name and is open once. After unlink it has 0 names and is still open once, so it exists without a name. After close both counts are 0 and the inode is freed.
Figure 2.6 An inode is freed when its last name and its last open file are both gone.

The kernel shows such a file under /proc. Here a shell opens report.txt on descriptor 7 and then removes both names:

$ exec 7< report.txt
$ rm report.txt copy.txt
$ ls -l /proc/$$/fd/7
7 -> /tmp/x/report.txt (deleted)
$ cat <&7
hello

The consequences are practical. A program can create a temporary file, open it and unlink it at once, and the file disappears when the program ends, however it ends. And a large log file that is deleted while a server still writes to it continues to occupy the disk, which is the usual explanation when df reports a full disk and du cannot find the files.

A file that is deleted while a process has it open keeps its inode, its data and its disk space until that process closes it.

fork and exec

fork gives the child a copy of the parent’s descriptor table. The copy is a new struct fdtable with the same pointers, and f_count of every open file is raised by 1. Parent and child then have independent tables, so either can close or reassign a descriptor without affecting the other, and they share every open file that both still hold.

A parent has descriptor 1 on the terminal and descriptor 3 on a log file. After fork the child has the same entries, and they point to the same open file descriptions: the terminal, with a reference count of 2, and the log file, with offset 120 and a reference count of 2.
Figure 2.7 After fork, the tables are separate and the open files are shared.

exec replaces the program and keeps the table, with 1 exception: descriptors whose close-on-exec bit is set are closed. A program sets the bit with the O_CLOEXEC flag of open, or later with fcntl and F_SETFD. Without it, every descriptor a program has open is passed to every program it starts, including descriptors for files those programs have no business with. Go’s standard library and Rust’s standard library set the flag on every descriptor they create, and pass a descriptor to a child only when the program asks for it.

Chapter 3 covers fork and exec themselves.

Measuring descriptors

fdfacts checks the statements of this chapter on a running kernel. It uses golang.org/x/sys/unix in Go and the nix crate with the fs and resource features in Rust.

$ cargo new fdfacts
$ cargo add -p fdfacts nix --features fs,resource

numbers closes descriptor 0 and opens a file.

Listing 2.1 The imports of fdfacts, in Go. code/go/fdfacts/main.go

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"golang.org/x/sys/unix"
)

Listing 2.2 The imports of fdfacts, in Rust. code/rust/fdfacts/src/main.rs

use std::fs::{self, File, OpenOptions};
use std::io::{self, Read};
use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::process::Command;

use nix::fcntl::{FcntlArg, FdFlag, OFlag, fcntl, open};
use nix::sys::resource::{Resource, getrlimit, setrlimit};
use nix::sys::stat::Mode;
use nix::unistd::{Whence, close, dup, lseek, write};

Listing 2.3 The number of a new descriptor, in Go. code/go/fdfacts/main.go

// numbers closes descriptor 0, the standard input, and opens a file. The
// kernel gives an open the lowest number that is free.
func numbers(path string) error {
	if err := unix.Close(0); err != nil {
		return err
	}

	fd, err := unix.Open(path, unix.O_RDONLY, 0)

	if err != nil {
		return err
	}

	fmt.Printf("descriptor 0 closed: the next open returns %d\n", fd)

	return nil
}

Listing 2.4 The number of a new descriptor, in Rust. code/rust/fdfacts/src/main.rs

/// Closes descriptor 0, the standard input, and opens a file. The kernel
/// gives an open the lowest number that is free.
fn numbers(path: &Path) -> io::Result<()> {
    close(0)?;

    let fd = open(path, OFlag::O_RDONLY, Mode::empty())?;
    println!(
        "descriptor 0 closed: the next open returns {}",
        fd.as_raw_fd()
    );

    Ok(())
}

limit lowers RLIMIT_NOFILE to 32 and opens a file until open fails. It reports the highest number it received and the error.

Listing 2.5 The limit on descriptors, in Go. code/go/fdfacts/main.go

// name returns the symbolic name of a system call error, such as EMFILE.
func name(err error) string {
	var errno unix.Errno

	if errors.As(err, &errno) {
		return unix.ErrnoName(errno)
	}

	return fmt.Sprint(err)
}

// limit lowers the limit on open descriptors to 32 and opens a file until
// the kernel refuses.
func limit(path string) error {
	var saved unix.Rlimit

	if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &saved); err != nil {
		return err
	}

	lowered := saved
	lowered.Cur = 32

	if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &lowered); err != nil {
		return err
	}

	defer unix.Setrlimit(unix.RLIMIT_NOFILE, &saved)

	var opened []int
	highest := -1

	for {
		fd, err := unix.Open(path, unix.O_RDONLY, 0)

		if err != nil {
			fmt.Printf("limit of 32 descriptors: the highest number obtained is %d, the next open fails with %s\n",
				highest, name(err))

			break
		}

		opened, highest = append(opened, fd), max(highest, fd)
	}

	for _, fd := range opened {
		unix.Close(fd)
	}

	return nil
}

Listing 2.6 The limit on descriptors, in Rust. code/rust/fdfacts/src/main.rs

/// Lowers the limit on open descriptors to 32 and opens a file until the
/// kernel refuses.
fn limit(path: &Path) -> io::Result<()> {
    let (soft, hard) = getrlimit(Resource::RLIMIT_NOFILE)?;

    setrlimit(Resource::RLIMIT_NOFILE, 32, hard)?;

    let mut opened = Vec::new();

    let error = loop {
        match open(path, OFlag::O_RDONLY, Mode::empty()) {
            Ok(fd) => opened.push(fd),
            Err(error) => break error,
        }
    };

    let highest = opened.iter().map(AsRawFd::as_raw_fd).fold(-1, i32::max);

    println!(
        "limit of 32 descriptors: the highest number obtained is {highest}, the next open fails with {error:?}"
    );

    drop(opened);
    setrlimit(Resource::RLIMIT_NOFILE, soft, hard)?;

    Ok(())
}

offsets opens a file, duplicates the descriptor, and opens the file again. It writes 5 bytes through the first descriptor and reads the offset through the other 2. lseek with a distance of 0 from the current position returns the offset without changing it.

Listing 2.7 The offset through a duplicate and through a second open, in Go. code/go/fdfacts/main.go

// offset returns the file offset that a descriptor refers to.
func offset(fd int) (int64, error) {
	return unix.Seek(fd, 0, unix.SEEK_CUR)
}

// offsets writes through one descriptor and reads the offset through a
// duplicate of it and through a second open of the same file.
func offsets(path string) error {
	first, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(first)

	duplicate, err := unix.Dup(first)

	if err != nil {
		return err
	}

	defer unix.Close(duplicate)

	second, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(second)

	if _, err := unix.Write(first, []byte("hello")); err != nil {
		return err
	}

	shared, err := offset(duplicate)

	if err != nil {
		return err
	}

	separate, err := offset(second)

	if err != nil {
		return err
	}

	fmt.Printf("after a write of 5 bytes: offset through the duplicate %d, through a second open %d\n",
		shared, separate)

	return nil
}

Listing 2.8 The offset through a duplicate and through a second open, in Rust. code/rust/fdfacts/src/main.rs

/// Returns the file offset that a descriptor refers to.
fn offset(fd: impl AsFd) -> nix::Result<i64> {
    lseek(fd, 0, Whence::SeekCur)
}

/// Writes through one descriptor and reads the offset through a duplicate of
/// it and through a second open of the same file.
fn offsets(path: &Path) -> io::Result<()> {
    let first = open(path, OFlag::O_WRONLY, Mode::empty())?;
    let duplicate = dup(&first)?;
    let second = open(path, OFlag::O_WRONLY, Mode::empty())?;

    write(&first, b"hello")?;

    let shared = offset(&duplicate)?;
    let separate = offset(&second)?;

    println!(
        "after a write of 5 bytes: offset through the duplicate {shared}, through a second open {separate}"
    );

    Ok(())
}

flags sets O_APPEND and close-on-exec on a duplicate and reads both through the original. F_GETFL and F_SETFL address f_flags in the struct file. F_GETFD and F_SETFD address the bitmap in the table.

Listing 2.9 Status flags and descriptor flags, in Go. code/go/fdfacts/main.go

// flags sets O_APPEND and close-on-exec through a duplicate and reads both
// through the original descriptor.
func flags(path string) error {
	first, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(first)

	duplicate, err := unix.Dup(first)

	if err != nil {
		return err
	}

	defer unix.Close(duplicate)

	if _, err := unix.FcntlInt(uintptr(duplicate), unix.F_SETFL, unix.O_APPEND); err != nil {
		return err
	}

	if _, err := unix.FcntlInt(uintptr(duplicate), unix.F_SETFD, unix.FD_CLOEXEC); err != nil {
		return err
	}

	status, err := unix.FcntlInt(uintptr(first), unix.F_GETFL, 0)

	if err != nil {
		return err
	}

	descriptor, err := unix.FcntlInt(uintptr(first), unix.F_GETFD, 0)

	if err != nil {
		return err
	}

	fmt.Printf("set on the duplicate, read on the original: O_APPEND %v, close-on-exec %v\n",
		status&unix.O_APPEND != 0, descriptor&unix.FD_CLOEXEC != 0)

	return nil
}

Listing 2.10 Status flags and descriptor flags, in Rust. code/rust/fdfacts/src/main.rs

/// Sets `O_APPEND` and close-on-exec through a duplicate and reads both
/// through the original descriptor.
fn flags(path: &Path) -> io::Result<()> {
    let first = open(path, OFlag::O_WRONLY, Mode::empty())?;
    let duplicate = dup(&first)?;

    fcntl(&duplicate, FcntlArg::F_SETFL(OFlag::O_APPEND))?;
    fcntl(&duplicate, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?;

    let status = OFlag::from_bits_truncate(fcntl(&first, FcntlArg::F_GETFL)?);
    let descriptor = FdFlag::from_bits_truncate(fcntl(&first, FcntlArg::F_GETFD)?);

    println!(
        "set on the duplicate, read on the original: O_APPEND {}, close-on-exec {}",
        status.contains(OFlag::O_APPEND),
        descriptor.contains(FdFlag::FD_CLOEXEC)
    );

    Ok(())
}

names creates a file, gives it a second name with link, and compares the inode numbers behind the names.

Listing 2.11 A second name for a file, in Go. code/go/fdfacts/main.go

// inode returns the inode number and the link count of a file.
func inode(path string) (number uint64, links uint64, err error) {
	var status unix.Stat_t

	err = unix.Stat(path, &status)

	return status.Ino, uint64(status.Nlink), err
}

// names gives a file a second name and compares the inodes behind the names.
func names(path string) error {
	first, second := path+".first", path+".second"

	if err := os.WriteFile(first, []byte("hello"), 0o644); err != nil {
		return err
	}

	defer os.Remove(first)
	defer os.Remove(second)

	if err := unix.Link(first, second); err != nil {
		return err
	}

	a, _, err := inode(first)

	if err != nil {
		return err
	}

	b, links, err := inode(second)

	if err != nil {
		return err
	}

	fmt.Printf("a second name for the file: same inode %v, link count %d\n", a == b, links)

	return nil
}

Listing 2.12 A second name for a file, in Rust. code/rust/fdfacts/src/main.rs

/// Gives a file a second name and compares the inodes behind the names.
fn names(path: &Path) -> io::Result<()> {
    let (first, second) = (path.with_extension("first"), path.with_extension("second"));

    fs::write(&first, "hello")?;
    fs::hard_link(&first, &second)?;

    let (a, b) = (fs::metadata(&first)?, fs::metadata(&second)?);

    println!(
        "a second name for the file: same inode {}, link count {}",
        a.ino() == b.ino(),
        b.nlink()
    );

    fs::remove_file(&first)?;
    fs::remove_file(&second)
}

unlinked opens a file, removes its name, and then reads the link count and the contents through the descriptor.

Listing 2.13 A file removed while it is open, in Go. code/go/fdfacts/main.go

// unlinked opens a file, removes its only name, and reads through the
// descriptor.
func unlinked(path string) error {
	file := path + ".unlinked"

	if err := os.WriteFile(file, []byte("hello"), 0o644); err != nil {
		return err
	}

	fd, err := unix.Open(file, unix.O_RDONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(fd)

	if err := unix.Unlink(file); err != nil {
		return err
	}

	var status unix.Stat_t

	if err := unix.Fstat(fd, &status); err != nil {
		return err
	}

	buf := make([]byte, 16)
	n, err := unix.Read(fd, buf)

	if err != nil {
		return err
	}

	fmt.Printf("the name removed, the file still open: link count %d, read through the descriptor returns %q\n",
		status.Nlink, buf[:n])

	return nil
}

Listing 2.14 A file removed while it is open, in Rust. code/rust/fdfacts/src/main.rs

/// Opens a file, removes its only name, and reads through the descriptor.
fn unlinked(path: &Path) -> io::Result<()> {
    let name = path.with_extension("unlinked");

    fs::write(&name, "hello")?;

    let mut file = File::open(&name)?;

    fs::remove_file(&name)?;

    let links = file.metadata()?.nlink();
    let mut text = String::new();

    file.read_to_string(&mut text)?;

    println!(
        "the name removed, the file still open: link count {links}, read through the descriptor returns {text:?}"
    );

    Ok(())
}

redirections runs a shell command that writes out to descriptor 1 and then err to descriptor 2, with both descriptors on the same file. In the first run descriptor 2 is a duplicate of descriptor 1. In the other run the file is opened once for each.

Listing 2.15 Descriptors 1 and 2 on the same file, in Go. code/go/fdfacts/main.go

// redirect runs a command that writes "out" to descriptor 1 and then "err" to
// descriptor 2, with both descriptors on the same file, and returns the
// file's contents.
func redirect(path string, stdout, stderr *os.File) (string, error) {
	command := exec.Command("sh", "-c", "echo out; echo err >&2")
	command.Stdout, command.Stderr = stdout, stderr

	if err := command.Run(); err != nil {
		return "", err
	}

	text, err := os.ReadFile(path)

	return strings.Join(strings.Fields(string(text)), " "), err
}

// redirections compares `> file 2>&1`, where descriptor 2 is a duplicate of
// descriptor 1, with `> file 2> file`, where the file is opened once for each.
func redirections(path string) error {
	stdout, err := os.Create(path)

	if err != nil {
		return err
	}

	defer stdout.Close()

	text, err := redirect(path, stdout, stdout)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2>&1: the file holds %q\n", text)

	first, err := os.Create(path)

	if err != nil {
		return err
	}

	defer first.Close()

	second, err := os.OpenFile(path, os.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer second.Close()

	text, err = redirect(path, first, second)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2> file: the file holds %q\n", text)

	return nil
}

Listing 2.16 Descriptors 1 and 2 on the same file, in Rust. code/rust/fdfacts/src/main.rs

/// Runs a command that writes "out" to descriptor 1 and then "err" to
/// descriptor 2, with both descriptors on the same file, and returns the
/// file's contents.
fn redirect(path: &Path, stdout: File, stderr: File) -> io::Result<String> {
    Command::new("sh")
        .args(["-c", "echo out; echo err >&2"])
        .stdout(stdout)
        .stderr(stderr)
        .status()?;

    let text = fs::read_to_string(path)?;
    let words: Vec<&str> = text.split_whitespace().collect();

    Ok(words.join(" "))
}

/// Compares `> file 2>&1`, where descriptor 2 is a duplicate of descriptor 1,
/// with `> file 2> file`, where the file is opened once for each.
fn redirections(path: &Path) -> io::Result<()> {
    let stdout = File::create(path)?;
    let stderr = stdout.try_clone()?;

    let text = redirect(path, stdout, stderr)?;
    println!("> file 2>&1: the file holds {text:?}");

    let first = File::create(path)?;
    let second = OpenOptions::new().write(true).open(path)?;

    let text = redirect(path, first, second)?;
    println!("> file 2> file: the file holds {text:?}");

    Ok(())
}
descriptor 0 closed: the next open returns 0
limit of 32 descriptors: the highest number obtained is 31, the next open fails with EMFILE
after a write of 5 bytes: offset through the duplicate 5, through a second open 0
set on the duplicate, read on the original: O_APPEND true, close-on-exec false
a second name for the file: same inode true, link count 2
the name removed, the file still open: link count 0, read through the descriptor returns "hello"
> file 2>&1: the file holds "out err"
> file 2> file: the file holds "err"
OutputConclusion
with 0 closed, open returns 0A new descriptor gets the lowest free number.
highest number 31, then EMFILEWith a limit of 32, the usable numbers are 0 to 31.
offset 5 through the duplicatedup shares the struct file, and the offset is in it.
offset 0 through a second openEach open creates a struct file with its own offset.
O_APPEND is visible on the originalStatus flags are in the struct file.
close-on-exec is not visible on the originalThe close-on-exec flag is in the table.
same inode, link count 22 names refer to 1 inode.
link count 0, and the read returns helloAn inode without a name exists for as long as it is open.
> file 2>&1 leaves out errWith a shared offset, the second write continues after the first.
> file 2> file leaves errWith separate offsets, both writes start at 0 and the second overwrites the first.

Modelling the tables

fdtable implements the structures of this chapter in user space, with the same reference counts, and runs the experiments of fdfacts on them.

Low-level design: fdtable

TypeKernel structureFieldsInvariant
inodestruct inodenumber, data, links, opensfreed when links and opens are both 0
filesystema directory and the inode tablenames, inodesevery name maps to an inode in inodes
descriptionstruct filefile, offset, appendMode, refsclosed when refs is 0
descriptoran entry of fd[] and its close-on-exec bitopen, cloexecbelongs to 1 table
processstruct files_structtable, limitthe index of an entry is the descriptor number
OperationBehaviourError
create(name), link(old, new), unlink(name)add and remove names; unlink frees the inode when nothing else refers to itENOENT
install(description)places a description at the lowest free indexEMFILE at the limit
open(name)looks the name up, creates a description with offset 0, installs itENOENT, EMFILE
dup(fd), dup2(from, to)install an existing description againEBADF
close(fd)frees the entry; closes the description at the last referenceEBADF
read(fd, n), write(fd, data)use and advance the description’s offsetEBADF
fork()copies the table and counts every copy
exec()closes the entries that have cloexec set

The Go version links the objects with pointers, as the kernel does. The Rust version keeps the inodes and the open file descriptions in tables owned by a Kernel value and refers to them by number, which avoids shared mutable ownership.

An inode has its contents and its 2 counts.

Listing 2.17 The imports of fdtable, in Go. code/go/fdtable/main.go

import (
	"errors"
	"fmt"
	"log"
	"strings"
)

Listing 2.18 The imports of fdtable, in Rust. code/rust/fdtable/src/main.rs

use std::collections::HashMap;
use std::fmt;

Listing 2.19 The inode, and the tables that hold inodes, in Go. code/go/fdtable/main.go

// The errors carry the names of the kernel's error codes for the same cases.
var (
	errBadDescriptor = errors.New("EBADF")
	errTooManyFiles  = errors.New("EMFILE")
	errNoEntry       = errors.New("ENOENT")
)

// An inode is a file: its number, its contents, and 2 reference counts.
// links counts the names that refer to it, and opens counts the open file
// descriptions that refer to it.
type inode struct {
	number int
	data   []byte
	links  int
	opens  int
}

// A filesystem holds the inodes and a single directory, which maps names to
// inodes.
type filesystem struct {
	names  map[string]*inode
	inodes map[int]*inode
	next   int
}

func newFilesystem() *filesystem {
	return &filesystem{names: map[string]*inode{}, inodes: map[int]*inode{}, next: 1}
}

// allocate returns a new inode with no name.
func (fs *filesystem) allocate() *inode {
	file := &inode{number: fs.next}
	fs.inodes[file.number] = file
	fs.next++

	return file
}

Listing 2.20 The inode, and the tables that hold inodes, in Rust. code/rust/fdtable/src/main.rs

/// The errors carry the names of the kernel's error codes for the same cases.
#[derive(Debug)]
enum Error {
    BadDescriptor,
    TooManyFiles,
    NoEntry,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Error::BadDescriptor => "EBADF",
            Error::TooManyFiles => "EMFILE",
            Error::NoEntry => "ENOENT",
        })
    }
}

/// A file: its contents and 2 reference counts. `links` counts the names
/// that refer to it, and `opens` counts the open file descriptions that refer
/// to it. Its number is its key in the kernel's table of inodes.
#[derive(Default)]
struct Inode {
    data: Vec<u8>,
    links: u32,
    opens: u32,
}

/// The kernel's tables: a single directory that maps names to inode numbers,
/// the inodes, and every open file description.
#[derive(Default)]
struct Kernel {
    names: HashMap<&'static str, u64>,
    inodes: HashMap<u64, Inode>,
    open_files: Vec<Description>,
    next: u64,
}

impl Kernel {
    /// Returns the number of a new inode with no name.
    fn allocate(&mut self) -> u64 {
        self.next += 1;
        self.inodes.insert(self.next, Inode::default());

        self.next
    }

    fn inode(&mut self, number: u64) -> Result<&mut Inode, Error> {
        self.inodes.get_mut(&number).ok_or(Error::NoEntry)
    }
}

create, link and unlink change the directory and the link count. release is the single place where an inode is freed, and it requires both counts to be 0.

Listing 2.21 Names: create, link, unlink, and release, in Go. code/go/fdtable/main.go

// create makes an empty file with one name.
func (fs *filesystem) create(name string) *inode {
	file := fs.allocate()
	file.links = 1
	fs.names[name] = file

	return file
}

// link adds a second name for an existing file.
func (fs *filesystem) link(existing, added string) error {
	file, ok := fs.names[existing]

	if !ok {
		return errNoEntry
	}

	file.links++
	fs.names[added] = file

	return nil
}

// unlink removes a name. The file itself is removed only when release finds
// no other reference to it.
func (fs *filesystem) unlink(name string) error {
	file, ok := fs.names[name]

	if !ok {
		return errNoEntry
	}

	delete(fs.names, name)
	file.links--
	fs.release(file)

	return nil
}

// release frees an inode that has no name and is not open.
func (fs *filesystem) release(file *inode) {
	if file.links == 0 && file.opens == 0 {
		delete(fs.inodes, file.number)
	}
}

Listing 2.22 Names: create, link, unlink, and release, in Rust. code/rust/fdtable/src/main.rs

impl Kernel {
    /// Makes an empty file with one name and returns its inode number.
    fn create(&mut self, name: &'static str) -> Result<u64, Error> {
        let number = self.allocate();

        self.inode(number)?.links = 1;
        self.names.insert(name, number);

        Ok(number)
    }

    /// Adds a second name for an existing file.
    fn link(&mut self, existing: &str, added: &'static str) -> Result<(), Error> {
        let number = *self.names.get(existing).ok_or(Error::NoEntry)?;

        self.inode(number)?.links += 1;
        self.names.insert(added, number);

        Ok(())
    }

    /// Removes a name. The file itself is removed only when `release` finds
    /// no other reference to it.
    fn unlink(&mut self, name: &str) -> Result<(), Error> {
        let number = self.names.remove(name).ok_or(Error::NoEntry)?;

        self.inode(number)?.links -= 1;
        self.release(number);

        Ok(())
    }

    /// Frees an inode that has no name and is not open.
    fn release(&mut self, number: u64) {
        if let Some(file) = self.inodes.get(&number)
            && file.links == 0
            && file.opens == 0
        {
            self.inodes.remove(&number);
        }
    }
}

The description corresponds to struct file, and the descriptor to a table entry together with its close-on-exec bit.

Listing 2.23 The description, the descriptor, and the process, in Go. code/go/fdtable/main.go

// A description is an open file description: the state created by one open.
// refs counts the descriptors, in all processes, that refer to it.
type description struct {
	file       *inode
	offset     int
	appendMode bool
	refs       int
}

// A descriptor is one entry of a process's table. The close-on-exec flag
// belongs to the entry.
type descriptor struct {
	open    *description
	cloexec bool
}

// A process is its descriptor table. The index of an entry is the file
// descriptor, a nil entry is a free number, and limit is the number of
// entries the process may use.
type process struct {
	fs    *filesystem
	table []*descriptor
	limit int
}

// newProcess returns a process with descriptors 0, 1 and 2 open on a
// terminal, which is a file without a name.
func newProcess(fs *filesystem) *process {
	p := &process{fs: fs, limit: 1024}
	terminal := &description{file: fs.allocate()}
	terminal.file.opens = 1

	for range 3 {
		p.install(terminal)
	}

	return p
}

Listing 2.24 The description, the descriptor, and the process, in Rust. code/rust/fdtable/src/main.rs

/// An open file description: the state created by one open. `file` is the
/// number of its inode, and `refs` counts the descriptors, in all processes,
/// that refer to it.
struct Description {
    file: u64,
    offset: usize,
    append_mode: bool,
    refs: u32,
}

/// One entry of a process's table. `open` is the index of a description in
/// the kernel's table. The close-on-exec flag belongs to the entry.
#[derive(Clone, Copy)]
struct Descriptor {
    open: usize,
    cloexec: bool,
}

/// A process is its descriptor table. The index of an entry is the file
/// descriptor, `None` is a free number, and `limit` is the number of entries
/// the process may use.
struct Process {
    table: Vec<Option<Descriptor>>,
    limit: usize,
}

impl Kernel {
    /// Adds a description for an inode and returns its index.
    fn describe(&mut self, file: u64) -> usize {
        self.open_files.push(Description {
            file,
            offset: 0,
            append_mode: false,
            refs: 0,
        });

        self.open_files.len() - 1
    }

    /// Returns a process with descriptors 0, 1 and 2 open on a terminal,
    /// which is a file without a name.
    fn process(&mut self) -> Result<Process, Error> {
        let mut process = Process {
            table: Vec::new(),
            limit: 1024,
        };

        let terminal = self.allocate();
        let open = self.describe(terminal);

        self.inode(terminal)?.opens = 1;

        for _ in 0..3 {
            self.install(&mut process, open)?;
        }

        Ok(process)
    }
}

install implements both rules of the descriptor table: the lowest free number and the limit. open creates a description, and dup installs an existing one, which is the whole difference between them.

Listing 2.25 Creating descriptors, in Go. code/go/fdtable/main.go

// install puts a description in the lowest free entry, counts the new
// reference, and returns the number of the entry.
func (p *process) install(open *description) (int, error) {
	fd := 0

	for fd < len(p.table) && p.table[fd] != nil {
		fd++
	}

	if fd >= p.limit {
		return 0, errTooManyFiles
	}

	if fd == len(p.table) {
		p.table = append(p.table, nil)
	}

	open.refs++
	p.table[fd] = &descriptor{open: open}

	return fd, nil
}

// lookup returns the entry of a descriptor.
func (p *process) lookup(fd int) (*descriptor, error) {
	if fd < 0 || fd >= len(p.table) || p.table[fd] == nil {
		return nil, errBadDescriptor
	}

	return p.table[fd], nil
}

// open finds the inode behind a name and creates a description for it, with
// offset 0.
func (p *process) open(name string) (int, error) {
	file, ok := p.fs.names[name]

	if !ok {
		return 0, errNoEntry
	}

	fd, err := p.install(&description{file: file})

	if err == nil {
		file.opens++
	}

	return fd, err
}

// dup makes a second descriptor for an existing description.
func (p *process) dup(fd int) (int, error) {
	entry, err := p.lookup(fd)

	if err != nil {
		return 0, err
	}

	return p.install(entry.open)
}

Listing 2.26 Creating descriptors, in Rust. code/rust/fdtable/src/main.rs

impl Kernel {
    /// Puts a description in the lowest free entry, counts the new reference,
    /// and returns the number of the entry.
    fn install(&mut self, process: &mut Process, open: usize) -> Result<usize, Error> {
        let fd = process
            .table
            .iter()
            .take_while(|entry| entry.is_some())
            .count();

        if fd >= process.limit {
            return Err(Error::TooManyFiles);
        }

        if fd == process.table.len() {
            process.table.push(None);
        }

        self.open_files[open].refs += 1;
        process.table[fd] = Some(Descriptor {
            open,
            cloexec: false,
        });

        Ok(fd)
    }

    /// Finds the inode behind a name and creates a description for it, with
    /// offset 0.
    fn open(&mut self, process: &mut Process, name: &str) -> Result<usize, Error> {
        let file = *self.names.get(name).ok_or(Error::NoEntry)?;
        let open = self.describe(file);
        let fd = self.install(process, open)?;

        self.inode(file)?.opens += 1;

        Ok(fd)
    }

    /// Makes a second descriptor for an existing description.
    fn dup(&mut self, process: &mut Process, fd: usize) -> Result<usize, Error> {
        let entry = process.lookup(fd)?;

        self.install(process, entry.open)
    }
}

impl Process {
    /// Returns the entry of a descriptor.
    fn lookup(&self, fd: usize) -> Result<Descriptor, Error> {
        self.table
            .get(fd)
            .copied()
            .flatten()
            .ok_or(Error::BadDescriptor)
    }
}

close lowers the count of the description and, at 0, the open count of the inode. dup2 closes its target first, which is why redirecting descriptor 1 releases whatever descriptor 1 was before.

Listing 2.27 Closing a descriptor, and dup2, in Go. code/go/fdtable/main.go

// close frees an entry and drops its reference to the description. The last
// reference closes the description, which drops its reference to the inode.
func (p *process) close(fd int) error {
	entry, err := p.lookup(fd)

	if err != nil {
		return err
	}

	p.table[fd] = nil
	entry.open.refs--

	if entry.open.refs == 0 {
		entry.open.file.opens--
		p.fs.release(entry.open.file)
	}

	return nil
}

// dup2 makes descriptor to refer to the description of descriptor from,
// closing whatever to referred to before.
func (p *process) dup2(from, to int) error {
	entry, err := p.lookup(from)

	if err != nil {
		return err
	}

	if from == to {
		return nil
	}

	if _, err := p.lookup(to); err == nil {
		p.close(to)
	}

	for len(p.table) <= to {
		p.table = append(p.table, nil)
	}

	entry.open.refs++
	p.table[to] = &descriptor{open: entry.open}

	return nil
}

Listing 2.28 Closing a descriptor, and dup2, in Rust. code/rust/fdtable/src/main.rs

impl Kernel {
    /// Frees an entry and drops its reference to the description. The last
    /// reference closes the description, which drops its reference to the
    /// inode.
    fn close(&mut self, process: &mut Process, fd: usize) -> Result<(), Error> {
        let entry = process.lookup(fd)?;
        let open = &mut self.open_files[entry.open];

        process.table[fd] = None;
        open.refs -= 1;

        if open.refs == 0 {
            let file = open.file;

            self.inode(file)?.opens -= 1;
            self.release(file);
        }

        Ok(())
    }

    /// Makes descriptor `to` refer to the description of descriptor `from`,
    /// closing whatever `to` referred to before.
    fn dup2(&mut self, process: &mut Process, from: usize, to: usize) -> Result<(), Error> {
        let entry = process.lookup(from)?;

        if from == to {
            return Ok(());
        }

        if process.lookup(to).is_ok() {
            self.close(process, to)?;
        }

        if process.table.len() <= to {
            process.table.resize(to + 1, None);
        }

        self.open_files[entry.open].refs += 1;
        process.table[to] = Some(Descriptor {
            open: entry.open,
            cloexec: false,
        });

        Ok(())
    }
}

read and write take the offset from the description. With appendMode set, write first moves the offset to the end of the file. The kernel performs that move and the write as a single atomic step, so concurrent appends do not overwrite each other.

Listing 2.29 Reading and writing through a descriptor, in Go. code/go/fdtable/main.go

// write copies data into the file at the description's offset and advances
// the offset. In append mode the offset first moves to the end of the file.
func (p *process) write(fd int, data string) error {
	entry, err := p.lookup(fd)

	if err != nil {
		return err
	}

	open, file := entry.open, entry.open.file

	if open.appendMode {
		open.offset = len(file.data)
	}

	end := open.offset + len(data)

	if end > len(file.data) {
		file.data = append(file.data, make([]byte, end-len(file.data))...)
	}

	copy(file.data[open.offset:], data)
	open.offset = end

	return nil
}

// read returns up to n bytes from the description's offset and advances the
// offset.
func (p *process) read(fd, n int) (string, error) {
	entry, err := p.lookup(fd)

	if err != nil {
		return "", err
	}

	open := entry.open
	end := min(open.offset+n, len(open.file.data))
	data := string(open.file.data[open.offset:end])
	open.offset = end

	return data, nil
}

Listing 2.30 Reading and writing through a descriptor, in Rust. code/rust/fdtable/src/main.rs

impl Kernel {
    /// Copies `data` into the file at the description's offset and advances
    /// the offset. In append mode the offset first moves to the end of the
    /// file.
    fn write(&mut self, process: &Process, fd: usize, data: &str) -> Result<(), Error> {
        let open = &mut self.open_files[process.lookup(fd)?.open];
        let file = self.inodes.get_mut(&open.file).ok_or(Error::NoEntry)?;

        if open.append_mode {
            open.offset = file.data.len();
        }

        let end = open.offset + data.len();

        if end > file.data.len() {
            file.data.resize(end, 0);
        }

        file.data[open.offset..end].copy_from_slice(data.as_bytes());
        open.offset = end;

        Ok(())
    }

    /// Returns up to `n` bytes from the description's offset and advances the
    /// offset.
    fn read(&mut self, process: &Process, fd: usize, n: usize) -> Result<String, Error> {
        let open = &mut self.open_files[process.lookup(fd)?.open];
        let file = self.inodes.get(&open.file).ok_or(Error::NoEntry)?;

        let end = (open.offset + n).min(file.data.len());
        let data = String::from_utf8_lossy(&file.data[open.offset..end]).into_owned();

        open.offset = end;

        Ok(data)
    }
}

fork copies the table and raises the count of every description. exec applies the close-on-exec flag.

Listing 2.31 Inheriting descriptors, in Go. code/go/fdtable/main.go

// fork returns a child whose table has a copy of every entry. The copies
// refer to the parent's descriptions, and each copy is counted.
func (p *process) fork() *process {
	child := &process{fs: p.fs, limit: p.limit, table: make([]*descriptor, len(p.table))}

	for fd, entry := range p.table {
		if entry != nil {
			entry.open.refs++
			child.table[fd] = &descriptor{open: entry.open, cloexec: entry.cloexec}
		}
	}

	return child
}

// exec closes every descriptor that has the close-on-exec flag.
func (p *process) exec() {
	for fd, entry := range p.table {
		if entry != nil && entry.cloexec {
			p.close(fd)
		}
	}
}

Listing 2.32 Inheriting descriptors, in Rust. code/rust/fdtable/src/main.rs

impl Kernel {
    /// Returns a child whose table has a copy of every entry. The copies
    /// refer to the parent's descriptions, and each copy is counted.
    fn fork(&mut self, parent: &Process) -> Process {
        for entry in parent.table.iter().flatten() {
            self.open_files[entry.open].refs += 1;
        }

        Process {
            table: parent.table.clone(),
            limit: parent.limit,
        }
    }

    /// Closes every descriptor that has the close-on-exec flag.
    fn exec(&mut self, process: &mut Process) -> Result<(), Error> {
        for fd in 0..process.table.len() {
            if process.table[fd].is_some_and(|entry| entry.cloexec) {
                self.close(process, fd)?;
            }
        }

        Ok(())
    }
}

The experiments repeat fdfacts on the model.

Listing 2.33 Numbers, the limit, offsets and flags on the model, in Go. code/go/fdtable/main.go

func numbers(fs *filesystem) error {
	p := newProcess(fs)

	if err := p.close(0); err != nil {
		return err
	}

	fd, err := p.open("file")
	fmt.Printf("descriptor 0 closed: the next open returns %d\n", fd)

	return err
}

func limit(fs *filesystem) error {
	p := newProcess(fs)
	p.limit = 32
	highest := -1

	for {
		fd, err := p.open("file")

		if err != nil {
			fmt.Printf("limit of 32 descriptors: the highest number obtained is %d, the next open fails with %v\n", highest, err)

			return nil
		}

		highest = max(highest, fd)
	}
}

func offsets(fs *filesystem) error {
	p := newProcess(fs)
	first, err := p.open("file")

	if err != nil {
		return err
	}

	duplicate, err := p.dup(first)

	if err != nil {
		return err
	}

	second, err := p.open("file")

	if err != nil {
		return err
	}

	if err := p.write(first, "hello"); err != nil {
		return err
	}

	fmt.Printf("after a write of 5 bytes: offset through the duplicate %d, through a second open %d\n",
		p.table[duplicate].open.offset, p.table[second].open.offset)

	return nil
}

func flags(fs *filesystem) error {
	p := newProcess(fs)
	first, err := p.open("file")

	if err != nil {
		return err
	}

	duplicate, err := p.dup(first)

	if err != nil {
		return err
	}

	p.table[duplicate].open.appendMode = true
	p.table[duplicate].cloexec = true

	fmt.Printf("set on the duplicate, read on the original: O_APPEND %v, close-on-exec %v\n",
		p.table[first].open.appendMode, p.table[first].cloexec)

	return nil
}

Listing 2.34 Numbers, the limit, offsets and flags on the model, in Rust. code/rust/fdtable/src/main.rs

fn numbers(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    kernel.close(&mut process, 0)?;

    let fd = kernel.open(&mut process, "file")?;
    println!("descriptor 0 closed: the next open returns {fd}");

    Ok(())
}

fn limit(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;
    let mut highest = 0;

    process.limit = 32;

    let error = loop {
        match kernel.open(&mut process, "file") {
            Ok(fd) => highest = highest.max(fd),
            Err(error) => break error,
        }
    };

    println!(
        "limit of 32 descriptors: the highest number obtained is {highest}, the next open fails with {error}"
    );

    Ok(())
}

fn offsets(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    let first = kernel.open(&mut process, "file")?;
    let duplicate = kernel.dup(&mut process, first)?;
    let second = kernel.open(&mut process, "file")?;

    kernel.write(&process, first, "hello")?;

    let shared = kernel.open_files[process.lookup(duplicate)?.open].offset;
    let separate = kernel.open_files[process.lookup(second)?.open].offset;

    println!(
        "after a write of 5 bytes: offset through the duplicate {shared}, through a second open {separate}"
    );

    Ok(())
}

fn flags(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    let first = kernel.open(&mut process, "file")?;
    let duplicate = kernel.dup(&mut process, first)?;

    kernel.open_files[process.lookup(duplicate)?.open].append_mode = true;

    if let Some(entry) = &mut process.table[duplicate] {
        entry.cloexec = true;
    }

    let original = process.lookup(first)?;

    println!(
        "set on the duplicate, read on the original: O_APPEND {}, close-on-exec {}",
        kernel.open_files[original.open].append_mode, original.cloexec
    );

    Ok(())
}

Listing 2.35 Names and the lifetime of an inode on the model, in Go. code/go/fdtable/main.go

func names(fs *filesystem) error {
	fs.create("first")

	if err := fs.link("first", "second"); err != nil {
		return err
	}

	a, b := fs.names["first"], fs.names["second"]
	fmt.Printf("a second name for the file: same inode %v, link count %d\n", a == b, b.links)

	return nil
}

func unlinked(fs *filesystem) error {
	p := newProcess(fs)
	file := fs.create("unlinked")
	file.data = []byte("hello")

	fd, err := p.open("unlinked")

	if err != nil {
		return err
	}

	if err := fs.unlink("unlinked"); err != nil {
		return err
	}

	data, err := p.read(fd, 16)

	if err != nil {
		return err
	}

	fmt.Printf("the name removed, the file still open: link count %d, read through the descriptor returns %q\n",
		file.links, data)

	return p.close(fd)
}

Listing 2.36 Names and the lifetime of an inode on the model, in Rust. code/rust/fdtable/src/main.rs

fn names(kernel: &mut Kernel) -> Result<(), Error> {
    kernel.create("first")?;
    kernel.link("first", "second")?;

    let (a, b) = (kernel.names["first"], kernel.names["second"]);

    println!(
        "a second name for the file: same inode {}, link count {}",
        a == b,
        kernel.inode(b)?.links
    );

    Ok(())
}

fn unlinked(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;
    let file = kernel.create("unlinked")?;

    kernel.inode(file)?.data = b"hello".to_vec();

    let fd = kernel.open(&mut process, "unlinked")?;

    kernel.unlink("unlinked")?;

    let links = kernel.inode(file)?.links;
    let data = kernel.read(&process, fd, 16)?;

    println!(
        "the name removed, the file still open: link count {links}, read through the descriptor returns {data:?}"
    );

    kernel.close(&mut process, fd)
}

redirect does what a shell does between fork and exec: it rearranges the child’s descriptors 1 and 2 with dup2. The parent’s table is unchanged.

Listing 2.37 Redirection on the model, in Go. code/go/fdtable/main.go

// redirect forks a child, points its descriptors 1 and 2 at the given
// descriptors, and has it write "out" and then "err". It returns the file's
// contents.
func redirect(shell *process, file *inode, stdout, stderr int) (string, error) {
	child := shell.fork()

	if err := child.dup2(stdout, 1); err != nil {
		return "", err
	}

	if err := child.dup2(stderr, 2); err != nil {
		return "", err
	}

	child.exec()

	if err := child.write(1, "out\n"); err != nil {
		return "", err
	}

	if err := child.write(2, "err\n"); err != nil {
		return "", err
	}

	return strings.Join(strings.Fields(string(file.data)), " "), nil
}

func redirections(fs *filesystem) error {
	shell := newProcess(fs)
	file := fs.create("output")
	stdout, err := shell.open("output")

	if err != nil {
		return err
	}

	text, err := redirect(shell, file, stdout, stdout)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2>&1: the file holds %q\n", text)

	file.data = nil
	first, err := shell.open("output")

	if err != nil {
		return err
	}

	second, err := shell.open("output")

	if err != nil {
		return err
	}

	text, err = redirect(shell, file, first, second)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2> file: the file holds %q\n", text)

	return nil
}

Listing 2.38 Redirection on the model, in Rust. code/rust/fdtable/src/main.rs

/// Forks a child, points its descriptors 1 and 2 at the given descriptors,
/// and has it write "out" and then "err". Returns the file's contents.
fn redirect(
    kernel: &mut Kernel,
    shell: &Process,
    file: u64,
    stdout: usize,
    stderr: usize,
) -> Result<String, Error> {
    let mut child = kernel.fork(shell);

    kernel.dup2(&mut child, stdout, 1)?;
    kernel.dup2(&mut child, stderr, 2)?;
    kernel.exec(&mut child)?;

    kernel.write(&child, 1, "out\n")?;
    kernel.write(&child, 2, "err\n")?;

    let text = String::from_utf8_lossy(&kernel.inode(file)?.data).into_owned();
    let words: Vec<&str> = text.split_whitespace().collect();

    Ok(words.join(" "))
}

fn redirections(kernel: &mut Kernel) -> Result<(), Error> {
    let mut shell = kernel.process()?;
    let file = kernel.create("output")?;
    let stdout = kernel.open(&mut shell, "output")?;

    let text = redirect(kernel, &shell, file, stdout, stdout)?;
    println!("> file 2>&1: the file holds {text:?}");

    kernel.inode(file)?.data.clear();

    let first = kernel.open(&mut shell, "output")?;
    let second = kernel.open(&mut shell, "output")?;

    let text = redirect(kernel, &shell, file, first, second)?;
    println!("> file 2> file: the file holds {text:?}");

    Ok(())
}
descriptor 0 closed: the next open returns 0
limit of 32 descriptors: the highest number obtained is 31, the next open fails with EMFILE
after a write of 5 bytes: offset through the duplicate 5, through a second open 0
set on the duplicate, read on the original: O_APPEND true, close-on-exec false
a second name for the file: same inode true, link count 2
the name removed, the file still open: link count 0, read through the descriptor returns "hello"
> file 2>&1: the file holds "out err"
> file 2> file: the file holds "err"

The output is identical to the output of fdfacts, line for line.

Redirection

A shell applies redirections from left to right, each as a dup2 in the child before exec.

  • > file opens the file and makes descriptor 1 refer to the new struct file.
  • 2>&1 makes descriptor 2 refer to the struct file that descriptor 1 refers to at that moment.
With > file 2>&1, descriptors 1 and 2 refer to one open file description. The write of out moves its offset from 0 to 4, the write of err continues at 4, and the file holds out and err. With > file 2> file, each descriptor has its own description with offset 0. The write of err starts at offset 0 and overwrites out, and the file holds err.
Figure 2.8 The same file reached through 1 struct file and through 2.

The order of redirections therefore changes the result. In > file 2>&1, descriptor 1 is on the file when descriptor 2 is duplicated from it, and both write to the file. In 2>&1 > file, descriptor 2 is duplicated first, while descriptor 1 is still the terminal. Descriptor 1 then moves to the file, and errors continue to go to the terminal.

>> file 2>> file is safe with separate struct file objects. Both are opened with O_APPEND, so each write goes to the current end of the file regardless of the offset.

Chapter 4 builds a shell that performs these redirections.

Inspecting a running process

The kernel publishes the descriptor table of every process under /proc, which makes the structures of this chapter visible on a live system.

CommandShows
ls -l /proc/<pid>/fdeach descriptor of the process and what it refers to
cat /proc/<pid>/fdinfo/<n>the offset, the flags and the inode number of descriptor n
ulimit -nthe limit on descriptors for processes started from this shell
cat /proc/sys/fs/file-nrthe number of open files in the whole system

For descriptor 7 of the shell above, after the names were removed:

$ cat /proc/$$/fdinfo/7
pos:	0
flags:	0400000
mnt_id:	28
ino:	19927765

pos is f_pos. flags is f_flags in octal: the last digit 0 is O_RDONLY, and 0400000 is O_LARGEFILE, which the kernel sets on every open on a 64-bit system. ino is the inode number, the same 19927765 that ls -li printed while the file had names.

The complete programs

Listing 2.39 The complete fdfacts, in Go. code/go/fdfacts/main.go

// Command fdfacts measures how the kernel hands out file descriptors and what
// a pair of descriptors shares: the number an open returns, the file offset, the
// status flags, and the close-on-exec flag. It ends with the redirection
// `> file 2>&1`, done once with a shared open file description and once with
// separate ones.
//
// Run it from code/go with:
//
//	go run ./fdfacts
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"golang.org/x/sys/unix"
)


// numbers closes descriptor 0, the standard input, and opens a file. The
// kernel gives an open the lowest number that is free.
func numbers(path string) error {
	if err := unix.Close(0); err != nil {
		return err
	}

	fd, err := unix.Open(path, unix.O_RDONLY, 0)

	if err != nil {
		return err
	}

	fmt.Printf("descriptor 0 closed: the next open returns %d\n", fd)

	return nil
}


// name returns the symbolic name of a system call error, such as EMFILE.
func name(err error) string {
	var errno unix.Errno

	if errors.As(err, &errno) {
		return unix.ErrnoName(errno)
	}

	return fmt.Sprint(err)
}

// limit lowers the limit on open descriptors to 32 and opens a file until
// the kernel refuses.
func limit(path string) error {
	var saved unix.Rlimit

	if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &saved); err != nil {
		return err
	}

	lowered := saved
	lowered.Cur = 32

	if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &lowered); err != nil {
		return err
	}

	defer unix.Setrlimit(unix.RLIMIT_NOFILE, &saved)

	var opened []int
	highest := -1

	for {
		fd, err := unix.Open(path, unix.O_RDONLY, 0)

		if err != nil {
			fmt.Printf("limit of 32 descriptors: the highest number obtained is %d, the next open fails with %s\n",
				highest, name(err))

			break
		}

		opened, highest = append(opened, fd), max(highest, fd)
	}

	for _, fd := range opened {
		unix.Close(fd)
	}

	return nil
}


// offset returns the file offset that a descriptor refers to.
func offset(fd int) (int64, error) {
	return unix.Seek(fd, 0, unix.SEEK_CUR)
}

// offsets writes through one descriptor and reads the offset through a
// duplicate of it and through a second open of the same file.
func offsets(path string) error {
	first, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(first)

	duplicate, err := unix.Dup(first)

	if err != nil {
		return err
	}

	defer unix.Close(duplicate)

	second, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(second)

	if _, err := unix.Write(first, []byte("hello")); err != nil {
		return err
	}

	shared, err := offset(duplicate)

	if err != nil {
		return err
	}

	separate, err := offset(second)

	if err != nil {
		return err
	}

	fmt.Printf("after a write of 5 bytes: offset through the duplicate %d, through a second open %d\n",
		shared, separate)

	return nil
}


// flags sets O_APPEND and close-on-exec through a duplicate and reads both
// through the original descriptor.
func flags(path string) error {
	first, err := unix.Open(path, unix.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(first)

	duplicate, err := unix.Dup(first)

	if err != nil {
		return err
	}

	defer unix.Close(duplicate)

	if _, err := unix.FcntlInt(uintptr(duplicate), unix.F_SETFL, unix.O_APPEND); err != nil {
		return err
	}

	if _, err := unix.FcntlInt(uintptr(duplicate), unix.F_SETFD, unix.FD_CLOEXEC); err != nil {
		return err
	}

	status, err := unix.FcntlInt(uintptr(first), unix.F_GETFL, 0)

	if err != nil {
		return err
	}

	descriptor, err := unix.FcntlInt(uintptr(first), unix.F_GETFD, 0)

	if err != nil {
		return err
	}

	fmt.Printf("set on the duplicate, read on the original: O_APPEND %v, close-on-exec %v\n",
		status&unix.O_APPEND != 0, descriptor&unix.FD_CLOEXEC != 0)

	return nil
}


// inode returns the inode number and the link count of a file.
func inode(path string) (number uint64, links uint64, err error) {
	var status unix.Stat_t

	err = unix.Stat(path, &status)

	return status.Ino, uint64(status.Nlink), err
}

// names gives a file a second name and compares the inodes behind the names.
func names(path string) error {
	first, second := path+".first", path+".second"

	if err := os.WriteFile(first, []byte("hello"), 0o644); err != nil {
		return err
	}

	defer os.Remove(first)
	defer os.Remove(second)

	if err := unix.Link(first, second); err != nil {
		return err
	}

	a, _, err := inode(first)

	if err != nil {
		return err
	}

	b, links, err := inode(second)

	if err != nil {
		return err
	}

	fmt.Printf("a second name for the file: same inode %v, link count %d\n", a == b, links)

	return nil
}


// unlinked opens a file, removes its only name, and reads through the
// descriptor.
func unlinked(path string) error {
	file := path + ".unlinked"

	if err := os.WriteFile(file, []byte("hello"), 0o644); err != nil {
		return err
	}

	fd, err := unix.Open(file, unix.O_RDONLY, 0)

	if err != nil {
		return err
	}

	defer unix.Close(fd)

	if err := unix.Unlink(file); err != nil {
		return err
	}

	var status unix.Stat_t

	if err := unix.Fstat(fd, &status); err != nil {
		return err
	}

	buf := make([]byte, 16)
	n, err := unix.Read(fd, buf)

	if err != nil {
		return err
	}

	fmt.Printf("the name removed, the file still open: link count %d, read through the descriptor returns %q\n",
		status.Nlink, buf[:n])

	return nil
}


// redirect runs a command that writes "out" to descriptor 1 and then "err" to
// descriptor 2, with both descriptors on the same file, and returns the
// file's contents.
func redirect(path string, stdout, stderr *os.File) (string, error) {
	command := exec.Command("sh", "-c", "echo out; echo err >&2")
	command.Stdout, command.Stderr = stdout, stderr

	if err := command.Run(); err != nil {
		return "", err
	}

	text, err := os.ReadFile(path)

	return strings.Join(strings.Fields(string(text)), " "), err
}

// redirections compares `> file 2>&1`, where descriptor 2 is a duplicate of
// descriptor 1, with `> file 2> file`, where the file is opened once for each.
func redirections(path string) error {
	stdout, err := os.Create(path)

	if err != nil {
		return err
	}

	defer stdout.Close()

	text, err := redirect(path, stdout, stdout)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2>&1: the file holds %q\n", text)

	first, err := os.Create(path)

	if err != nil {
		return err
	}

	defer first.Close()

	second, err := os.OpenFile(path, os.O_WRONLY, 0)

	if err != nil {
		return err
	}

	defer second.Close()

	text, err = redirect(path, first, second)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2> file: the file holds %q\n", text)

	return nil
}


func run() error {
	dir, err := os.MkdirTemp("", "fdfacts")

	if err != nil {
		return err
	}

	defer os.RemoveAll(dir)

	path := filepath.Join(dir, "file")

	if err := os.WriteFile(path, nil, 0o644); err != nil {
		return err
	}

	for _, experiment := range []func(string) error{numbers, limit, offsets, flags, names, unlinked, redirections} {
		if err := experiment(path); err != nil {
			return err
		}
	}

	return nil
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

Listing 2.40 The complete fdfacts, in Rust. code/rust/fdfacts/src/main.rs

//! Measures how the kernel hands out file descriptors and what a pair of
//! descriptors shares: the number an open returns, the file offset, the
//! status flags, and the close-on-exec flag. It ends with the redirection
//! `> file 2>&1`, done once with a shared open file description and once with
//! separate ones.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p fdfacts
//! ```

use std::fs::{self, File, OpenOptions};
use std::io::{self, Read};
use std::os::fd::{AsFd, AsRawFd};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use std::process::Command;

use nix::fcntl::{FcntlArg, FdFlag, OFlag, fcntl, open};
use nix::sys::resource::{Resource, getrlimit, setrlimit};
use nix::sys::stat::Mode;
use nix::unistd::{Whence, close, dup, lseek, write};

/// Closes descriptor 0, the standard input, and opens a file. The kernel
/// gives an open the lowest number that is free.
fn numbers(path: &Path) -> io::Result<()> {
    close(0)?;

    let fd = open(path, OFlag::O_RDONLY, Mode::empty())?;
    println!(
        "descriptor 0 closed: the next open returns {}",
        fd.as_raw_fd()
    );

    Ok(())
}

/// Lowers the limit on open descriptors to 32 and opens a file until the
/// kernel refuses.
fn limit(path: &Path) -> io::Result<()> {
    let (soft, hard) = getrlimit(Resource::RLIMIT_NOFILE)?;

    setrlimit(Resource::RLIMIT_NOFILE, 32, hard)?;

    let mut opened = Vec::new();

    let error = loop {
        match open(path, OFlag::O_RDONLY, Mode::empty()) {
            Ok(fd) => opened.push(fd),
            Err(error) => break error,
        }
    };

    let highest = opened.iter().map(AsRawFd::as_raw_fd).fold(-1, i32::max);

    println!(
        "limit of 32 descriptors: the highest number obtained is {highest}, the next open fails with {error:?}"
    );

    drop(opened);
    setrlimit(Resource::RLIMIT_NOFILE, soft, hard)?;

    Ok(())
}

/// Returns the file offset that a descriptor refers to.
fn offset(fd: impl AsFd) -> nix::Result<i64> {
    lseek(fd, 0, Whence::SeekCur)
}

/// Writes through one descriptor and reads the offset through a duplicate of
/// it and through a second open of the same file.
fn offsets(path: &Path) -> io::Result<()> {
    let first = open(path, OFlag::O_WRONLY, Mode::empty())?;
    let duplicate = dup(&first)?;
    let second = open(path, OFlag::O_WRONLY, Mode::empty())?;

    write(&first, b"hello")?;

    let shared = offset(&duplicate)?;
    let separate = offset(&second)?;

    println!(
        "after a write of 5 bytes: offset through the duplicate {shared}, through a second open {separate}"
    );

    Ok(())
}

/// Sets `O_APPEND` and close-on-exec through a duplicate and reads both
/// through the original descriptor.
fn flags(path: &Path) -> io::Result<()> {
    let first = open(path, OFlag::O_WRONLY, Mode::empty())?;
    let duplicate = dup(&first)?;

    fcntl(&duplicate, FcntlArg::F_SETFL(OFlag::O_APPEND))?;
    fcntl(&duplicate, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?;

    let status = OFlag::from_bits_truncate(fcntl(&first, FcntlArg::F_GETFL)?);
    let descriptor = FdFlag::from_bits_truncate(fcntl(&first, FcntlArg::F_GETFD)?);

    println!(
        "set on the duplicate, read on the original: O_APPEND {}, close-on-exec {}",
        status.contains(OFlag::O_APPEND),
        descriptor.contains(FdFlag::FD_CLOEXEC)
    );

    Ok(())
}

/// Gives a file a second name and compares the inodes behind the names.
fn names(path: &Path) -> io::Result<()> {
    let (first, second) = (path.with_extension("first"), path.with_extension("second"));

    fs::write(&first, "hello")?;
    fs::hard_link(&first, &second)?;

    let (a, b) = (fs::metadata(&first)?, fs::metadata(&second)?);

    println!(
        "a second name for the file: same inode {}, link count {}",
        a.ino() == b.ino(),
        b.nlink()
    );

    fs::remove_file(&first)?;
    fs::remove_file(&second)
}

/// Opens a file, removes its only name, and reads through the descriptor.
fn unlinked(path: &Path) -> io::Result<()> {
    let name = path.with_extension("unlinked");

    fs::write(&name, "hello")?;

    let mut file = File::open(&name)?;

    fs::remove_file(&name)?;

    let links = file.metadata()?.nlink();
    let mut text = String::new();

    file.read_to_string(&mut text)?;

    println!(
        "the name removed, the file still open: link count {links}, read through the descriptor returns {text:?}"
    );

    Ok(())
}

/// Runs a command that writes "out" to descriptor 1 and then "err" to
/// descriptor 2, with both descriptors on the same file, and returns the
/// file's contents.
fn redirect(path: &Path, stdout: File, stderr: File) -> io::Result<String> {
    Command::new("sh")
        .args(["-c", "echo out; echo err >&2"])
        .stdout(stdout)
        .stderr(stderr)
        .status()?;

    let text = fs::read_to_string(path)?;
    let words: Vec<&str> = text.split_whitespace().collect();

    Ok(words.join(" "))
}

/// Compares `> file 2>&1`, where descriptor 2 is a duplicate of descriptor 1,
/// with `> file 2> file`, where the file is opened once for each.
fn redirections(path: &Path) -> io::Result<()> {
    let stdout = File::create(path)?;
    let stderr = stdout.try_clone()?;

    let text = redirect(path, stdout, stderr)?;
    println!("> file 2>&1: the file holds {text:?}");

    let first = File::create(path)?;
    let second = OpenOptions::new().write(true).open(path)?;

    let text = redirect(path, first, second)?;
    println!("> file 2> file: the file holds {text:?}");

    Ok(())
}

fn main() -> io::Result<()> {
    let dir = std::env::temp_dir().join(format!("fdfacts-{}", std::process::id()));
    let path = dir.join("file");

    fs::create_dir_all(&dir)?;
    fs::write(&path, "")?;

    let result = numbers(&path)
        .and_then(|()| limit(&path))
        .and_then(|()| offsets(&path))
        .and_then(|()| flags(&path))
        .and_then(|()| names(&path))
        .and_then(|()| unlinked(&path))
        .and_then(|()| redirections(&path));

    fs::remove_dir_all(&dir)?;

    result
}

Listing 2.41 The complete fdtable, in Go. code/go/fdtable/main.go

// Command fdtable models the kernel's file tables in user space: names that
// refer to inodes, open file descriptions that refer to an inode and hold an
// offset, and a descriptor table for each process. Every object counts the
// references to it and is released when the count reaches 0. The program
// repeats the experiments of fdfacts on the model and prints the same lines.
//
// Run it from code/go with:
//
//	go run ./fdtable
package main

import (
	"errors"
	"fmt"
	"log"
	"strings"
)


// The errors carry the names of the kernel's error codes for the same cases.
var (
	errBadDescriptor = errors.New("EBADF")
	errTooManyFiles  = errors.New("EMFILE")
	errNoEntry       = errors.New("ENOENT")
)

// An inode is a file: its number, its contents, and 2 reference counts.
// links counts the names that refer to it, and opens counts the open file
// descriptions that refer to it.
type inode struct {
	number int
	data   []byte
	links  int
	opens  int
}

// A filesystem holds the inodes and a single directory, which maps names to
// inodes.
type filesystem struct {
	names  map[string]*inode
	inodes map[int]*inode
	next   int
}

func newFilesystem() *filesystem {
	return &filesystem{names: map[string]*inode{}, inodes: map[int]*inode{}, next: 1}
}

// allocate returns a new inode with no name.
func (fs *filesystem) allocate() *inode {
	file := &inode{number: fs.next}
	fs.inodes[file.number] = file
	fs.next++

	return file
}


// create makes an empty file with one name.
func (fs *filesystem) create(name string) *inode {
	file := fs.allocate()
	file.links = 1
	fs.names[name] = file

	return file
}

// link adds a second name for an existing file.
func (fs *filesystem) link(existing, added string) error {
	file, ok := fs.names[existing]

	if !ok {
		return errNoEntry
	}

	file.links++
	fs.names[added] = file

	return nil
}

// unlink removes a name. The file itself is removed only when release finds
// no other reference to it.
func (fs *filesystem) unlink(name string) error {
	file, ok := fs.names[name]

	if !ok {
		return errNoEntry
	}

	delete(fs.names, name)
	file.links--
	fs.release(file)

	return nil
}

// release frees an inode that has no name and is not open.
func (fs *filesystem) release(file *inode) {
	if file.links == 0 && file.opens == 0 {
		delete(fs.inodes, file.number)
	}
}


// A description is an open file description: the state created by one open.
// refs counts the descriptors, in all processes, that refer to it.
type description struct {
	file       *inode
	offset     int
	appendMode bool
	refs       int
}

// A descriptor is one entry of a process's table. The close-on-exec flag
// belongs to the entry.
type descriptor struct {
	open    *description
	cloexec bool
}

// A process is its descriptor table. The index of an entry is the file
// descriptor, a nil entry is a free number, and limit is the number of
// entries the process may use.
type process struct {
	fs    *filesystem
	table []*descriptor
	limit int
}

// newProcess returns a process with descriptors 0, 1 and 2 open on a
// terminal, which is a file without a name.
func newProcess(fs *filesystem) *process {
	p := &process{fs: fs, limit: 1024}
	terminal := &description{file: fs.allocate()}
	terminal.file.opens = 1

	for range 3 {
		p.install(terminal)
	}

	return p
}


// install puts a description in the lowest free entry, counts the new
// reference, and returns the number of the entry.
func (p *process) install(open *description) (int, error) {
	fd := 0

	for fd < len(p.table) && p.table[fd] != nil {
		fd++
	}

	if fd >= p.limit {
		return 0, errTooManyFiles
	}

	if fd == len(p.table) {
		p.table = append(p.table, nil)
	}

	open.refs++
	p.table[fd] = &descriptor{open: open}

	return fd, nil
}

// lookup returns the entry of a descriptor.
func (p *process) lookup(fd int) (*descriptor, error) {
	if fd < 0 || fd >= len(p.table) || p.table[fd] == nil {
		return nil, errBadDescriptor
	}

	return p.table[fd], nil
}

// open finds the inode behind a name and creates a description for it, with
// offset 0.
func (p *process) open(name string) (int, error) {
	file, ok := p.fs.names[name]

	if !ok {
		return 0, errNoEntry
	}

	fd, err := p.install(&description{file: file})

	if err == nil {
		file.opens++
	}

	return fd, err
}

// dup makes a second descriptor for an existing description.
func (p *process) dup(fd int) (int, error) {
	entry, err := p.lookup(fd)

	if err != nil {
		return 0, err
	}

	return p.install(entry.open)
}


// close frees an entry and drops its reference to the description. The last
// reference closes the description, which drops its reference to the inode.
func (p *process) close(fd int) error {
	entry, err := p.lookup(fd)

	if err != nil {
		return err
	}

	p.table[fd] = nil
	entry.open.refs--

	if entry.open.refs == 0 {
		entry.open.file.opens--
		p.fs.release(entry.open.file)
	}

	return nil
}

// dup2 makes descriptor to refer to the description of descriptor from,
// closing whatever to referred to before.
func (p *process) dup2(from, to int) error {
	entry, err := p.lookup(from)

	if err != nil {
		return err
	}

	if from == to {
		return nil
	}

	if _, err := p.lookup(to); err == nil {
		p.close(to)
	}

	for len(p.table) <= to {
		p.table = append(p.table, nil)
	}

	entry.open.refs++
	p.table[to] = &descriptor{open: entry.open}

	return nil
}


// write copies data into the file at the description's offset and advances
// the offset. In append mode the offset first moves to the end of the file.
func (p *process) write(fd int, data string) error {
	entry, err := p.lookup(fd)

	if err != nil {
		return err
	}

	open, file := entry.open, entry.open.file

	if open.appendMode {
		open.offset = len(file.data)
	}

	end := open.offset + len(data)

	if end > len(file.data) {
		file.data = append(file.data, make([]byte, end-len(file.data))...)
	}

	copy(file.data[open.offset:], data)
	open.offset = end

	return nil
}

// read returns up to n bytes from the description's offset and advances the
// offset.
func (p *process) read(fd, n int) (string, error) {
	entry, err := p.lookup(fd)

	if err != nil {
		return "", err
	}

	open := entry.open
	end := min(open.offset+n, len(open.file.data))
	data := string(open.file.data[open.offset:end])
	open.offset = end

	return data, nil
}


// fork returns a child whose table has a copy of every entry. The copies
// refer to the parent's descriptions, and each copy is counted.
func (p *process) fork() *process {
	child := &process{fs: p.fs, limit: p.limit, table: make([]*descriptor, len(p.table))}

	for fd, entry := range p.table {
		if entry != nil {
			entry.open.refs++
			child.table[fd] = &descriptor{open: entry.open, cloexec: entry.cloexec}
		}
	}

	return child
}

// exec closes every descriptor that has the close-on-exec flag.
func (p *process) exec() {
	for fd, entry := range p.table {
		if entry != nil && entry.cloexec {
			p.close(fd)
		}
	}
}


func numbers(fs *filesystem) error {
	p := newProcess(fs)

	if err := p.close(0); err != nil {
		return err
	}

	fd, err := p.open("file")
	fmt.Printf("descriptor 0 closed: the next open returns %d\n", fd)

	return err
}

func limit(fs *filesystem) error {
	p := newProcess(fs)
	p.limit = 32
	highest := -1

	for {
		fd, err := p.open("file")

		if err != nil {
			fmt.Printf("limit of 32 descriptors: the highest number obtained is %d, the next open fails with %v\n", highest, err)

			return nil
		}

		highest = max(highest, fd)
	}
}

func offsets(fs *filesystem) error {
	p := newProcess(fs)
	first, err := p.open("file")

	if err != nil {
		return err
	}

	duplicate, err := p.dup(first)

	if err != nil {
		return err
	}

	second, err := p.open("file")

	if err != nil {
		return err
	}

	if err := p.write(first, "hello"); err != nil {
		return err
	}

	fmt.Printf("after a write of 5 bytes: offset through the duplicate %d, through a second open %d\n",
		p.table[duplicate].open.offset, p.table[second].open.offset)

	return nil
}

func flags(fs *filesystem) error {
	p := newProcess(fs)
	first, err := p.open("file")

	if err != nil {
		return err
	}

	duplicate, err := p.dup(first)

	if err != nil {
		return err
	}

	p.table[duplicate].open.appendMode = true
	p.table[duplicate].cloexec = true

	fmt.Printf("set on the duplicate, read on the original: O_APPEND %v, close-on-exec %v\n",
		p.table[first].open.appendMode, p.table[first].cloexec)

	return nil
}


func names(fs *filesystem) error {
	fs.create("first")

	if err := fs.link("first", "second"); err != nil {
		return err
	}

	a, b := fs.names["first"], fs.names["second"]
	fmt.Printf("a second name for the file: same inode %v, link count %d\n", a == b, b.links)

	return nil
}

func unlinked(fs *filesystem) error {
	p := newProcess(fs)
	file := fs.create("unlinked")
	file.data = []byte("hello")

	fd, err := p.open("unlinked")

	if err != nil {
		return err
	}

	if err := fs.unlink("unlinked"); err != nil {
		return err
	}

	data, err := p.read(fd, 16)

	if err != nil {
		return err
	}

	fmt.Printf("the name removed, the file still open: link count %d, read through the descriptor returns %q\n",
		file.links, data)

	return p.close(fd)
}


// redirect forks a child, points its descriptors 1 and 2 at the given
// descriptors, and has it write "out" and then "err". It returns the file's
// contents.
func redirect(shell *process, file *inode, stdout, stderr int) (string, error) {
	child := shell.fork()

	if err := child.dup2(stdout, 1); err != nil {
		return "", err
	}

	if err := child.dup2(stderr, 2); err != nil {
		return "", err
	}

	child.exec()

	if err := child.write(1, "out\n"); err != nil {
		return "", err
	}

	if err := child.write(2, "err\n"); err != nil {
		return "", err
	}

	return strings.Join(strings.Fields(string(file.data)), " "), nil
}

func redirections(fs *filesystem) error {
	shell := newProcess(fs)
	file := fs.create("output")
	stdout, err := shell.open("output")

	if err != nil {
		return err
	}

	text, err := redirect(shell, file, stdout, stdout)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2>&1: the file holds %q\n", text)

	file.data = nil
	first, err := shell.open("output")

	if err != nil {
		return err
	}

	second, err := shell.open("output")

	if err != nil {
		return err
	}

	text, err = redirect(shell, file, first, second)

	if err != nil {
		return err
	}

	fmt.Printf("> file 2> file: the file holds %q\n", text)

	return nil
}


func main() {
	experiments := []func(*filesystem) error{numbers, limit, offsets, flags, names, unlinked, redirections}

	for _, experiment := range experiments {
		fs := newFilesystem()
		fs.create("file")

		if err := experiment(fs); err != nil {
			log.Fatal(err)
		}
	}
}

Listing 2.42 The complete fdtable, in Rust. code/rust/fdtable/src/main.rs

//! Models the kernel's file tables in user space: names that refer to inodes,
//! open file descriptions that refer to an inode and hold an offset, and a
//! descriptor table for each process. Every object counts the references to
//! it and is released when the count reaches 0. The program repeats the
//! experiments of fdfacts on the model and prints the same lines.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p fdtable
//! ```

use std::collections::HashMap;
use std::fmt;

/// The errors carry the names of the kernel's error codes for the same cases.
#[derive(Debug)]
enum Error {
    BadDescriptor,
    TooManyFiles,
    NoEntry,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Error::BadDescriptor => "EBADF",
            Error::TooManyFiles => "EMFILE",
            Error::NoEntry => "ENOENT",
        })
    }
}

/// A file: its contents and 2 reference counts. `links` counts the names
/// that refer to it, and `opens` counts the open file descriptions that refer
/// to it. Its number is its key in the kernel's table of inodes.
#[derive(Default)]
struct Inode {
    data: Vec<u8>,
    links: u32,
    opens: u32,
}

/// The kernel's tables: a single directory that maps names to inode numbers,
/// the inodes, and every open file description.
#[derive(Default)]
struct Kernel {
    names: HashMap<&'static str, u64>,
    inodes: HashMap<u64, Inode>,
    open_files: Vec<Description>,
    next: u64,
}

impl Kernel {
    /// Returns the number of a new inode with no name.
    fn allocate(&mut self) -> u64 {
        self.next += 1;
        self.inodes.insert(self.next, Inode::default());

        self.next
    }

    fn inode(&mut self, number: u64) -> Result<&mut Inode, Error> {
        self.inodes.get_mut(&number).ok_or(Error::NoEntry)
    }
}

impl Kernel {
    /// Makes an empty file with one name and returns its inode number.
    fn create(&mut self, name: &'static str) -> Result<u64, Error> {
        let number = self.allocate();

        self.inode(number)?.links = 1;
        self.names.insert(name, number);

        Ok(number)
    }

    /// Adds a second name for an existing file.
    fn link(&mut self, existing: &str, added: &'static str) -> Result<(), Error> {
        let number = *self.names.get(existing).ok_or(Error::NoEntry)?;

        self.inode(number)?.links += 1;
        self.names.insert(added, number);

        Ok(())
    }

    /// Removes a name. The file itself is removed only when `release` finds
    /// no other reference to it.
    fn unlink(&mut self, name: &str) -> Result<(), Error> {
        let number = self.names.remove(name).ok_or(Error::NoEntry)?;

        self.inode(number)?.links -= 1;
        self.release(number);

        Ok(())
    }

    /// Frees an inode that has no name and is not open.
    fn release(&mut self, number: u64) {
        if let Some(file) = self.inodes.get(&number)
            && file.links == 0
            && file.opens == 0
        {
            self.inodes.remove(&number);
        }
    }
}

/// An open file description: the state created by one open. `file` is the
/// number of its inode, and `refs` counts the descriptors, in all processes,
/// that refer to it.
struct Description {
    file: u64,
    offset: usize,
    append_mode: bool,
    refs: u32,
}

/// One entry of a process's table. `open` is the index of a description in
/// the kernel's table. The close-on-exec flag belongs to the entry.
#[derive(Clone, Copy)]
struct Descriptor {
    open: usize,
    cloexec: bool,
}

/// A process is its descriptor table. The index of an entry is the file
/// descriptor, `None` is a free number, and `limit` is the number of entries
/// the process may use.
struct Process {
    table: Vec<Option<Descriptor>>,
    limit: usize,
}

impl Kernel {
    /// Adds a description for an inode and returns its index.
    fn describe(&mut self, file: u64) -> usize {
        self.open_files.push(Description {
            file,
            offset: 0,
            append_mode: false,
            refs: 0,
        });

        self.open_files.len() - 1
    }

    /// Returns a process with descriptors 0, 1 and 2 open on a terminal,
    /// which is a file without a name.
    fn process(&mut self) -> Result<Process, Error> {
        let mut process = Process {
            table: Vec::new(),
            limit: 1024,
        };

        let terminal = self.allocate();
        let open = self.describe(terminal);

        self.inode(terminal)?.opens = 1;

        for _ in 0..3 {
            self.install(&mut process, open)?;
        }

        Ok(process)
    }
}

impl Kernel {
    /// Puts a description in the lowest free entry, counts the new reference,
    /// and returns the number of the entry.
    fn install(&mut self, process: &mut Process, open: usize) -> Result<usize, Error> {
        let fd = process
            .table
            .iter()
            .take_while(|entry| entry.is_some())
            .count();

        if fd >= process.limit {
            return Err(Error::TooManyFiles);
        }

        if fd == process.table.len() {
            process.table.push(None);
        }

        self.open_files[open].refs += 1;
        process.table[fd] = Some(Descriptor {
            open,
            cloexec: false,
        });

        Ok(fd)
    }

    /// Finds the inode behind a name and creates a description for it, with
    /// offset 0.
    fn open(&mut self, process: &mut Process, name: &str) -> Result<usize, Error> {
        let file = *self.names.get(name).ok_or(Error::NoEntry)?;
        let open = self.describe(file);
        let fd = self.install(process, open)?;

        self.inode(file)?.opens += 1;

        Ok(fd)
    }

    /// Makes a second descriptor for an existing description.
    fn dup(&mut self, process: &mut Process, fd: usize) -> Result<usize, Error> {
        let entry = process.lookup(fd)?;

        self.install(process, entry.open)
    }
}

impl Process {
    /// Returns the entry of a descriptor.
    fn lookup(&self, fd: usize) -> Result<Descriptor, Error> {
        self.table
            .get(fd)
            .copied()
            .flatten()
            .ok_or(Error::BadDescriptor)
    }
}

impl Kernel {
    /// Frees an entry and drops its reference to the description. The last
    /// reference closes the description, which drops its reference to the
    /// inode.
    fn close(&mut self, process: &mut Process, fd: usize) -> Result<(), Error> {
        let entry = process.lookup(fd)?;
        let open = &mut self.open_files[entry.open];

        process.table[fd] = None;
        open.refs -= 1;

        if open.refs == 0 {
            let file = open.file;

            self.inode(file)?.opens -= 1;
            self.release(file);
        }

        Ok(())
    }

    /// Makes descriptor `to` refer to the description of descriptor `from`,
    /// closing whatever `to` referred to before.
    fn dup2(&mut self, process: &mut Process, from: usize, to: usize) -> Result<(), Error> {
        let entry = process.lookup(from)?;

        if from == to {
            return Ok(());
        }

        if process.lookup(to).is_ok() {
            self.close(process, to)?;
        }

        if process.table.len() <= to {
            process.table.resize(to + 1, None);
        }

        self.open_files[entry.open].refs += 1;
        process.table[to] = Some(Descriptor {
            open: entry.open,
            cloexec: false,
        });

        Ok(())
    }
}

impl Kernel {
    /// Copies `data` into the file at the description's offset and advances
    /// the offset. In append mode the offset first moves to the end of the
    /// file.
    fn write(&mut self, process: &Process, fd: usize, data: &str) -> Result<(), Error> {
        let open = &mut self.open_files[process.lookup(fd)?.open];
        let file = self.inodes.get_mut(&open.file).ok_or(Error::NoEntry)?;

        if open.append_mode {
            open.offset = file.data.len();
        }

        let end = open.offset + data.len();

        if end > file.data.len() {
            file.data.resize(end, 0);
        }

        file.data[open.offset..end].copy_from_slice(data.as_bytes());
        open.offset = end;

        Ok(())
    }

    /// Returns up to `n` bytes from the description's offset and advances the
    /// offset.
    fn read(&mut self, process: &Process, fd: usize, n: usize) -> Result<String, Error> {
        let open = &mut self.open_files[process.lookup(fd)?.open];
        let file = self.inodes.get(&open.file).ok_or(Error::NoEntry)?;

        let end = (open.offset + n).min(file.data.len());
        let data = String::from_utf8_lossy(&file.data[open.offset..end]).into_owned();

        open.offset = end;

        Ok(data)
    }
}

impl Kernel {
    /// Returns a child whose table has a copy of every entry. The copies
    /// refer to the parent's descriptions, and each copy is counted.
    fn fork(&mut self, parent: &Process) -> Process {
        for entry in parent.table.iter().flatten() {
            self.open_files[entry.open].refs += 1;
        }

        Process {
            table: parent.table.clone(),
            limit: parent.limit,
        }
    }

    /// Closes every descriptor that has the close-on-exec flag.
    fn exec(&mut self, process: &mut Process) -> Result<(), Error> {
        for fd in 0..process.table.len() {
            if process.table[fd].is_some_and(|entry| entry.cloexec) {
                self.close(process, fd)?;
            }
        }

        Ok(())
    }
}

fn numbers(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    kernel.close(&mut process, 0)?;

    let fd = kernel.open(&mut process, "file")?;
    println!("descriptor 0 closed: the next open returns {fd}");

    Ok(())
}

fn limit(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;
    let mut highest = 0;

    process.limit = 32;

    let error = loop {
        match kernel.open(&mut process, "file") {
            Ok(fd) => highest = highest.max(fd),
            Err(error) => break error,
        }
    };

    println!(
        "limit of 32 descriptors: the highest number obtained is {highest}, the next open fails with {error}"
    );

    Ok(())
}

fn offsets(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    let first = kernel.open(&mut process, "file")?;
    let duplicate = kernel.dup(&mut process, first)?;
    let second = kernel.open(&mut process, "file")?;

    kernel.write(&process, first, "hello")?;

    let shared = kernel.open_files[process.lookup(duplicate)?.open].offset;
    let separate = kernel.open_files[process.lookup(second)?.open].offset;

    println!(
        "after a write of 5 bytes: offset through the duplicate {shared}, through a second open {separate}"
    );

    Ok(())
}

fn flags(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;

    let first = kernel.open(&mut process, "file")?;
    let duplicate = kernel.dup(&mut process, first)?;

    kernel.open_files[process.lookup(duplicate)?.open].append_mode = true;

    if let Some(entry) = &mut process.table[duplicate] {
        entry.cloexec = true;
    }

    let original = process.lookup(first)?;

    println!(
        "set on the duplicate, read on the original: O_APPEND {}, close-on-exec {}",
        kernel.open_files[original.open].append_mode, original.cloexec
    );

    Ok(())
}

fn names(kernel: &mut Kernel) -> Result<(), Error> {
    kernel.create("first")?;
    kernel.link("first", "second")?;

    let (a, b) = (kernel.names["first"], kernel.names["second"]);

    println!(
        "a second name for the file: same inode {}, link count {}",
        a == b,
        kernel.inode(b)?.links
    );

    Ok(())
}

fn unlinked(kernel: &mut Kernel) -> Result<(), Error> {
    let mut process = kernel.process()?;
    let file = kernel.create("unlinked")?;

    kernel.inode(file)?.data = b"hello".to_vec();

    let fd = kernel.open(&mut process, "unlinked")?;

    kernel.unlink("unlinked")?;

    let links = kernel.inode(file)?.links;
    let data = kernel.read(&process, fd, 16)?;

    println!(
        "the name removed, the file still open: link count {links}, read through the descriptor returns {data:?}"
    );

    kernel.close(&mut process, fd)
}

/// Forks a child, points its descriptors 1 and 2 at the given descriptors,
/// and has it write "out" and then "err". Returns the file's contents.
fn redirect(
    kernel: &mut Kernel,
    shell: &Process,
    file: u64,
    stdout: usize,
    stderr: usize,
) -> Result<String, Error> {
    let mut child = kernel.fork(shell);

    kernel.dup2(&mut child, stdout, 1)?;
    kernel.dup2(&mut child, stderr, 2)?;
    kernel.exec(&mut child)?;

    kernel.write(&child, 1, "out\n")?;
    kernel.write(&child, 2, "err\n")?;

    let text = String::from_utf8_lossy(&kernel.inode(file)?.data).into_owned();
    let words: Vec<&str> = text.split_whitespace().collect();

    Ok(words.join(" "))
}

fn redirections(kernel: &mut Kernel) -> Result<(), Error> {
    let mut shell = kernel.process()?;
    let file = kernel.create("output")?;
    let stdout = kernel.open(&mut shell, "output")?;

    let text = redirect(kernel, &shell, file, stdout, stdout)?;
    println!("> file 2>&1: the file holds {text:?}");

    kernel.inode(file)?.data.clear();

    let first = kernel.open(&mut shell, "output")?;
    let second = kernel.open(&mut shell, "output")?;

    let text = redirect(kernel, &shell, file, first, second)?;
    println!("> file 2> file: the file holds {text:?}");

    Ok(())
}

fn main() -> Result<(), Error> {
    let experiments = [
        numbers,
        limit,
        offsets,
        flags,
        names,
        unlinked,
        redirections,
    ];

    for experiment in experiments {
        let mut kernel = Kernel::default();

        kernel.create("file")?;
        experiment(&mut kernel)?;
    }

    Ok(())
}

Summary

  • A file descriptor is an index into a per-process table, struct fdtable. A process can name only the objects in its own table.
  • A new descriptor gets the lowest free number. RLIMIT_NOFILE limits the numbers, and open fails with EMFILE at the limit.
  • A table entry points to a struct file, the open file description. It holds the offset f_pos, the status flags f_flags, and the function table f_op through which read and write reach a file, a pipe or a socket.
  • A struct file refers to an inode. The inode holds the type, owner, permissions, size and data location of a file, and no name.
  • Names are directory entries. Several entries may refer to the same inode, and i_nlink counts them.
  • dup and fork create table entries for an existing struct file, so the offset and the status flags are shared. Each open creates a new struct file.
  • A struct file is closed when its last descriptor is closed. An inode is freed when it has no name and is not open.
  • The close-on-exec flag belongs to the table entry. exec closes the descriptors that have it.
  • 2>&1 duplicates descriptor 1 onto descriptor 2. The descriptors then share an offset, and their output appears in order.

References