Katkat

Implement the Linux cp Command, Then Explain Its OS and C++ Internals

MicrosoftSoftware Engineer · Onsite

Last updated

A low-level engineering exercise to implement the Linux cp command in C++ and then explain what happens beneath it. It tests system calls, short writes and error handling, the page cache and kernel-assisted copying, and RAII-based management of file descriptors.

How would you implement the Linux cp command? Write it, then be ready for the discussion to go deep into the operating-system and C++ concepts underneath your implementation.

Clarifying Questions

  • Which interfaces may I use: POSIX system calls, the C standard library, or C++17 std::filesystem?
  • Should the first version handle only regular files, or also directories (-r), symbolic links and special files?
  • If DEST already exists, should it be overwritten silently, or should the program ask or fail?
  • Must permissions, ownership and timestamps be preserved, as cp -p does?

Part 1 — Implement the basic copy

Write a C++ program, invoked as cp SOURCE DEST, that copies a regular file. DEST may be a new path, an existing file, or an existing directory. Handle the errors a real command-line tool has to handle, and exit with a non-zero status on failure.

What This Part Should Cover

  • A correct copy loop, including partial writes, interrupted calls and end of file
  • Handling of an existing file, a directory target, and SOURCE and DEST being the same file
  • Clear error messages and release of every resource on every path

Part 2 — What happens underneath

Walk through what the operating system does while your program runs: from each system call into the kernel, through the file system and its caches, down to the disk. Then explain how you would make the copy faster for very large files.

What This Part Should Cover

  • User space and kernel space, the cost of a system call, and the effect of the buffer size
  • The page cache, read-ahead and write-back, and what a successful write actually guarantees
  • Kernel-assisted copying and when each option applies or falls back

Part 3 — Make it robust C++

Now treat the code as production C++. How do you guarantee that file descriptors are always released and that errors are reported correctly? What would it take to support directories, symbolic links and preserving metadata, and to avoid leaving a half-written DEST behind when the copy fails?

What This Part Should Cover

  • Descriptor ownership through RAII, including copy and move semantics
  • An error model (exceptions or error codes) and when errno must be read
  • Recursive copy, symbolic links, metadata, and never leaving a half-written file behind

Hints

Hint 1

Think in system calls: Decide which calls you need to open, read, write and close the files, and what each one can return besides full success.

Hint 2

Look closely at DEST before writing to it: DEST may already exist, may be a directory, or may even be SOURCE itself under another name.

Hint 3

Count the copies and the crossings: Follow one block of data and count how many times it crosses between user space and the kernel, and how many times it is copied in memory.

Hint 4

Ask whether your program needs the bytes at all: Consider what the kernel or the file system could do if the data never had to pass through your buffer.

Hint 5

Let object lifetime do the cleanup: Think about tying the release of a descriptor to the end of an object's lifetime, and what copying such an object should mean.

Hint 6

Decide what a failure leaves behind: If the copy fails halfway, think about what the user finds at DEST afterwards.

Follow-up questions

  • How would you make the new DEST appear atomically to other processes reading it?
  • How do you copy a sparse file without filling its holes with zeros?
  • What does your program produce if another process is writing to SOURCE during the copy?
  • How does moving a file differ from copying it when the source and destination are on the same file system, and when they are not?