What are our goals?
I/O devices require a different access strategy than the previously considered components because they are much slower. As many as 20 million instructions can be executed in a processor while awaiting the results of reading a single random sector off a hard drive. Also, since many I/O devices are used for permanent storage, robustness is a major factor. The lack of standardization in device interfaces can also create a problem which we would like to abstract away from the user to allow more portable access to peripherals. Because of these factors, system calls are an appropriate way to implement device access functions because they can be executed almost entirely in the kernel.
Before considering the appropriate interface for devices, it is important to partition them into their two main categories and consider the properties of each.
Disk Family
responds to requests – programs request data from the disk and wait for a response
finite capacity – the disk will eventually fill up
random access – programs may request data from anywhere on the disk
Network devices, keyboard, mouse, other input devices
responds to events – data is generated spontaneously, such as a keystroke or an incoming network packet
no limit to the amount of data supplied
data stream – the data supplied by these devices can only be accessed when they occur, not before or after
There are also other types of devices (such as the display), but we will focus on these two generic types of devices.
File Descriptors
There are many ways to implement device access, but we will focus on the strategy adopted by UNIX. In order to simplify the issues of device access, UNIX has a common interface for all I/O devices. All devices and the data on them is described by the file descriptor type and accessed with the same API.
The file descriptor consists of an integer number as a file handle and an offset, which points to the specific location within the file that the next read or write will come from.
ssize_t read(int fd, void* addr, size_t size);
returns the number of bytes read off the disk, 0 if it reached the end of file (EOF), or -1 for error
fd – file descriptor, represented as an integer
size – number of bytes to read
off_t lseek(int fd, off_t offset, int whence);
returns the new offset
whence indicates what the offset is relative to:
SEEK_SET – relative to the start of the file
SEEK_CUR – relative to the current offset
SEEK_END – relative to the end of the file
The process of making a disk access system call generally proceeds like the following:
user process issues system call
Example: read(5, 0x7fff000, 512);
Note that the memory address specified in the read call is actually a virtual address, which gets translated by the kernel before access
the kernel takes over, examines the calling process's registers and memory, and determines the appropriate type of system call to make
kernel consults File Descriptor Table within the process descriptor
Note that the File Descriptor Table is another field in the process descriptor. This consists of an array of file descriptors indicating which files that process is currently reading from or writing to.
In order to open a file descriptor, a process must make the system call
int open( const char* location, int access_flags, ...);
returns the file descriptor table index of the opened file, or -1 on error. The cause of the error can be determined by checking the global variable errno.
access_flags – integer mask specifying the type of access the process wishes to have to the file:
O_RDONLY- read-only access
O_RDWR- read and write access
O_WRONLY – write-only access
trailing arguments are variable, and depend on the value of the flags
When a process is finished with a file, it must close it in order to prevent a file descriptor leak. The system call to close a file descriptor is as follows:
int close(int fd);
returns 0 for success. Returns -1 on error and setts errno appropriately.
The following code segments will produce errors:
close(-1); - This will fail because -1 is not a valid file descriptor. Calling close with an argument larger than the number of file descriptors or an invalid file descriptor will have the same result.
close(0); close(0); - The second call will fail because the file has already been closed.
If access to the file was buffered through the operating system rather than actually going to disk, errno will have the value EIO
Because of the high frequency of errors that can arise when writing to a disk, it is important to always check for errors after system calls.