The five calls the whole file API is built from
◈ 10 cardsWrite the POSIX-correct call for opening, reading, writing, seeking and closing a file, and handle a short read instead of assuming it away.
Five calls, and the shape of all of them
Low-level file I/O on UNIX is five system calls. Everything else — fopen, fgets, a Python file object, a Java stream — is built on top of these, in your own address space.
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbyte);
ssize_t write(int fd, const void *buf, size_t nbyte);
off_t lseek(int fd, off_t offset, int whence);
int close(int fd);
Read those return types carefully, because they are where the textbook is wrong and where the exam is sharp. open() returns a descriptor — a small non-negative int — or −1. read() and write() return a count of bytes transferred, or −1. lseek() returns the new offset, or −1. Only close() returns 0 for success.
open() — the flags, and the third argument
The second argument is a bitwise OR of flags from <fcntl.h>. Exactly one of the three access modes must be present:
O_RDONLY— reading only.O_WRONLY— writing only.O_RDWR— both.
On top of that you OR whatever else you need. O_CREAT creates the file if it does not exist — and it is the one flag that makes the third argument mandatory, because a file that is about to exist needs permission bits. O_TRUNC discards any existing contents. O_APPEND forces every write to the current end of file, no matter where the offset happens to be. O_EXCL, used with O_CREAT, makes the call fail if the file already exists, which is how you claim a name without a race.
The familiar creat(path, mode) is nothing but open(path, O_WRONLY | O_CREAT | O_TRUNC, mode), and there is no reason to write it in new code.
Worked example — a minimal cp
Open the source for reading, open (or create) the destination for writing, shuttle bytes through a buffer, close both. In full, with every return checked:
int src = open(argv[1], O_RDONLY);
if (src == -1) { perror("open source"); return 1; }
int dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (dst == -1) { perror("open destination"); return 1; }
char buf[512];
ssize_t n;
while ((n = read(src, buf, sizeof buf)) > 0)
if (write(dst, buf, (size_t)n) != n) { perror("write"); return 1; }
if (n == -1) { perror("read"); return 1; }
close(src);
close(dst);
Every part of that loop condition is doing work.
read() is asked for sizeof buf bytes and returns how many it actually got. Three outcomes, and the loop distinguishes all three. A positive count means that many bytes are in buf — possibly fewer than 512, and that is not an error. Zero means end of file: there is nothing left, ever. −1 means a genuine failure, which is why n is tested again after the loop exits.
The write passes n, not sizeof buf. Writing the full buffer would append up to 511 bytes of stale garbage from the previous pass to the end of the copy — a classic bug that only shows up on the final, partial block.
And note 0666 on the destination: the actual permissions become 0666 & ~umask, so with the usual umask 022 the file lands as rw-r--r--.
The short read is the point
read() is permitted to return fewer bytes than you asked for. It is guaranteed to fill the buffer only for a regular file with at least that many bytes remaining. For a pipe, a socket, a terminal, or a file near its end, a short count is normal and routine: a pipe hands over whatever the writer has produced so far.
So a single read() is never a way to read n bytes. The loop is not defensive style; it is the interface. This is exactly why A3 Q7.2 asks for a loop rather than a call.
lseek(), random access, and file holes
A descriptor carries a file offset: the byte position the next read or write will start at. Sequential I/O just lets it advance. lseek() moves it explicitly:
off_t size = lseek(fd, 0, SEEK_END); /* the size of the file, in bytes */
off_t here = lseek(fd, 0, SEEK_CUR); /* where we are, without moving */
lseek(fd, 0, SEEK_SET); /* rewind */
SEEK_SET counts from the start, SEEK_CUR from the current position, SEEK_END from end of file. Because the return value is the new offset, those first two lines are the idiomatic ways to ask "how big is this file?" and "where am I?" — a question the book's "Success: 0" would make unanswerable.
Seeking past the end is legal, and it is how you punch a file hole. Write a byte at offset 100000 of an empty file and the region in between was never stored: it reads back as null bytes, but it occupies no disk blocks. ls -l reports the logical size, just over 100 KB; du reports the allocated blocks, which may be one. When those two disagree by a lot, you are looking at a sparse file.
lseek() fails with ESPIPE on anything that is not seekable — a pipe, a FIFO, a socket, a terminal. There is no "position" in a stream of bytes that arrives over time.