K&R C Exercise 8-1: cat with read, write, open and close

Exercise 8-1. Rewrite the program cat of Chapter 7 using read, write, open and close instead 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_t and checking that write wrote 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:

All K&R Solutions  | 
Exercise 8-2 →

Book:

The C Programming Language, 2nd Ed — Kernighan & Ritchie

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>