13.4. Alternatives to read() and write() While the read() and write() system calls are all applications need to retrieve and store data in a file, they are not always the fastest method. They allow a single piece of data to be manipulated; if multiple pieces of data need to be written, multiple system calls are required. Similarly, if the application needs to access data in different locations in the file, it must call lseek() between every read() or write(), doubling the number of system calls needed. To improve efficiency, there are other system calls that improve things. 13.4.1. Scatter/Gather Reads and Writes Applications often want to read and write various types of data to consecutive areas of a file. Although this can be done fairly easily through multiple read() and write() calls, this solution is not particularly efficient. Applications could instead move all the data into a consecutive memory region, allowing a single system call, but doing so results in many unnecessary memory operations. Linux provides readv() and writev(), which implement scatter/gather reads and writes.[27] Instead of being passed a single pointer and buffer size, as their standard siblings are, these system calls are passed an array of records, each record describing a buffer. The buffers get read from or written to in the order they are listed in the array. Each buffer is described by a struct iovec. [27] They are so named because the reads scatter data across memory, and the writes gather data from different memory regions. They are also known as vector reads and writes, which is the origin of the v at the end of readv() and writev(). #include <sys/uio.h> struct iovec { void * iov_base; /* buffer address */ size_t iov_len; /* buffer length */ }; The first element, iov_base, points to the buffer space. The iov_len item is the number of characters in the buffer. These items are the same as the second and third parameters passed to read() and write(). Here are the prototypes for readv() and writev(): #include <sys/uio.h> int readv(int fd, const struct iovec * vector, size_t count); int writev(int fd, const struct iovec * vector, size_t count); The first argument is the file descriptor to be read from or written to. The second, vector, points to an array of count struct iovec items. Both functions return the total number of bytes read or written. Here is a simple example program that uses writev() to display a simple message on standard output: 1: /* gather.c */ 2: 3: #include <sys/uio.h> 4: 5: int main(void) { 6: struct iovec buffers[3]; 7: 8: buffers[0].iov_base = "hello"; 9: buffers[0].iov_len = 5; 10: 11: buffers[1].iov_base = " "; 12: buffers[1].iov_len = 1; 13: 14: buffers[2].iov_base = "world\n"; 15: buffers[2].iov_len = 6; 16: 17: writev(1, buffers, 3); 18: 19: return 0; 20: } 13.4.2. Ignoring the File Pointer Programs that use binary files often look something like this: lseek(fd, SEEK_SET, offset1); read(fd, buffer, bufferSize); offset2 = someOperation(buffer); lseek(fd, SEEK_SET, offset2); read(fd, buffer2, bufferSize2); offset3 = someOperation(buffer2); lseek(fd, SEEK_SET, offset3); read(fd, buffer3, bufferSize3); The need to lseek() to a new location before every read() doubles the number of system calls, as the file pointer is never positioned correctly after a read() because the data is not stored in the file consecutively. There are alternatives to read() and write() that require the file offset as a parameter, and neither alternative uses the file pointer to know what part of the file to access or to update it. Both functions work only on seekable files, as nonseekable files can be read from and written to only at the current location. #define _XOPEN_SOURCE 500 #include <unistd.h> size_t pread(int fd, void *buf, size_t count, off_t offset); size_t pwrite(int fd, void *buf, size_t count, off_t offset); #endif These look just like the prototypes for read() and write() with a fourth parameter, offset. The offset specifies which point in the file the data should be read from or written to. Like their namesakes, these functions return the number of bytes that were transferred. Here is a version of pread() implemented using read() and lseek() that should make its function easy to understand:[28] [28] This emulated version gets most of the behavior right, but it acts differently than the actual system call if signals are received while it is being executed. int pread(int fd, void * data, int size, int offset) { int oldOffset; int rc; int oldErrno; /* move the file pointer to the new location */ oldOffset = lseek(fd, SEEK_SET, offset); if (oldOffset < 0) return -1; rc = read(fd, data, size); /* restore the file pointer, being careful to save errno */ oldErrno = errno; lseek(fd, SEEK_SET, oldOffset); errno = oldErrno; return rc; } |