Advanced File I/O
Beyond read/write — duplicating descriptors, seeking with lseek, sparse files and holes, and controlling files with fcntl.
Once you can open/read/write/close, a handful of extra system calls unlock the rest of
Unix file I/O: duplicating descriptors (the basis of redirection), moving the file offset,
sparse files, and per-descriptor control with fcntl.
Duplicating descriptors
dup() and dup2() make a second descriptor that refers to the same open file
description — so both share one offset and set of flags. This is exactly how shell redirection
works: to send a program's output to a file, you open the file and then dup2 it onto
descriptor 1 (stdout):
int fd = open("out.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // stdout now writes to out.log
// everything printf/write to fd 1 now lands in out.logThat is the mechanism behind ./program > out.log in the shell.
Moving the offset: lseek
Every open file has a current offset. lseek(fd, offset, whence) moves it, where whence
is one of:
SEEK_SET— from the beginning of the fileSEEK_CUR— relative to the current positionSEEK_END— from the end (e.g.lseek(fd, 0, SEEK_END)gives the file size)
This enables random access — jump anywhere, then read or write.
Sparse files and holes
Seeking past the end of a file and writing creates a hole: a region that reads back as
zeros but occupies no disk space. Such a file is called sparse, and its logical size
(what ls reports) can be far larger than its physical size (what du reports):
logical: [ data ][ hole (zeros) ][ data ]
physical: ▓▓▓▓▓▓ ▓▓▓▓▓▓ ← only data costs diskModern kernels let you walk the structure efficiently with lseek's SEEK_DATA and
SEEK_HOLE (which report ENXIO past the end).
Truncating and inspecting
truncate(path, len)/ftruncate(fd, len)— grow or shrink a file to an exact size.stat(path, &st)/fstat(fd, &st)— retrieve metadata: size, permissions, owner, timestamps, and the block count (useful for spotting sparse files).
Controlling descriptors: fcntl
fcntl(fd, cmd, …) is the swiss-army knife for descriptors. Common uses:
F_DUPFD— duplicate a descriptor (likedup)FD_CLOEXEC— close this descriptor automatically onexecO_APPEND— always append on writeO_NONBLOCK— make I/O non-blocking (important later, for pipes and sockets)
See it in code
The Redirect the output,
Creating holes, and
Detect holes samples put dup2, lseek, and
SEEK_DATA/SEEK_HOLE to work.
Practice / lab
- Redirect a program's stdout and stderr to a file using
dup2, then observe withstrace. - Create a sparse file by
lseek-ing past the end and writing; comparels -lwithdu. - Use
fstatto print a file's logical size and allocated blocks.
Homework
References & resources
Required
- The Linux Programming Interface (Kerrisk) — Ch. 5–6: File I/O, deeper concepts
- Understanding sparse files (Red Hat)
Optional / enrichment
- Beej's Guide — file descriptors
dup(2)·lseek(2)·fcntl(2)lseek()to read/write alternate bytes (GeeksforGeeks)
Self-check
- What do two descriptors share after
dup2, and what does that imply about the offset? - How does
dup2relate to shell redirection like> out.log? - What is the difference between
SEEK_ENDandSEEK_CUR? - How does a sparse file save disk space, and how can you observe it?
- What is
FD_CLOEXECfor?