Memra

The five calls the whole file API is built from

◈ 10 cards

Write 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.

FlagEffectNeeds the third argument?O_RDONLYopen for reading onlynoO_WRONLYopen for writing onlynoO_RDWRopen for reading andwritingnoO_CREATcreate the file if it doesnot existYES - mode_t modeO_APPENDevery write goes to thecurrent end of filenoO_TRUNCdiscard the existingcontents on opennoO_EXCLwith O_CREAT, fail if thefile already existsonly via O_CREATcreat(path, mode) is exactly open(path, O_WRONLY | O_CREAT | O_TRUNC, mode). The mode is filtered by theumask.
Exactly one access mode, then whatever else you OR on. Only `O_CREAT` demands the third argument.
NORMAL ~/memra/learn/comp-325/open-read-write-close-and-lseek utf-8 LF