Exercise 8-1. Rewrite the program
catof Chapter 7 usingread,write,openandcloseinstead of their standard library equivalents. Perform experiments to determine the relative speeds of the two versions.
The Chapter 7 cat goes through the standard library — getchar and putchar — which quietly buffer I/O for you. This exercise drops down one level to the raw UNIX system calls. The key idea: read(fd, buf, n) and write(fd, buf, n) move up to n bytes per call, and every call crosses into the kernel. That crossing costs roughly a microsecond regardless of how many bytes travel with it — so the buffer size you choose is the performance story, as the experiment below shows brutally.
Two conventions worth noting: file descriptors 0 and 1 are stdin and stdout (so cat with no arguments is just filecopy(0, 1)), and on error we use _exit from inside the copy loop — with no stdio buffers in play there is nothing to flush.
Solution
/* K&R Exercise 8-1: cat using read, write, open, close
* Compile: gcc -ansi -Wall -Wextra ex8_1.c -o cat_syscall */
#define _POSIX_C_SOURCE 200112L
#include <stdio.h> /* fprintf for error messages only */
#include <fcntl.h>
#include <unistd.h>
#define BUFSIZE 4096
/* copy file descriptor from to file descriptor to */
static void filecopy(int from, int to)
{
char buf[BUFSIZE];
ssize_t n;
while ((n = read(from, buf, BUFSIZE)) > 0) {
if (write(to, buf, (size_t)n) != n) {
fprintf(stderr, "cat: write error\n");
_exit(2);
}
}
}
int main(int argc, char *argv[])
{
int fd;
if (argc == 1) { /* no args: copy stdin */
filecopy(0, 1);
} else {
while (--argc > 0) {
if ((fd = open(*++argv, O_RDONLY, 0)) == -1) {
fprintf(stderr, "cat: can't open %s\n", *argv);
return 1;
}
filecopy(fd, 1);
close(fd);
}
}
return 0;
}
Compile and Run
gcc -ansi -Wall -Wextra ex8_1.c -o cat_syscall
./cat_syscall file1.txt file2.txt # cat named files
./cat_syscall < input.txt # or filter stdin
Sample Output
$ printf "line one\nline two\n" > small.txt $ ./cat_syscall small.txt line one line two $ printf "via stdin\n" | ./cat_syscall via stdin
The Speed Experiment
Copying the same 10 MB file to /dev/null on Linux (gcc, x86-64), measured with time -p — real captured runs:
| Version | Syscalls made | Time |
|---|---|---|
Chapter 7 getchar/putchar (stdio) |
~2,400 (glibc buffers 8 KB internally) | 0.09 s |
read/write, BUFSIZE 4096 |
~5,000 | < 0.01 s |
read/write, BUFSIZE 1 |
~20,000,000 | 13.10 s |
The lesson in one line: the cost is the number of kernel crossings, not the bytes moved. With a 4 KB buffer the raw syscall version edges out stdio (no per-character macro work), but shrink the buffer to 1 byte and the same program becomes more than a thousand times slower — 20 million kernel entries for 10 MB. stdio’s whole reason to exist is doing this buffering so you never have to think about it.
What This Exercise Teaches
open,read,write,close— the UNIX file-descriptor interface under stdio- File descriptors 0/1/2 as pre-opened stdin, stdout, stderr
- Why system calls are expensive: user/kernel mode switches dominate small-buffer I/O
ssize_tand checking thatwritewrote everything it was asked to
Set Up Your C Environment
This chapter uses UNIX system calls — a Linux machine, macOS terminal, or WSL2 on Windows all work:
- Install GCC on Ubuntu/Linux
- Install GCC on Windows 11 (use the WSL2 path for this chapter)
- Install GCC on macOS
All K&R Solutions |
Exercise 8-2 →
Book:
The C Programming Language, 2nd Ed — Kernighan & Ritchie