Skip to main content

open, read, write, close, lseek

What This Concept Is

These are the five raw I/O syscalls that every higher-level C I/O facility (FILE *, fopen, fgets, fread) is built on.

  • int open(const char *path, int flags, mode_t mode) -- ask the kernel to open path; return a fd or -1. flags include O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_CLOEXEC, O_NONBLOCK.
  • ssize_t read(int fd, void *buf, size_t n) -- try to read up to n bytes into buf; return the number actually read, 0 for end-of-file, or -1 with errno.
  • ssize_t write(int fd, const void *buf, size_t n) -- try to write up to n bytes from buf; return the number actually written, or -1 with errno.
  • int close(int fd) -- release the fd. After close, the number may be reused by the next open/pipe/dup.
  • off_t lseek(int fd, off_t off, int whence) -- reposition the file offset. whence is SEEK_SET, SEEK_CUR, or SEEK_END.

The model is byte-oriented. There is no notion of "lines" or "records"; that is a convention layered on top in user code.

Why It Matters Here

This is the smallest complete I/O interface in UNIX. Everything else -- stdio, mmap, sockets, pipes -- is either a wrapper over these five (plus a couple of relatives like pread, pwrite, readv, writev) or an alternative to them that still uses the fd abstraction.

Understanding the failure modes is the point. read can return fewer bytes than you asked for. write can too. The loops that handle partial transfers are the distinguishing feature of systems code.

Concrete Example

A minimal cat-like program:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

static int cat_fd(int in) {
char buf[4096];
for (;;) {
ssize_t n = read(in, buf, sizeof buf);
if (n == 0) return 0;
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
char *p = buf;
size_t left = (size_t)n;
while (left > 0) {
ssize_t w = write(1, p, left);
if (w < 0) {
if (errno == EINTR) continue;
return -1;
}
p += w; left -= (size_t)w;
}
}
}

int main(int argc, char **argv) {
if (argc < 2) return cat_fd(0) < 0;
for (int i = 1; i < argc; i++) {
int fd = open(argv[i], O_RDONLY);
if (fd < 0) { perror(argv[i]); return 1; }
if (cat_fd(fd) < 0) { perror("cat"); return 1; }
close(fd);
}
return 0;
}

Two things worth memorizing. First, both loops (read and write) handle EINTR by retrying. Second, write is inside a nested loop -- it can return fewer bytes than requested, so you advance the pointer and try again.

Common Confusion / Misconception

"read(fd, buf, 100) reads 100 bytes." Only if 100 bytes happen to be immediately available. Pipes, sockets, and slow disks routinely return short counts. Code that assumes "full" reads works in testing (small files come back in one chunk) and fails in production.

"Short write is impossible because I am writing to a local file." For regular files this is usually true on Linux, but write may be interrupted by a signal or run out of disk space mid-write. Production code loops.

"lseek(fd, 0, SEEK_SET) rewinds the file." Yes -- but note that lseek is a no-op on non-seekable fds (pipes, sockets, terminals) and returns -1 with errno == ESPIPE. You cannot rewind a pipe.

Another trap: forgetting the third argument to open when O_CREAT is set. Without a mode_t, permissions are undefined; the kernel may refuse the call. Use open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644).

How To Use It

For every raw I/O call:

  1. Check the return value against -1 for error, and branch on errno (EINTR -> retry; EAGAIN -> maybe retry later; ENOENT -> user-visible message).
  2. Loop short transfers. Never treat read or write as "all or nothing."
  3. close every fd you open, on every exit path. (goto cleanup is idiomatic here, even though it is otherwise frowned on.)

Check Yourself

  1. Why must you loop read?
  2. What happens if you call lseek on a pipe?
  3. What is the minimum mode-safe form of open when creating a file?

Mini Drill or Application

Do all four:

  1. Implement the cat_fd loop above from memory. Test it on /etc/hostname and on standard input.
  2. Extend it: write wc -l (count newlines) using only read, write, and lseek -- no stdio. Compare its output with system wc -l on at least three files.
  3. Add O_APPEND to an open and write from two processes to the same file. Verify output is not interleaved inside a single write.
  4. Explain, in one sentence, why O_APPEND gives a different guarantee than lseek(fd, 0, SEEK_END); write(fd, ...);.

Read This Only If Stuck