4.2.32 · D3Operating Systems

Worked examples — File operations — open, read, write, seek, close

2,610 words12 min readBack to topic

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.


The scenario matrix

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) degenerate/edge position
E read returns n < count (short read) small file / partial data
F read returns 0 EOF — not an error
G read/write returns -1 genuine error, errno set
H write short write → loop must resume from buf + total
I open degenerate: missing file, no O_CREAT fails, returns -1 (ENOENT)
J Limiting value: fd exhaustion (ulimit) leaking fds until open fails
K Real-world word problem concurrent append logging
L Exam twist offset arithmetic under O_APPEND + SEEK_CUR

Building the picture: the offset number line

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.

Figure — File operations — open, read, write, seek, close

Worked examples

Example 1 — absolute forward jump (cell A, D)


Example 2 — negative offset from end (cell B)

Figure — File operations — open, read, write, seek, close
  1. 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."
  2. 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.
  3. The -25 degenerate case: newpos = 20 + (-25) = -5 < 0. A negative result is illegallseek 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. ✔


Example 3 — seeking past EOF creates a hole (cell C)

Figure — File operations — open, read, write, seek, close
  1. 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.
  2. 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.
  3. 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.

Verify: logical size = \max(\text{old size}, \text{write end}) = \max(0, 101) = 101. Byte 100 holds 'X'; bytes 0–99 read as 0. ✔


Example 4 — short read on a tiny file (cell E, F)


Example 5 — the short-write loop (cell H)


Example 6 — open on a missing file (cell I, G)


Example 7 — leaking fds until exhaustion (cell J, limiting value)

Figure — File operations — open, read, write, seek, close
  1. 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.
  2. 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.
  3. 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). ✔


Example 8 — concurrent append logging (real world) (cell K)


Example 9 — exam twist: mixed SEEK_CUR with O_APPEND (cell L)


Case-coverage check

Recall Did we hit every matrix cell?

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