Notes 1: File I/O
Corresponds to Chapter 3 in Advanced Programming in the Unix
There are a slew of functions in the C library that handle I/O pretty
nicely, such as fopen(), fprintf(), etc. But these
are library calls, not system calls, and are pretty much
So, forget about them for now and concentrate on the system calls, which
will be presented momentarily. First this: all the C library file
functions dealt with a type FILE*. This type, however, is
unused with the system calls. They all deal with a beast called a "file
descriptor", which is really just a non-negative integer. When you
open() a file, you get a file descriptor returned to you which
is used in subsequent read() and write() calls.
Generally speaking, all of the following calls will return -1
when an error occurs, and will set the global variable errno to
int creat(const char *pathname, mode_t mode);
This creates a file with the given path and mode (permissions), and
returns a file descriptor to that file, which is open for writing. You
probably won't use this system call. Use open() instead.
int open(const char *pathname, int flags, ... /*mode_t mode*/);
A file can be opened for reading and/or writing (or appending), or can
be created with this system call. Flags should be set to one of
O_RDONLY, O_WRONLY, or O_RDWR, which can then
be bitwise-OR'd with several flags which offer more control over how the
file is to be opened (this list is incomplete):
- Tells open to create the file if it doesn't exist. This is the only
flag which requires that the mode be added as an argument to
open(). The mode can be in normal octal format (e.g.
0644) or can be created using macros defined in
- If you specify O_CREAT and OR it with this flag, an error
will be returned if the file already exists. This can be useful for
creating lock files, since the test-n-set is atomic.
- Truncate the file
- Move the file pointer to the the end of the file immediately.
int close(int fd);
Close the given file descriptor.
int read(int fd, void *buf, size_t count);
For a given file descriptor, fd, read count bytes into
the buffer buf. If the read is successful, the number of bytes
actually read is returned, otherwise -1 is returned. (A return
value of 0 indicates EOF.
int write(int fd, void *buf, size_t count);
Writes up to count bytes from buf to file descriptor
fd. The number of bytes actually written is returned, or
-1 on error.
off_t lseek(int fd, off_t offset, int whence);
Moves the file pointer for a given fd to a position relative to
whence. The value of whence can be one of:
- The file pointer is set to the beginning of the file plus
- The file pointer is set to its current position plus offset
- The file pointer is set to the file length (end of the file) plus
Of course, offset can be positive or negative.
The return value (on success) is the new location of the file pointer.
This enables you to determine the current position of the file pointer
by the value of lseek(fd, 0, SEEK_CUR); (seek to
How it all works
Each process has its own file descriptor table. Each entry in the file
descriptor table points to an entry in the system file table. Each
entry in the file table points to an entry in the v-node table, as
Figure 1. File Descriptor, File Table, and v-node table
Adapted from Stevens APITUE.
By using this system, different processes can have the same file
open to different offsets, since both processes can have an independent
entry in the file table. The file remains consistent since both file
table entries point to the same v-node entry. Convince the college to
offer CSCI 257 (Design of the UNIX Operating System) and you'll learn
all about it.
The fun really begins when multiple file descriptors point to the same
file table entry. Even more fun is when those two file descriptors are
in different processes. (This can be accomplished with dup(),
below, or fork() or through file descriptor passing, all
int dup(int oldfd);
Sometimes you'll want to duplicate an entry your process' file
descriptor table. For this purpose you can use one of the
int dup2(int oldfd, int newfd);
The first of these, dup() simply goes through the file
descriptor table, finds the first unused descriptor, and points it to
the same file table entry as the file descriptor specified in the call.
Remember: the first available file descriptor is used (starting from 0).
If you want to specify a file descriptor to "dup() into", then
dup2() is for you. The second argument, newfd, is
closed if necessary then made the same as the oldfd.
Example: You want to use the well-known perror() function to
print error messages from your CGI script. The problem is that CGI
scripts need to put all their output on stdout (file descriptor
1) if it is to be seen on the web, and perror() dumps to
stderr (file descriptor 2)! What to do!
Fear not: simply do the following:
close(2); /* close stderr */
dup(1); /* dup stdout into ex-stderr */
What just happened? Well we close()'d file descriptor 2, so it
was is the first available (assuming stdin, file descriptor 0
is still open). Then we dup()'d fd1 into fd2. Now, for all
practical purposes, these file descriptors are identical
(duplicated) since they both point to the same file table entry
(the one for stdout). perror() will attempt to write
to fd2, but it will go to stdout instead of stderr!
Even more simply:
dup2(1, 2); /* dup file descriptor 1 into 2 */
which will give the same effect, and is probably a bit more robust.
int fcntl(int fd, int cmd, ... /* int arg */);
This function allows you to modify the attributes of a file that has
already been opened. Some of the possible cmds are described
- Make arg be a copy of fd. Use dup2()
- Get the file descriptor flags. There is only one currently: the
close-on-exec flag. If this returns 0, the flag is clear; if
it returns 1, it is set. If the flag is set, this file
descriptor will be closed when exec() is called.
- Set the close-on-exec flag to arg (1 or 0).
- Get the file status flags. These are the same flags that were
passed to the open() call (like O_RDONLY,
O_APPEND, etc.). Note, however, that unlike the other flags,
O_RDONLY, O_WRONLY, and O_RDWR are not bits
that are available for testing. The entire return value must be AND'ed
with O_ACCMODE before comparisons to the above three values can
be made. The other flags, like O_APPEND and
O_NONBLOCK can simply be tested by AND'ing them directly with
the return value.
- Set the file status flags. Only some of the flags can be set (O_RDONLY,
O_RDWR, O_WRONLY certainly cannot).
More on fcntl() will be revealed as it becomes important. Or
read the man page. You know you want to.