8.13 File descriptor layer
A cool aspect of the Unix interface is that most resources in Unix are represented as files, including devices such as the console, pipes, and of course, real files. The file descriptor layer is the layer that achieves this uniformity.
Xv6 gives each process its own table of open files, or
file descriptors, as we saw in
Chapter 1.
Each open file is represented by a
struct file
(kernel/file.h:1),
which is a wrapper around either an inode or a pipe,
plus an I/O offset.
Each call to
open
creates a new open file (a new
struct
file
):
if multiple processes open the same file independently,
the different instances will have different I/O offsets.
On the other hand, a single open file
(the same
struct
file
)
can appear
multiple times in one process’s file table
and also in the file tables of multiple processes.
This would happen if one process used
open
to open the file and then created aliases using
dup
or shared it with a child using
fork
.
A reference count tracks the number of references to
a particular open file.
A file can be open for reading or writing or both.
The
readable
and
writable
fields track this.
All the open files in the system are kept in a global file table,
the
ftable
.
The file table
has functions to allocate a file
(filealloc
),
create a duplicate reference
(filedup
),
release a reference
(fileclose
),
and read and write data
(fileread
and
filewrite
).
The first three follow the now-familiar form.
filealloc
(kernel/file.c:30)
scans the file table for an unreferenced file
(f->ref
==
0
)
and returns a new reference;
filedup
(kernel/file.c:48)
increments the reference count;
and
fileclose
(kernel/file.c:60)
decrements it.
When a file’s reference count reaches zero,
fileclose
releases the underlying pipe or inode,
according to the type.
The functions
filestat
,
fileread
,
and
filewrite
implement the
stat
,
read
,
and
write
operations on files.
filestat
(kernel/file.c:88)
is only allowed on inodes and calls
stati
.
fileread
and
filewrite
check that the operation is allowed by
the open mode and then
pass the call through to either
the pipe or inode implementation.
If the file represents an inode,
fileread
and
filewrite
use the I/O offset as the offset for the operation
and then advance it
(kernel/file.c:122-123)
(kernel/file.c:153-154).
Pipes have no concept of offset.
Recall that the inode functions require the caller
to handle locking
(kernel/file.c:94-96)
(kernel/file.c:121-124)
(kernel/file.c:163-166).
The inode locking has the convenient side effect that the
read and write offsets are updated atomically, so that
multiple writing to the same file simultaneously
cannot overwrite each other’s data, though their writes may end up interlaced.