Basic File I/O
Introduction
In this article (which is adapted from Chapter 4 of Solaris Systems Programming) we’re going to look at some of the basic, low level I/O functions provided by the Solaris operating environment. Low level I/O is sometimes refered to as unbuffered file I/O, because the functions we’re going to talk aboout do not do any buffering, unlike those in the Standard I/O library (Although this is technically correct, it can be a little misleading, as it is possible to use the Standard I/O library, which is normally buffered, in unbuffered mode.)
We first describe file descriptors, and then describe some of the
functions that most UNIX I/O operations rely on, for example, open,
close, read, write, and lseek.
File Descriptors
As far as user mode processes are concerned, file descriptors are the fundamental way of accessing files. A file descriptor, which is a small positive integer, is actually the offest into the process’ process file table. Each process has a process file table associated with it, and it is this table which provides a mapping between the process’ idea of the file, and the kernel’s. Each of the basic file I/O functions takes a file descriptor as an argument, or returns one.
There are three file descriptors that the shell opens for us: standard
input, standard output, and standard error. These are conventionally
assigned to descriptors 0, 1, and 2 respectively. Instead of these magic
numbers, newer programs should use the POSIX.1 constants STDIN_FILENO,
STDOUT_FILENO, and STDERR_FILENO, which are defined in
<unistd.h>.
The open Function
Before we can do any I/O with a file, we must open it for reading or
writing (or both). To open a file, we use the open function.
int open (const char *path, int oflag, /* mode_t mode */...);
The name of the file we want to open is pointed to by path, and the mode we want to
open it for, as well as other flags, is specified in oflag. If the call
to open causes a file to be created, it is created with the permissions
in mode, modified by the process’ umask. If the call to open is
successful, the file’s file descriptor is returned; otherwise,
−1 is returned, and errno is set appropriately. The file descriptor
that is returned is guaranteed to be the lowest unused one available.
We open a file for reading, writing, or both, by setting oflag to one
of the following mutually exclusive constants:
O_RDONLY | Open the file for reading only. |
O_WRONLY | Open the file for writing only. |
O_RDWR | Open the file for reading and writing. The result of applying this flag when opening a FIFO is undefined. |
We may also specify other options by bitwise-ORing one or more of the
following constants with oflag:
O_APPEND | Sets the file offset to the end of the file prior to each write. This is useful, if, for example, more than one process is updating the file, as the writes won’t overwrite each other. |
O_CREAT | If
the file exists, this flag has no effect, unless O_EXCL is
also specified, in which case, see below. Otherwise the file
is created, with the file’s owner set to the effective
user ID of the process. If the directory in which the file is
being created has the S_ISGID bit set, the group ID of the file
is set to the directory’s, otherwise the file’s
group ID is set to the process’ effective GID. The access
permission bits of the file are set by mode, as modified by the
process’ umask. |
O_DSYNC | Write
I/O operations on the file descriptor do not complete until the
data is transferred to the physical storage medium. Normally a
call to write returns once the data has been copied to a buffer
in the kernel. It has no idea whether the data actually got
stored on the physical medium or not. |
O_EXCL | If
this and the O_CREAT flags are set, the call to open will fail
if the file to be opened exists. The check, and the subsequent
creation of the file if it doesn’t exist are atomic
with respect to other processes trying the same operation on
the same file. The effect of setting O_EXCL but not O_CREAT
is undefined. |
O_LARGEFILE | Setting
this option will set the maximum offset for the file to the
largest that can be stored in an off_t64. If this flag is not set,
the maximum allowable offset is restricted to the 32 bit off_t.
This flag was introduced in Solaris 2.6, as part of the large files feature set. In releases of Solaris prior to 2.6, files were limited to 4 GB in size. |
O_NOCTTY | If the file to open is a terminal device, setting this flag will prevent the terminal from becoming the controlling terminal for the process. |
O_NONBLOCK or
O_NDELAY | These flags effect future reads
or writes to the file. If the file to be opened is a FIFO, then
setting either O_NONBLOCK or O_NDELAY will cause a read-only
open to return without delay; a write-only open will fail if
no process currently has the FIFO open for reading. If both
O_NONBLOCK and O_NDELAY are clear, then a read-only open will
block until a process opens the FIFO for writing. Similarly,
a write-only open of a FIFO will block until a processes opens
it for reading.
If the file to be opened is a block or character special file that
supports non-blocking opens, then setting either |
O_RSYNC | If this flag is set, reading the data will block until any pending writes which affect the data are complete. Consider the situation where we want to read a block of data, which another process is updating. If this flag is not set, it is indeterminate whether the data returned will be that which is on the disk, or that which is scheduled to be written. |
O_SYNC |
The result of setting this flag is similar to setting O_DSYNC,
except that the write blocks until the data to be transferred is
written to the physical medium, and the on-disk file attributes
are updated. |
O_TRUNC | If the file exists, and is successfully opened for writing, setting this flag will cause the file’s length to be truncated to 0 bytes. Any data that the file contained are discarded. |
The creat Function
The creat system call is another way of creating a file.
int creat (const char *path, mode_t mode);
The name of the file to create is pointed to by path. If the file is
successfully created, its permission bits are set to mode, as modified
by the process’ umask; if the file can’t be created, creat
returns −1, and errno is set. The creat system call is equivalent to
open (path, O_WRONLY | O_CREAT | O_TRUNC, mode);
In early versions of UNIX, the oflag option to open only allowed
the values 0, 1, and 2 (for read-only, write-only, and read-write
respectively—in Solaris, these are the values for O_RDONLY, O_WRONLY,
and O_RDWR), hence the need for creat. Since support for O_CREAT and
O_TRUNC was added to open, the use of creat has become less necessary.
The decision to call this function creat instead of create
was arbitrary. Ken Thompson, one of the original authors of UNIX, has
been quoted as saying that if he could change anything in UNIX, he would
call this function create rather than creat.
The close Function
The close function closes an open file.
int close (int fd);
fd should be a file descriptor that was previously
returned by open, creat, dup,
(or dup2), or pipe. When a process closes a
file, any locks that it may have on the file are released.
When a process exits, all of its files are automatically closed, a fact taken advantage of by many programmers. However, it is good programming practise to close a file when we are finished with it, as file descriptors are a finite resource.
The lseek and llseek Functions
Every open file has an associated file offset, which determines
where the next read or write operation will start from. The file offset
is set to 0 when a file has been opened, and is automatically increased
after each successful read or write. Reads from a file descriptor start
from the current file offset, as do writes, unless the O_APPEND flag was
set when the file was opened (in which case the file offset gets set to
the end of the file at the start of every write).
We can change the file offset for an open file by using either the
lseek or llseek functions.
off_t lseek (int fd, off_t offset, int whence);
offset_t llseek (int fd, offset_t offset, int whence);
How the value of offset is interpreted depends on the value of the whence argument.
- If
whenceisSEEK_SET, the file ponter is set tooffsetbytes. - If
whenceis set toSEEK_CUR, the file pointer is set to its current location plusoffset. - If
whenceisSEEK_END, the file pointer is set to the end of the file plusoffset.
The constants SEEK_SET, SEEK_CUR, and
SEEK_END are defined in <unistd.h>,
and have the values of 0, 1, and 2 respectively, for compatibility with
older code.
The l in lseek’s name stands for long
integer. The lseek system call was added to UNIX in Version
7, the same time that the long data type was introduced to C. Similarly,
the ll in llseek’s name stands for long
long integer.
The llseek system call is the 64 bit API version of
lseek, which uses the 64 bit offset_t rather
than the 32 bit off_t.
We can use the following code to position the file pointer at the beginning of a file:
lseek (fd, 0, SEEK_SET);
Similarly, we can use the following code to position the file pointer at the end of a file:
lseek (fd, 0, SEEK_END);
Notice that off_t and offset_t are signed
quantities. This means that negative offsets may be specified. Attempts
to seek before the start of a file result in an error.
It is possible to use lseek to seek beyond the end of a
file. When we next write to the file, it gets extended, creating a hole
in the file. These holes are read back as 0. A file with holes in is
also called a sparse file.
The tell Function
The tell function is used to get the current file offset
for a file descriptor.
off_t tell (int fd);
Notice that the return type is an off_t (which is a 32
bit quantity), rather than an offset_t (which is a 64 bit
quantity). This means that we cannot safely use it for files that were
opened with the O_LARGEFILE flag specified, because the
file’s offset may be too large to fit in to an off_t.
The read and pread Functions
We use the read and pread functions to read
data from an open file.
ssize_t read (int fd, void *buf, size_t nbyte);
ssize_t pread (int fd, void *buf, size_t nbyte, off_t offset);
The read function reads up to nbyte bytes from
the open file referred to by fd into the buffer pointed to by
buf. If the read is successful, the number of bytes read is
returned, unless we are at the end of file, in which case 0 is returned.
It’s possible that read will read less than the number
of bytes we requested with nbyte. There are several reasons
why this could happen.
- We are reading from a regular file, and we encounter the end
of file before reading the number of requested bytes. For example,
if there only 64 bytes remaining until the end of file and we
request 128 bytes,
readwill return 64. The next time we try to read from the file, 0 will be returned (assuming that no other process has written to the file in the meantime). - Reading from a terminal usually happens one line at a time.
The read operation starts at the file’s current offset, which gets incremented by the number of bytes read before a successful return.
The pread function is identical to read,
except that pread read operations start at the specified
offset, without changing the file pointer. Attempting to
perform a pread on a file that is incapabale of seeking
results in an error.
The write and pwrite Functions
We use the write and pwrite functions to write
data to an open file.
ssize_t write (int fd, void *buf, size_t nbyte);
ssize_t pwrite (int fd, void *buf, size_t nbyte, off_t offset);
The write function writes up to nbyte bytes
to the open file referred to by fd from the buffer pointed
to by buf. If the write is successful, the number of bytes
written is returned.
Normally a write operation starts at the file’s current offset,
which gets incremented by the number of bytes written before a successful
return. However, if the O_APPEND flag was specified when the
file was opened, the file pointer is set to the end of the file before
the buffer is written. The moving of the file pointer and the writing
of the data are performed atomically.
The pwrite function is identical to write,
except pwrite write operations start at the given
offset, without changing the file pointer. As with
pread, attempting to perform a pwrite on a
file that is incapable of seeking results in an error.
Summary
In this article we’ve had a whirlwind tour of some of the low level I/O functions provided by Solaris, and we also talked about file descriptors.
Author’s Bio
Rich Teer has more than 10 years of industry experience with UNIX systems and C programming. He lives in Kelowna, BC, where he runs his own Solaris consultancy and web hosting company, Rite Online Inc. In what little spare time he has between consulting assignments, running a business, teaching himself Java, and writing, Rich enjoys spending time with his wife, Jenny, and their dog, Judge. He is currently writing a book called Solaris Systems Programming, which will be published by Addison Wesley in 2002.