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
readthrough 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,forkandexecdo 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.
- 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 errorEBADF, “bad file descriptor”. - 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. - It calls the read function of whatever the file is. The
struct fileholds a table of functions, and a disk file, a pipe and a socket each supply their own. - 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.
- It copies up to 100 bytes from those pages into
buf, in the memory of the process. - It adds the number of bytes copied to the offset and returns that number.
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 */
/* ... */
};
| Field | Meaning |
|---|---|
fd | An array of pointers. fd[3] is the open file behind descriptor 3, or NULL. |
open_fds | 1 bit per descriptor, set while the number is in use. |
close_on_exec | 1 bit per descriptor: the close-on-exec flag, described below. |
max_fds | The current length of fd. The table starts with 64 entries and is replaced by a larger one when a higher number is needed. |
count | How 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 */
/* ... */
};
| Field | Meaning |
|---|---|
f_pos | The offset. read and write start here and advance it. lseek sets it. |
f_flags | The 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_mode | Whether the file was opened for reading, for writing, or both. A write on a file opened read-only fails with EBADF. |
f_count | The number of references to this struct file: table entries in any process, plus system calls in progress. |
f_inode | The inode of the file. |
f_op | A 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)
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.
Several everyday facts follow from this split.
- Renaming a file within a file system is fast at any size.
renamechanges 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
unlinkbecause 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.
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 from | New table entry | New struct file | The descriptors share |
|---|---|---|---|
dup or dup2 | yes | no | the offset, the status flags, and the file |
fork, in the child | yes | no | the offset, the status flags, and the file |
a second open of the same name | yes | yes | only the file |
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.
| Object | Count | Raised by | Lowered by | At 0 |
|---|---|---|---|---|
struct file | f_count | open, dup, fork | close, exit of a process | the open file is closed, and its reference to the inode is dropped |
| inode | i_nlink | link, creating the file | unlink, rename over the name | the file has no name |
| inode | open count | each struct file that refers to it | closing a struct file | the 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 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.
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"
| Output | Conclusion |
|---|---|
with 0 closed, open returns 0 | A new descriptor gets the lowest free number. |
highest number 31, then EMFILE | With a limit of 32, the usable numbers are 0 to 31. |
| offset 5 through the duplicate | dup shares the struct file, and the offset is in it. |
| offset 0 through a second open | Each open creates a struct file with its own offset. |
O_APPEND is visible on the original | Status flags are in the struct file. |
| close-on-exec is not visible on the original | The close-on-exec flag is in the table. |
| same inode, link count 2 | 2 names refer to 1 inode. |
link count 0, and the read returns hello | An inode without a name exists for as long as it is open. |
> file 2>&1 leaves out err | With a shared offset, the second write continues after the first. |
> file 2> file leaves err | With 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
| Type | Kernel structure | Fields | Invariant |
|---|---|---|---|
inode | struct inode | number, data, links, opens | freed when links and opens are both 0 |
filesystem | a directory and the inode table | names, inodes | every name maps to an inode in inodes |
description | struct file | file, offset, appendMode, refs | closed when refs is 0 |
descriptor | an entry of fd[] and its close-on-exec bit | open, cloexec | belongs to 1 table |
process | struct files_struct | table, limit | the index of an entry is the descriptor number |
| Operation | Behaviour | Error |
|---|---|---|
create(name), link(old, new), unlink(name) | add and remove names; unlink frees the inode when nothing else refers to it | ENOENT |
install(description) | places a description at the lowest free index | EMFILE at the limit |
open(name) | looks the name up, creates a description with offset 0, installs it | ENOENT, EMFILE |
dup(fd), dup2(from, to) | install an existing description again | EBADF |
close(fd) | frees the entry; closes the description at the last reference | EBADF |
read(fd, n), write(fd, data) | use and advance the description’s offset | EBADF |
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.
> fileopens the file and makes descriptor 1 refer to the newstruct file.2>&1makes descriptor 2 refer to thestruct filethat descriptor 1 refers to at that moment.
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.
| Command | Shows |
|---|---|
ls -l /proc/<pid>/fd | each 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 -n | the limit on descriptors for processes started from this shell |
cat /proc/sys/fs/file-nr | the 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_NOFILElimits the numbers, andopenfails withEMFILEat the limit. - A table entry points to a
struct file, the open file description. It holds the offsetf_pos, the status flagsf_flags, and the function tablef_opthrough whichreadandwritereach a file, a pipe or a socket. - A
struct filerefers 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_nlinkcounts them. dupandforkcreate table entries for an existingstruct file, so the offset and the status flags are shared. Eachopencreates a newstruct file.- A
struct fileis 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.
execcloses the descriptors that have it. 2>&1duplicates descriptor 1 onto descriptor 2. The descriptors then share an offset, and their output appears in order.
References
open(2),dup(2),fcntl(2),link(2),unlink(2),stat(2)andinode(7), Linux manual pages: https://man7.org/linux/man-pages/man7/inode.7.html- M. Kerrisk, The Linux Programming Interface, No Starch Press, 2010, chapters 4, 5 and 18.
include/linux/fdtable.h,include/linux/fs.h,include/linux/dcache.handfs/file.c, Linux 5.15: https://elixir.bootlin.com/linux/v5.15/source/include/linux/fs.h- Overview of the Linux Virtual File System, Linux kernel documentation: https://www.kernel.org/doc/html/v5.15/filesystems/vfs.html