Notes 1: File I/O

Corresponds to Chapter 3 in Advanced Programming in the Unix Environment.

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 platform independent.

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 reflect this.

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 sys/stat.h.

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 offset bytes.

The file pointer is set to its current position plus offset bytes.

The file pointer is set to the file length (end of the file) plus offset bytes.

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 current location).

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 shown:

[File Descriptor Image]
Figure 1. File Descriptor, File Table, and v-node table relationship.
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 described later.)

int dup(int oldfd);
int dup2(int oldfd, int newfd);

Sometimes you'll want to duplicate an entry your process' file descriptor table. For this purpose you can use one of the dup() functions.

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 below:

Make arg be a copy of fd. Use dup2() instead.

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.