Ch11.20: Processes and Pipes
1. Why Processes Belong in the I/O Chapter
A new process does not get new kinds of I/O. It gets three inherited handles — standard input, standard output and standard error — plus whatever additional handles you choose to pass to it. That is the same file-shaped model the rest of this chapter has been using. Starting a process is therefore not a separate subsystem; it is a redirection problem with a lifetime attached.
fast_io exposes that model through native_process and
native_pipe. The names are platform-neutral: on POSIX they resolve
to the fork/exec implementation, on Windows to
CreateProcess or the NT process API. The code you write does not
change.
This page is about the shape of the API, not about every platform flag. The rule
is the same one used everywhere else in this book: keep the child's handles
explicit, close the ends you do not use, and let transmit drain a
pipe instead of writing another read/write loop.
2. native_process: Spawn, Wait, Detach
The constructor takes the executable, its arguments, its environment, its
standard-I/O redirection, and a process_mode bitmask. Every argument
has a default, so the smallest useful program is a path and a wait:
#include <fast_io.h>
int main()
{
::fast_io::native_process child{
"child.exe",
::fast_io::native_process_args{},
{},
{.in = ::fast_io::in(), .out = ::fast_io::out(), .err = ::fast_io::err()},
::fast_io::process_mode::none};
auto status{::fast_io::wait(child)};
#if defined(_WIN32)
::fast_io::io::println("exit code: ", ::fast_io::wait_status_to_int(status));
#else
if (WIFEXITED(status.wait_loc))
{
::fast_io::io::println("exit code: ", WEXITSTATUS(status.wait_loc));
}
#endif
}
native_process is move-only and owns the child handle. Its destructor
waits for the child if nobody has waited yet, so a forgotten process does not
become an unbounded zombie. wait consumes that ownership and returns
the platform's wait status. On POSIX the status is the raw
waitpid value, which is why the example uses WIFEXITED
and WEXITSTATUS; on Windows the same field is already the exit code.
There are two ways to give up the wait. detach(child) releases the
handle and lets the child keep running; kill(child, status) asks the
platform to terminate it. kill is deliberately platform-shaped: on
POSIX it sends a signal, so pass a posix_wait_status containing the
signal number; on Windows it terminates the process with the supplied exit code.
3. Arguments and Environment
native_process_args is variadic and accepts anything
fast_io can print. It handles the platform string conversion, so you
do not build a char* array or a UTF-16 command line by hand:
::fast_io::native_process child{
"child.exe",
::fast_io::native_process_args{
"plain", 42, 3.5, u8"utf-8", L"wide", U"utf-32"},
::fast_io::native_process_envs{
"MODE=test", "LEVEL=2"},
{.in = ::fast_io::in(), .out = ::fast_io::out(), .err = ::fast_io::err()}};
By default the executable path is prepended to the argument list as
argv[0]. If you need to choose argv[0] yourself, pass
the args_with_argv0 tag:
::fast_io::native_process child{
"child.exe",
::fast_io::args_with_argv0,
::fast_io::native_process_args{"my-name", "--verbose"}};
An empty native_process_envs means “inherit the parent's
environment”. A non-empty one is the complete environment for the child,
not an addition to the parent's. If you want to extend the parent's environment,
read what you need from the parent and pass the full set explicitly.
4. Redirection: The Three Standard Handles
The fourth constructor argument is a native_process_io with three
fields: in, out and err. Each field accepts
a fast_io device, a pipe, or posix_dev_null(). The last
one is the platform-independent spelling for “discard this stream”,
even on Windows.
// Child inherits the terminal for stdout/stderr, and reads nothing.
::fast_io::native_process quiet{
"child.exe",
{},
{},
{.in = ::fast_io::posix_dev_null(),
.out = ::fast_io::out(),
.err = ::fast_io::err()}};
Redirection is established in the child before the program starts. The parent
keeps its own handles; the child receives duplicates. This is why redirecting
stdout to a file does not close the parent's terminal, and why a
pipe can be read by the parent after the child exits.
5. native_pipe and iobuf_pipe
A pipe is a unidirectional byte stream with two ends. native_pipe is
the unbuffered pair of native handles: in() is the read end and
out() is the write end. iobuf_pipe wraps that pair in
the same buffered I/O object used by iobuf_file; its
.handle member is the underlying native_pipe.
| Type | Buffered | Use it for |
|---|---|---|
::fast_io::native_pipe |
No | Byte movement with transmit, or when you need the raw handles
for poll/select. |
::fast_io::iobuf_pipe |
Yes | print, scan, and line-oriented protocols.
scan requires a buffered input, so this is the type to use when
the pipe carries values rather than raw bytes. |
The pipe object owns the whole pair, including the write end. When the read side sees end-of-file, that means every write end in every process has been closed. If the parent keeps its copy of the write end open, the read side will wait forever. This is the single most common pipe bug.
6. Capturing a Child's Output
The following program gives the child a null stdin, a pipe for stdout, and the
parent's stderr. It closes the parent's copy of the pipe's write end, then uses
transmit to drain the read end until the child exits and the pipe
reaches end-of-file:
#include <fast_io.h>
#include <fast_io_device.h>
int main()
{
::fast_io::iobuf_pipe child_out;
::fast_io::native_process child{
"child.exe",
::fast_io::native_process_args{},
{},
{.in = ::fast_io::posix_dev_null(),
.out = child_out.handle,
.err = ::fast_io::err()},
::fast_io::process_mode::none};
// The parent must not keep the write end open.
child_out.handle.out().close();
// Byte movement, not formatting: no temporary vector, no read/write loop.
::fast_io::io::transmit(child_out, ::fast_io::out());
auto status{::fast_io::wait(child)};
::fast_io::io::println(::fast_io::err(), "child finished");
}
If you need both stdout and stderr, do not drain them one after the other unless
you know the child's output is small. A child that fills the stderr pipe while
the parent is blocked reading stdout will deadlock. Either drain both
concurrently with threads or coroutines, redirect one of them to a file or
posix_dev_null(), or use poll on the two read ends.
Chapter 23 shows the asynchronous version of the same idea.
7. Pipes Are Not Message Queues
A pipe preserves bytes, not boundaries. Two writes may be read as one chunk, and
one write may be read in pieces. On POSIX, a write of at most
PIPE_BUF bytes is atomic with respect to other writers; larger writes
are not. A protocol that needs records must define a length prefix, a delimiter,
or a fixed-size header, and then use print/scan or an
explicit framing layer on top.
EOF is also a property of the write ends, not of a message. If a process forks another child, every inherited write end must be closed before the reader can see EOF. This is a feature when you use it deliberately and a hang when you forget it.
On POSIX, writing to a pipe with no reader can raise SIGPIPE. A
long-running server should decide whether to ignore that signal or handle it as a
normal write error, and should not discover the default disposition by crashing.
8. Lifetime, Failure and Platform Notes
-
Ownership.
native_processis move-only. Moving it transfers the child handle; copying would create two owners and two waits. -
Destructor. It waits for the child. Call
waitwhen you need the status, anddetachwhen you deliberately want the child to outlive the object. -
Failure. Construction and
waitreport errors as herbceptions. The samecatch throws(::std::error)pattern used throughout this chapter applies. -
POSIX modes.
process_mode::new_sessioncallssetsid;process_mode::posix_vforkusesvforkand restricts what the child may do beforeexec;process_mode::followcontrols whether a symlink is followed. -
Windows modes.
process_mode::alloc_new_consolegives the child its own console;killterminates it with the given exit code. -
Path spelling. Use
::fast_io::mnp::os_c_strwhen a path comes fromargv, so the native encoding is used on each platform.
Key takeaways
native_processowns a child process, is move-only, and waits in its destructor;wait,detachandkillare the three lifetime decisions.native_process_argsandnative_process_envsaccept printable values and perform the platform string conversion; an empty environment means inherit, a non-empty one replaces it.native_process_ioredirectsin,outanderr;posix_dev_null()discards a stream portably.- Use
iobuf_pipewhen the pipe carries formatted values, andnative_pipewhen it carries raw bytes fortransmit. - Close the parent's copy of every unused write end, or the reader will never see EOF.
- Drain multiple pipes concurrently; sequential drains can deadlock when the child fills the pipe you are not reading.
- A pipe is a byte stream with no message boundaries. Bytes use
transmit; structured data usesprint/scanwith framing.