ยง39.1โ€“39.4Files, Directories, and the Basic Interface

Part III OSTEP pp. 467โ€“471 ยท ~7 min read

  • file
  • directory
  • inode number
  • directory tree
  • absolute pathname
  • file descriptor
  • open file table

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 : 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 (youโ€™ll meet inodes properly later).

The second is the directory . 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 rooted at /. Any file is then named by an absolute pathname that walks the tree from the root:

/foobarbar.txtbarfoobar.txtdirectoryfile

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 โ€” 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.

process: ofile[]0 stdin1 stdout2 stderr3 foo โ†’open file (struct file)readable/writable ยท offsetrefcount ยท inode ptrinode(on disk)

Aside: The creat() System Call

The older way to make a file is creat() โ€” 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:

strace of `cat foo`: every file access is a system call. The file descriptor (3) threads through read and close; fd 1 is standard output.
cat(process)
OS / file system
Open the file for reading
Read, then echo to the screen
Hit end-of-file, then close
step 1 / 9 ยท time flows downward

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 . 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?

6 questions