We have virtualized the CPU (the process) and memory (the address space). Now the third pillar: persistent storage โ where users keep the data they actually care about, on a device that (unlike memory) survives power loss. This interlude covers the interface; later chapters build the implementation.
The Crux: How To Manage A Persistent Device
How should the OS manage a persistent device? What are the APIs it presents, and what are the important aspects of the implementation?39.1 Files And Directories
Two abstractions organize storage. The first is the file file The file-system abstraction for a linear array of bytes you can read and write. The OS usually knows nothing about a file's contents; its job is to store the bytes persistently and return them intact. Each file also has a low-level name (its inode number). defined in ch. 39 โ open in glossary : a linear array of bytes you can read or write. The OS mostly doesnโt care what those bytes mean (C code, a photo, text) โ its job is to store them persistently and hand back exactly what you put in. Every file also has a low-level name, a number called its inode number inode number A file's low-level name: a unique number (also called an i-number) the file system uses internally to identify a file, as opposed to the human-readable name stored in a directory. A directory entry maps a name to an inode number. defined in ch. 39 โ open in glossary (youโll meet inodes properly later).
The second is the directory directory A special file whose contents are a list of (user-readable name, low-level name/inode-number) pairs. Each entry maps a name to either a file or another directory; nesting directories builds the directory tree. A directory has its own inode number and always contains '.' (itself) and '..' (its parent). You never write a directory directly โ the file system owns its format.
defined in ch. 39 โ open in glossary
. A directory also has an
inode number, but its contents are specific: a list of (user-readable name,
inode number) pairs. A directory named foo holding a file with low-level
name 10 and readable name โfooโ contains the entry ("foo", 10). Because each
entry can point to a file or another directory, nesting them builds a
directory tree directory tree The single hierarchy (a.k.a. directory hierarchy) into which all files and directories are organized, starting at the root directory '/'. A name like /foo/bar.txt is an absolute pathname walking the tree from the root.
defined in ch. 39 โ open in glossary
rooted at /. Any file is then
named by an absolute pathname absolute pathname A name that locates a file or directory by walking the directory tree from the root, e.g. /foo/bar.txt. Directories and files can share a name if they sit at different places in the tree.
defined in ch. 39 โ open in glossary
that walks the
tree from the root:
Valid directories above: /, /foo, /bar, /bar/bar, /bar/foo. Valid
files: /foo/bar.txt and /bar/foo/bar.txt. Note the two different files
both named bar.txt โ same name, different places in the tree, no conflict.
(The .txt is just convention; nothing enforces that main.c holds C.)
Tip: Think Carefully About Naming
In UNIX, nearly everything โ files, devices, pipes, even processes โ is named through the file system, under one directory tree. This uniformity of naming makes the whole system simpler and more modular. The first step to accessing any resource is being able to name it.39.2โ39.3 Creating Files
The interface starts with creating, accessing, and deleting files โ including
the mysteriously-named call to remove a file, which weโll demystify at the
end of the chapter. To create a file you call open() with the O_CREAT
flag:
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
Those flags say: create the file if it doesnโt exist (O_CREAT), allow only
writing (O_WRONLY), and if it already exists truncate it to zero bytes
(O_TRUNC). The third argument sets permissions (here, owner read+write).
What open() returns is the key: a file
descriptor file descriptor A small non-negative integer, private per process, returned by open() and used to read/write a file. It indexes a per-process array of pointers into the open file table. Think of it as a capability โ an opaque handle granting access. Descriptors 0, 1, 2 are stdin, stdout, stderr, so the first opened file is usually 3.
defined in ch. 39 โ open in glossary
โ a small integer, private to the process, that you then use
to read or write the file. It is a capability: an opaque handle granting
you the power to act on that file. The OS keeps them per process, in a simple
array in the process structure (from the xv6 kernel):
struct proc {
...
struct file *ofile[NOFILE]; // open files, indexed by descriptor
...
};
A descriptor is an index into the processโs open-file array, which points at a kernel open-file object, which points at the fileโs on-disk inode.
Aside: The creat() System Call
The older way to make a file iscreat() โ essentially
open() with O_CREAT | O_WRONLY | O_TRUNC. Since
open() can create, creat() has fallen out of favor.
Asked what heโd change about UNIX, Ken Thompson reportedly said: โIโd spell
creat with an e.โ39.4 Reading And Writing Files
How does a program actually touch a file? The tool strace traces every
system call a program makes. Run it on cat foo (after echo hello > foo) and
step through what happens:
Ask the OS for access to foo, read-only.
Two things to notice. First, why does the file open as descriptor 3? Every
process starts with three descriptors already open: standard input (0),
standard output (1), and standard error (2). So the next open lands on
3. Second, read() returns the number of bytes read โ 6 for โhello\nโ โ and
a later read() returning 0 signals end-of-file. Writing works the same
way: open() (for writing), one or more write()s, then close().
Aside: The Open File Table
Each processโs descriptor array points into a system-wide open file table open file table A system-wide table of entries, each describing one open file: which inode it refers to, whether it is readable/writable, a reference count, and the current file offset. A process's file descriptors point into this table; usually one-to-one, but entries can be shared (via fork() or dup()). defined in ch. 39 โ open in glossary . Each entry there tracks which underlying file a descriptor refers to, whether itโs readable/writable, and the current offset โ where the next read or write will land. That offset is the star of the next section.Tip: Use strace (And Similar Tools)
strace shows exactly which system calls a program makes, with
arguments and return codes โ a superb way to understand whatโs really going
on. Useful flags: -f follows forked children, -t
timestamps each call, -e trace=open,close,read,write filters to
just those calls. (On a Mac, try dtruss.)Check yourself: files, directories, and descriptors
1.What is the difference between a file and a directory in this model?
2.What is a file's 'low-level name'?
3.In the directory tree, can two different files both be named bar.txt?
4.What does open() return, and how should you think of it?
5.When cat opens foo, the descriptor is 3, not 0. Why?
6.In the strace, read(3, buf, 4096) returns 6, and a later read returns 0. What do those returns mean?