This page is the exhaustive worked-examples companion to 4.2.32 File operations — open, read, write, seek, close. We stop saying how open, read, write, lseek, close behave and instead drive them through every case — every sign of an offset, every degenerate input, every limiting value — until no scenario can surprise you.
Every situation these five calls can throw is one of the cells below. The examples that follow are each labelled with the cell they cover, and together they hit all of them.
Cell
Case class
What makes it tricky
A
lseek positive offset, SEEK_SET
absolute jump forward
B
lseek negative offset, SEEK_END
"N bytes before end" — sign matters
C
lseek offset past EOF + write
creates a sparse file (a hole)
D
lseek at exact boundary (offset = 0, offset = size)
Before the examples, one image. The offset is a single non-negative integer — a bookmark position measured in bytes from the start of the file. Everything lseek does is arithmetic on this one number.
Apply the SEEK_END formula.newpos = size + offset = 20 + (-4) = 16.
Why this step?SEEK_END starts at position = size (20). A negative offset walks backwards from there — this is the idiom for "the last N bytes."
Read 4 bytes at offsets 16, 17, 18, 19 → the final four bytes. Offset ends at 16 + 4 = 20 = size (bookmark now at EOF).
Why this step?size - 4 is the first of the last four indices; reading to size exactly consumes the tail.
The -25 degenerate case:newpos = 20 + (-25) = -5 < 0. A negative result is illegal — lseek returns -1 and sets errno = EINVAL. Nothing moves.
Why this step? Cell B must also cover the invalid sub-case: you cannot seek before the start of the file.
Verify: last valid index is size-1 = 19; our four indices end at 19. ✔ For -25: 20-25 = -5, and a bookmark can't be negative, so failure is correct. ✔
Seek to 100 on a size-0 file.newpos = 100. This is allowed — seeking past EOF is legal; it does not yet grow the file.
Why this step?lseek only touches the offset integer (mistake-killer: it moves no data), so it happily lands past the current end.
Write 1 byte at offset 100. Now the file must be at least 100 + 1 = 101 bytes. Bytes 0–99 were never written.
Why this step? The gap between old size (0) and the write position (100) becomes a hole.
The hole reads back as zeros. Any read over bytes 0–99 returns \texttt{0x00}. On disk the hole may occupy no real blocks — that's a sparse file.
Why this step? The filesystem records "these blocks are implicitly zero" rather than storing 100 literal zero bytes.
Count the used slots. fds 0,1,2 are stdin/stdout/stderr → 3 used, 1024 - 3 = 1021 free.
Why this step? The limit counts all open descriptors, including the three inherited ones.
Each open consumes one slot (returns 3, then 4, 5, … 1023). After 1021 successful opens the table is full.
Why this step? Without close, the reference count never drops, so slots are never returned.
The next (1022nd) open fails with -1, errno = EMFILE ("Too many open files").
Why this step? This is why leaking descriptors eventually breaks a long-running server — see ulimit and resource limits.
Verify: successful opens = 1024 - 3 = 1021; the 1022nd fails. ✔ Highest fd handed out = 1023 (the last free index). ✔
A ::: Example 1 (SEEK_SET forward)
B ::: Example 2 (SEEK_END negative, incl. invalid -25)
C ::: Example 3 (seek past EOF → sparse hole)
D ::: Examples 1 & 2 (offset at boundaries 0 / size)
E ::: Example 4 (short read, n < count)
F ::: Example 4 (read returns 0 = EOF)
G ::: Examples 6 & 7 (return -1, errno set)
H ::: Example 5 (short-write loop)
I ::: Example 6 (missing file, no O_CREAT → ENOENT)
J ::: Example 7 (fd exhaustion → EMFILE)
K ::: Example 8 (concurrent O_APPEND logging)
L ::: Example 9 (mixed SEEK_CUR arithmetic)
Prerequisites you may want open alongside this page: System calls and the kernel boundary, File descriptor table, File system & inodes, and for the buffered contrast Buffered I/O (fopen/fread). The concurrency example connects to Pipes and sockets (where short reads are the norm, not the exception).