C++ Module 20 – Systems programming with C++
POSIX in C++: files, pipes and child processes
Use open, read, write and close through an RAII descriptor in C++, turn errno into std::error_code, and run programs whose output comes back through a pipe.
What you will learn
- Wrap each POSIX file descriptor in a move-only owner that closes it
- Convert errno into a std::error_code and compare it with std::errc
- Capture a child program's output with fork, exec, a pipe and waitpid
- Choose between fork and exec, posix_spawn, popen and std::system
- Reproduce and fix short writes, EINTR, leaked descriptors and wait-order deadlocks
Before you start
What you’ll learn · Before you start
What you will learn
- Wrap each POSIX file descriptor in a move-only owner that closes it
- Convert errno into a std::error_code and compare it with std::errc
- Capture a child program's output with fork, exec, a pipe and waitpid
- Choose between fork and exec, posix_spawn, popen and std::system
- Reproduce and fix short writes, EINTR, leaked descriptors and wait-order deadlocks
Before you start
On this page
A program that starts echo and keeps the 19 bytes it prints needs pipe, fork, dup2, execvp, read and
waitpid, and one forgotten close makes it wait forever. Below <fstream>, POSIX, the standard that macOS and Linux
implement, gives C++ file descriptor: A small non-negative integer that a process uses to name an open file, pipe or socket. open() returns the lowest number not in use, and read(), write() and close() take it as their first argument., small integers that name an open file,
pipe: A one-way channel in the kernel with a write end and a read end, each a file descriptor. Bytes come out of the read end in the order they went in, and read() returns 0 once the pipe is empty and every write end is closed. or socket, and child processes, started with fork and exec, whose output a pipe brings
back.
Calls such as open, read and fork return -1 and set errno: The number a failed C or POSIX call leaves behind to say why it failed, such as ENOENT for a file that does not exist. The return value says that the call failed; errno, read right away, says why. when they fail, and
std::error_code turns that into an error you can compare and print. Three habits keep this safe on macOS and Linux:
every descriptor has one owner that closes it, every read and write is ready to move fewer bytes than it asked for,
and every pipe end a process does not use is closed.
A descriptor is a small number with exactly one owner
copy.cpp copies a 10,000-byte file with open, read, write and close. Each descriptor it opens belongs to a
FileDescriptor from fd.hpp, which is move-only, like std::unique_ptr, and closes the descriptor in its destructor:
C++ header · fd.hpp
// fd.hpp: a POSIX file descriptor with exactly one owner, and the small helpers that POSIX code needs.
#pragma once
#include <cerrno>
#include <cstddef>
#include <expected>
#include <span>
#include <system_error>
#include <utility>
#include <fcntl.h>
#include <unistd.h>
// errno as a portable std::error_code, read right after the call that failed.
inline std::error_code last_error() { return {errno, std::generic_category()}; }
class FileDescriptor {
public:
FileDescriptor() = default;
explicit FileDescriptor(int fd) noexcept : fd_(fd) {}
FileDescriptor(FileDescriptor&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) reset(std::exchange(other.fd_, -1));
return *this;
}
~FileDescriptor() { reset(); }
int get() const noexcept { return fd_; }
explicit operator bool() const noexcept { return fd_ >= 0; }
void reset(int fd = -1) noexcept {
if (fd_ >= 0) ::close(fd_); // called once: after EINTR, Linux has already closed fd_ and may reuse the number
fd_ = fd;
}
private:
int fd_ = -1; // -1: owns nothing
};
// open(2) with O_CLOEXEC, so the descriptor never leaks into programs started later.
inline std::expected<FileDescriptor, std::error_code> open_file(const char* path, int flags, mode_t mode = 0644) {
int fd = ::open(path, flags | O_CLOEXEC, mode);
if (fd < 0) return std::unexpected(last_error());
return FileDescriptor(fd);
}
// write(2) may write fewer bytes than asked: keep going until everything is written.
inline std::error_code write_all(int fd, std::span<const std::byte> data) {
while (!data.empty()) {
ssize_t n = ::write(fd, data.data(), data.size());
if (n < 0) {
if (errno == EINTR) continue; // interrupted by a signal before writing anything: try again
return last_error();
}
data = data.subspan(static_cast<std::size_t>(n));
}
return {};
}
// A pipe whose two ends close themselves in programs started by exec. macOS has no pipe2(), so the flag is set
// right after pipe(); on Linux, pipe2(ends, O_CLOEXEC) does both in one step.
struct Pipe {
FileDescriptor read_end, write_end;
};
inline std::expected<Pipe, std::error_code> make_pipe() {
int ends[2];
if (::pipe(ends) != 0) return std::unexpected(last_error());
Pipe p{FileDescriptor(ends[0]), FileDescriptor(ends[1])};
if (::fcntl(ends[0], F_SETFD, FD_CLOEXEC) != 0 || ::fcntl(ends[1], F_SETFD, FD_CLOEXEC) != 0)
return std::unexpected(last_error());
return p;
} C++ · copy.cpp
// copy.cpp: copies a file with open, read, write and close, and reports failures as std::error_code.
#include "fd.hpp"
#include <array>
#include <format>
#include <print>
#include <string>
#include <vector>
// What one copy did, for the report: the two descriptors and every value that read() returned.
struct CopyLog {
int from_fd = -1, to_fd = -1;
std::vector<ssize_t> reads;
std::size_t copied = 0;
};
std::error_code copy_file(const char* from, const char* to, CopyLog& log) {
auto in = open_file(from, O_RDONLY);
if (!in) return in.error();
auto out = open_file(to, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (!out) return out.error();
log.from_fd = in->get();
log.to_fd = out->get();
std::array<std::byte, 4096> buffer;
for (;;) {
ssize_t n = ::read(in->get(), buffer.data(), buffer.size());
log.reads.push_back(n);
if (n == 0) return {}; // end of file: both descriptors close as in and out go out of scope
if (n < 0) {
if (errno == EINTR) continue;
return last_error();
}
if (auto e = write_all(out->get(), std::span(buffer).first(static_cast<std::size_t>(n)))) return e;
log.copied += static_cast<std::size_t>(n);
}
}
void report(const char* from, const char* to) {
CopyLog log;
std::error_code e = copy_file(from, to, log);
if (e) {
std::println("{} -> {}: {} (errno {})", from, to, e.message(), e.value());
return;
}
std::println("{} -> {}: copied {} bytes", from, to, log.copied);
std::println(" descriptors {} and {}; read() returned {}", log.from_fd, log.to_fd, log.reads);
}
int main() {
std::string text;
for (int i = 1; i <= 1000; ++i) text += std::format("line {:04}\n", i); // 10 bytes per line
{
auto f = open_file("notes.txt", O_WRONLY | O_CREAT | O_TRUNC);
if (!f || write_all(f->get(), std::as_bytes(std::span(text)))) return 1;
}
report("notes.txt", "copy.txt");
report("missing.txt", "copy.txt");
report("notes.txt", "."); // "." is a directory
CopyLog log;
std::println("missing.txt is errc::no_such_file_or_directory: {}",
copy_file("missing.txt", "x.txt", log) == std::errc::no_such_file_or_directory);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined copy.cpp -o copy && ./copy
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined copy.cpp -o copy-gcc && ./copy-gcc
cmp notes.txt copy.txt && echo "copy.txt is identical to notes.txt"
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined copy.cpp -o copy && ./copy notes.txt -> copy.txt: copied 10000 bytes descriptors 3 and 4; read() returned [4096, 4096, 1808, 0] missing.txt -> copy.txt: No such file or directory (errno 2) notes.txt -> .: Is a directory (errno 21) missing.txt is errc::no_such_file_or_directory: true $ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined copy.cpp -o copy-gcc && ./copy-gcc notes.txt -> copy.txt: copied 10000 bytes descriptors 3 and 4; read() returned [4096, 4096, 1808, 0] missing.txt -> copy.txt: No such file or directory (errno 2) notes.txt -> .: Is a directory (errno 21) missing.txt is errc::no_such_file_or_directory: true $ cmp notes.txt copy.txt && echo "copy.txt is identical to notes.txt" copy.txt is identical to notes.txt
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 copy_session.py
open gave out 3 and 4, the lowest numbers not in use. read returns the number of bytes it read, 0 at the end of the
file, or -1 with errno set, and write may write fewer bytes than asked, so both run in loops that also call again
when a signal interrupts them (EINTR). Step through the copy and watch the descriptors and the offsets:
The drawing shows the whole scene; its 9 steps are listed below.
Every step as text
- Before the copy.
mainhas written notes.txt through descriptor 3 and closed it again, so only 0, 1 and 2, the standard streams, are open. NeitherFileDescriptorowns anything yet: each holds -1. - open notes.txt.
open_file("notes.txt", O_RDONLY)gets 3, the lowest number not in use, andintakes it. The open file description behind it starts at offset 0. - open copy.txt.
O_CREAT | O_TRUNCcreates copy.txt, or empties it if it is there, andouttakes the next number, 4. - First read: 4,096.
read(3, …)fills the 4,096-byte buffer and returns 4,096, the first value the run printed. The offset of notes.txt moves to 4,096. - write_all: 4,096.
write_all(4, …)writes the 4,096 bytes into copy.txt. It callswriteagain whenever one call moves fewer bytes than were left. - Second read: 4,096. The second read returns 4,096 again and moves the offset to 8,192.
write_allcopies those bytes as well. - A short read: 1,808. Only 1,808 bytes are left, so the third read returns 1,808: fewer than it asked for, and not an error. After
write_all, copy.txt holds all 10,000 bytes. - End of file: 0. The fourth read returns 0, because the offset is at the end of the file.
copy_filereturns an emptystd::error_code, which means success. - Two destructors, two closes. Leaving
copy_filedestroysoutfirst, which closes 4, and thenin, which closes 3: local objects are destroyed in the reverse order of their construction. Each early return closes what was opened the same way.
A descriptor is only a number. It refers to an open file description: The kernel's record of one opening of a file: the current offset and the flags given to open(). Descriptors made by dup2() or fork() share one description; two calls to open() make two. in the
kernel, which holds the file’s offset and the flags given to open. Descriptors made by dup2 and fork share one
description, and so one offset; two calls to open make two. open_file also passes O_CLOEXEC, which sets the
close-on-exec: A flag on a file descriptor, FD_CLOEXEC, that makes exec close it, so the program started next never inherits it. O_CLOEXEC sets the flag when open() creates the descriptor. flag: a program started with exec never inherits the descriptor.
A descriptor wrapper that can be copied
struct Fd { int fd; ~Fd() { ::close(fd); } }; can be copied, and then two objects close one number; the second
close may hit a file opened since. Declaring the move operations deletes the copies, as in FileDescriptor, and
std::exchange hands the number over.
Checkpoint
Check: A descriptor and its one owner
errno becomes an error code that compares on every system
The two failed copies came back as std::error_code values: a failed call returns -1 and leaves a number in errno,
which last_error() wraps at once as std::error_code(errno, std::generic_category()). Follow the missing file, stage
by stage, from the failed open to the lines the run printed:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The call fails.
open_file("missing.txt", O_RDONLY)callsopen, which returns -1 because no file has that name. - errno says why. The failed call has left 2 in
errno, the number ofENOENT, "no such file or directory". The -1 says only that the call failed. - Wrapped at once.
last_error()turnserrnointostd::error_code(errno, std::generic_category())straight away, before another call can change it. - Returned, not thrown.
open_filereturns the code insidestd::unexpected, andcopy_filepasses it up as its own result. Nothing is thrown, and nothing is printed yet. - Printed.
reportprints the code'smessage(), the system's text, and itsvalue(), the number: "No such file or directory (errno 2)". - Compared.
== std::errc::no_such_file_or_directoryprinted true. The condition names the error, so the same test holds on a system that gives it another number.
message() gave the system’s text and value() the number: 2 for ENOENT, the missing file, and 21 for EISDIR, the
directory. The comparison with std::errc::no_such_file_or_directory printed true, and it holds on every system,
because POSIX fixes the names of the error numbers but not their values. Read errno straight after the failure,
before any other call: a later call may change it, even one that succeeds.
Checkpoint
Check: errno as a std::error_code
A child runs the program, and a pipe brings back its output
run_and_capture in capture.cpp runs a program in a child process and returns its status and its standard output.
Four children show the four outcomes:
// capture.cpp: runs a program in a child process and collects what it writes to standard output, through a pipe.
#include "fd.hpp"
#include <print>
#include <string>
#include <system_error>
#include <vector>
#include <sys/wait.h>
struct Result {
int status; // as waitpid reports it: decode with WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG
std::string output;
};
Result run_and_capture(std::vector<const char*> argv) {
auto pipe = make_pipe();
if (!pipe) throw std::system_error(pipe.error(), "pipe");
argv.push_back(nullptr); // exec wants a null-terminated array
pid_t pid = ::fork();
if (pid < 0) throw std::system_error(last_error(), "fork");
if (pid == 0) {
// The child, a copy of this process. dup2 makes descriptor 1, its standard output, refer to the pipe; the
// copy it makes has no close-on-exec flag, so it is the only end of the pipe the new program keeps.
::dup2(pipe->write_end.get(), STDOUT_FILENO);
::execvp(argv[0], const_cast<char* const*>(argv.data()));
::_exit(127); // only reached if exec failed; 127 is what shells use for "command not found"
}
// The parent. Its own write end must go, or read() would wait for this process to write and never see the end.
pipe->write_end.reset();
std::string output;
char buffer[4096];
for (;;) {
ssize_t n = ::read(pipe->read_end.get(), buffer, sizeof buffer);
if (n > 0) output.append(buffer, static_cast<std::size_t>(n));
else if (n == 0) break; // end of file: every write end is closed, so the child has finished writing
else if (errno != EINTR) throw std::system_error(last_error(), "read");
}
int status = 0;
while (::waitpid(pid, &status, 0) < 0) // reaps the child, so it does not stay behind as a zombie
if (errno != EINTR) throw std::system_error(last_error(), "waitpid");
return {status, std::move(output)};
}
void show(std::vector<const char*> argv) {
Result r = run_and_capture(argv);
std::string how = WIFEXITED(r.status) ? std::format("exited with {}", WEXITSTATUS(r.status))
: WIFSIGNALED(r.status) ? std::format("killed by signal {}", WTERMSIG(r.status))
: "stopped";
std::println("{}: {}, {} bytes {:?}", argv[0], how, r.output.size(), r.output);
}
int main() {
show({"echo", "hello from a child"});
show({"sh", "-c", "echo one; echo two; exit 3"});
show({"no-such-program"});
show({"sh", "-c", "kill -TERM $$"});
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined capture.cpp -o capture && ./capture
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined capture.cpp -o capture-gcc && ./capture-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined capture.cpp -o capture && ./capture echo: exited with 0, 19 bytes "hello from a child\n" sh: exited with 3, 8 bytes "one\ntwo\n" no-such-program: exited with 127, 0 bytes "" sh: killed by signal 15, 0 bytes "" $ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address,undefined capture.cpp -o capture-gcc && ./capture-gcc echo: exited with 0, 19 bytes "hello from a child\n" sh: exited with 3, 8 bytes "one\ntwo\n" no-such-program: exited with 127, 0 bytes "" sh: killed by signal 15, 0 bytes ""
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_session.py
fork returns twice, once in each process: the child gets 0, and the parent gets the new child’s process ID. The child
points its standard output at the pipe with dup2 and becomes echo with execvp, which searches PATH. The parent
closes its own copy of the write end, reads until read returns 0, and only then calls waitpid. The diagram shows
where each end of the pipe ends up:
Capturing a child's output through a pipe
Text description of the diagram
The diagram shows three boxes from top to bottom: the parent at the top, the child below it and the pipe at the bottom, joined by three arrows that go round in a loop.
- The parent, capture, keeps only the read end of the pipe: it closed its own copy of the write end right after fork, or it would never see end of file.
- A dashed arrow from the parent down to the child is labelled "fork(), then waitpid()": the parent created the child with fork and, after reading everything, collects its exit status with waitpid.
- The child is the program started with exec, here echo. Its descriptor 1, standard output, is the write end of the pipe, because dup2 copied the write end onto descriptor 1 before exec. An arrow labelled write() goes from the child down to the pipe.
- The pipe lives in the kernel and holds up to 64 KiB; when it is full, the writer waits. An arrow from the pipe back up to the parent says that read() returns 0, end of file, once every write end is closed.
Step through the first child, the echo that wrote 19 bytes, call by call:
The drawing shows the whole scene; its 10 steps are listed below.
Every step as text
- pipe().
make_pipe()creates the pipe and marks both of its ends close-on-exec. The parent holds the read end and the write end. - fork().
fork()copies the parent into a child, both ends of the pipe included, and returns in both processes: the child sees 0, the parent sees the child's process ID. - dup2(). The child makes descriptor 1, its standard output, refer to the write end. The copy that
dup2makes has no close-on-exec flag. - execvp("echo").
execvpsearchesPATHand replaces the child's program with echo. exec closes every close-on-exec descriptor, so echo keeps only descriptor 1 on the pipe. - The parent's write end goes. The parent resets its own write end. From now on echo is the only writer; without this close, the parent's read could never reach the end of the file.
- echo writes 19 bytes. echo writes "hello from a child" and a newline, 19 bytes, into the pipe.
- read() returns 19. The parent's
readreturns the 19 bytes, andrun_and_captureadds them to its output. - echo exits. echo exits with status 0. Its descriptor 1 closes, and that was the last write end of the pipe. Until the parent waits for it, the child is a zombie.
- read() returns 0. The next
readreturns 0: the pipe is empty and no writer is left, so the output is complete. Only now does the parent wait. - waitpid().
waitpidcollects the status and removes the zombie.WIFEXITEDis true andWEXITSTATUSgives 0, the first line of the run with its 19 bytes.
The status needs macros to read: WIFEXITED and WEXITSTATUS gave 3 for exit 3, and WIFSIGNALED and WTERMSIG
gave signal 15, SIGTERM. A missing program ends the child in _exit(127), the shells’ code for “command not found”,
because only a failed exec returns. A child that has finished but has not been waited for stays in the process table
as a zombie process: A child process that has finished but whose exit status its parent has not collected yet with wait() or waitpid(). It keeps its entry in the process table until the parent waits for it., so every fork needs its waitpid.
Key idea
Close the pipe ends you do not use, read until read returns 0, then call waitpid, which collects the status and
removes the zombie.
Checkpoint
Check: Capturing a child's output
Three other ways to start a program, and when each fits
Three other calls do more of the work, each at a cost:
| Call | Through a shell | The output comes back | Waits |
|---|---|---|---|
posix_spawn, posix_spawnp |
No | Through file actions such as dup2 |
When you call waitpid |
popen, then pclose |
Yes, /bin/sh |
As a FILE* opened with "r" |
In pclose, which returns the status |
std::system |
Yes | Never: it captures nothing | Until the command ends |
popen works in one direction, as POSIX and Linux define it: "r" reads what the command prints, and "w" writes to
its standard input. macOS also accepts "r+", one stream that does both. On Windows, CreateProcess starts a
program, and pipes come from CreatePipe.
Keep text from users away from the shell
popen and std::system hand their command to /bin/sh, which reads spaces, quotes and semicolons in a file name
as part of the command. Pass an array of arguments to execvp or posix_spawnp, and each one reaches the program as
it is.
Checkpoint
Check: Choosing how to start a program
Check yourself
Practise the ideas above on new cases, one question at a time, with the idea you know least asked first.
Practice that adapts to you
Practise this lesson
13 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Short transfers, EINTR and leaked descriptors, reproduced Pro: opens with a Pro, Premium or Ultimate pass
Code that works in a quick test fails under load when a call moves fewer bytes than asked, when a signal interrupts it, or when a descriptor leaks into another program. Each case is reproduced here on both compilers, with its fix.
In this part:
- A write that stops at 65,536 bytes, measured
- Two writers, one log file: 1,000 of 2,000 lines lost
- EINTR reproduced: one read, interrupted and then restarted
- A leaked descriptor that keeps a pipe open
- Generic or system category, and how two error codes compare
3 diagrams · 10 runnable examples (C++, C++ (cloexec_leak.cpp), C++ (pipe2_probe.cpp), Python) · about 15 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Running children without deadlocks: wait order, exec errors and an exercise Pro: opens with a Pro, Premium or Ultimate pass
A parent and a child that wait for each other hang forever, and a program that cannot be started can look exactly like one that failed. This part reproduces the deadlock, tells the two failures apart, and ends with an exercise that feeds a child while reading from it.
In this part:
- The wait-first deadlock, reproduced with a 1-second deadline
- Telling “could not start” apart from “exited with 127”
- Your turn: feed a child and read it at the same time
2 diagrams · 6 runnable examples (C++, C++ header, Python) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
What do the return values 0 and -1 of read() mean, and when should the call simply be made again?
read() returns the number of bytes it placed in the buffer, which may be fewer than asked for: a pipe or socket returns what has arrived, and a file near its end returns what is left. 0 means end of file: a regular file has no more bytes, or every write end of a pipe or the other side of a socket is closed. -1 means an error, and errno says which. Two errors are not real failures. EINTR means a signal arrived before anything was read, so the call is made again. EAGAIN, or EWOULDBLOCK, on a non-blocking descriptor means nothing is there yet, so the program waits, for example with poll(), and then reads again. Every other errno value is a failure to report, best as a std::error_code built with std::generic_category().
-
Mid to senior
A C++ service runs a command-line converter and reads its output through a pipe. It works for small files but hangs forever on large ones, and both processes stay alive. What is the likely bug, how do you confirm it, and how do you fix it?
-
Mid to senior
After a restart, a server cannot bind its port and reports "Address already in use". The old server process is gone, but a helper it started hours earlier is still running. Explain what holds the port and fix the server.
-
Senior to staff
Design the run_command() function of a C++ build tool that starts thousands of compiler processes from a multithreaded parent: arguments, environment, working directory, output capture, time limits and errors. Would you use fork and exec or posix_spawn, and why?
Key takeaways
- A file descriptor is a small number that
openhands out lowest first: give each one a move-only owner that closes it, and open withO_CLOEXEC. readandwritemay move fewer bytes than asked, andreadreturns 0 at the end of the file, so both run in loops that also call again afterEINTR.- Wrap
errnoat once in astd::error_codeofstd::generic_category(), and compare it withstd::errc. - Run a program with
forkandexec, close the pipe ends you do not use, read until end of file, then callwaitpid. - Give programs an array of arguments:
popenandstd::systemgo through a shell, which must never see text from users.
Terms in this lesson
- close-on-exec
- A flag on a file descriptor, FD_CLOEXEC, that makes exec close it, so the program started next never inherits it. O_CLOEXEC sets the flag when open() creates the descriptor.
- errno
- The number a failed C or POSIX call leaves behind to say why it failed, such as ENOENT for a file that does not exist. The return value says that the call failed; errno, read right away, says why.
- file descriptor
- A small non-negative integer that a process uses to name an open file, pipe or socket. open() returns the lowest number not in use, and read(), write() and close() take it as their first argument.
- open file description
- The kernel's record of one opening of a file: the current offset and the flags given to open(). Descriptors made by dup2() or fork() share one description; two calls to open() make two.
- pipe
- A one-way channel in the kernel with a write end and a read end, each a file descriptor. Bytes come out of the read end in the order they went in, and read() returns 0 once the pipe is empty and every write end is closed.
- zombie process
- A child process that has finished but whose exit status its parent has not collected yet with wait() or waitpid(). It keeps its entry in the process table until the parent waits for it.
References
- The Open Group Base Specifications Issue 8 (POSIX.1-2024) (The Open Group)
- open, openat: open file (The Open Group)
- read: read from a file (The Open Group)
- write: write on a file (The Open Group)
- close, posix_close: close a file descriptor (The Open Group)
- close(2): close a file descriptor (Linux man-pages) (Linux man-pages project)
- close(2): delete a descriptor (XNU manual page) (Apple)
- errno: error return value (The Open Group)
- <errno.h>: system error numbers (The Open Group)
- pipe, pipe2: create an interprocess channel (The Open Group)
- fork: create a new process (The Open Group)
- exec: execute a file (The Open Group)
- wait, waitpid: wait for a child process to stop or terminate (The Open Group)
- dup, dup2, dup3: duplicate an open file descriptor (The Open Group)
- posix_spawn, posix_spawnp: spawn a process (The Open Group)
- sigaction: examine and change a signal action (The Open Group)
- fcntl: file control (The Open Group)
- popen: initiate a pipe stream to or from a process (The Open Group)
- popen(3): pipe stream to or from a process (Linux man-pages) (Linux man-pages project)
- popen(3): process I/O (macOS manual page) (Apple)
- System Interfaces, General Information: signal concepts, async-signal-safe functions and file descriptor allocation (The Open Group)
- pipe(7): overview of pipes and FIFOs (Linux man-pages) (Linux man-pages project)
- fcntl(2): file control (XNU manual page) (Apple)
- Working Draft, C++: [stmt.jump.general] Jump statements, general (ISO C++ committee working draft)
- Working Draft, C++: [class.copy.ctor] Copy/move constructors (ISO C++ committee working draft)
- std::error_code (cppreference.com)
- std::generic_category (cppreference.com)
- std::system_category (cppreference.com)
- std::system (cppreference.com)
- CreateProcessW function (processthreadsapi.h) (Microsoft)
- CreatePipe function (namedpipeapi.h) (Microsoft)
- pthread_sigmask, sigprocmask: examine and change blocked signals (The Open Group)
- Command Injection (OWASP Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress