From b467394705238ea44d8057da08fa28dd6182c90f Mon Sep 17 00:00:00 2001 From: PatternCatalyst Date: Sun, 19 Jul 2026 00:31:19 -0400 Subject: [PATCH] =?UTF-8?q?docs(r05):=20Parts=203-5=20=E2=80=94=20processe?= =?UTF-8?q?s/signals/privilege,=20memory,=20IPC=20(ch=2011-20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pmon v0-v5 (lifecycle, signalfd supervision, pidfd engine, VM capability drop, pipe log pipeline, UDS control plane + SCM_RIGHTS), memmap, shmkv v0-v1 (cross-language on-disk format, futex/seqlock coordination), allocbench. 17 diagram pairs. Adversarial validation: 57/57 matrix PASS incl. VM lane 14 negative control; codetabs verbatim; interop verified. --- _docs/11-process-lifecycle.md | 634 +++++ _docs/12-signals-and-safe-handling.md | 696 +++++ _docs/13-pidfd-modern-process-mgmt.md | 504 ++++ _docs/14-identity-and-privilege.md | 633 ++++ _docs/15-virtual-memory.md | 526 ++++ _docs/16-mmap-and-shared-mappings.md | 496 ++++ _docs/17-allocators-and-gc-runtimes.md | 478 ++++ _docs/18-pipes-fifos-splice.md | 526 ++++ _docs/19-unix-sockets-fd-passing.md | 497 ++++ _docs/20-shared-memory-coordination.md | 532 ++++ .../diagrams/11-fork-exec-wait-seq.excalidraw | 1519 ++++++++++ assets/diagrams/11-fork-exec-wait-seq.svg | 50 + .../11-process-lifecycle-states.excalidraw | 1412 +++++++++ .../diagrams/11-process-lifecycle-states.svg | 50 + .../12-signal-delivery-paths.excalidraw | 2032 +++++++++++++ assets/diagrams/12-signal-delivery-paths.svg | 66 + .../12-supervisor-state-machine.excalidraw | 1138 ++++++++ .../diagrams/12-supervisor-state-machine.svg | 40 + .../13-pid-reuse-race-pidfd.excalidraw | 1924 +++++++++++++ assets/diagrams/13-pid-reuse-race-pidfd.svg | 61 + .../14-credential-sets-caps.excalidraw | 2533 +++++++++++++++++ assets/diagrams/14-credential-sets-caps.svg | 78 + .../diagrams/15-address-space-map.excalidraw | 1499 ++++++++++ assets/diagrams/15-address-space-map.svg | 50 + assets/diagrams/15-fault-flow.excalidraw | 1131 ++++++++ assets/diagrams/15-fault-flow.svg | 36 + .../diagrams/16-mapping-page-cache.excalidraw | 1284 +++++++++ assets/diagrams/16-mapping-page-cache.svg | 45 + assets/diagrams/16-shmkv-layout.excalidraw | 1528 ++++++++++ assets/diagrams/16-shmkv-layout.svg | 49 + assets/diagrams/17-arena-vs-gc.excalidraw | 2114 ++++++++++++++ assets/diagrams/17-arena-vs-gc.svg | 70 + assets/diagrams/18-log-topology.excalidraw | 2205 ++++++++++++++ assets/diagrams/18-log-topology.svg | 75 + assets/diagrams/18-pipe-internals.excalidraw | 1703 +++++++++++ assets/diagrams/18-pipe-internals.svg | 57 + .../19-scm-rights-transfer.excalidraw | 1545 ++++++++++ assets/diagrams/19-scm-rights-transfer.svg | 54 + .../diagrams/19-uds-control-plane.excalidraw | 1917 +++++++++++++ assets/diagrams/19-uds-control-plane.svg | 63 + assets/diagrams/20-futex-wait-wake.excalidraw | 1629 +++++++++++ assets/diagrams/20-futex-wait-wake.svg | 53 + assets/diagrams/20-ipc-matrix.excalidraw | 1669 +++++++++++ assets/diagrams/20-ipc-matrix.svg | 52 + assets/diagrams/README.md | 17 + examples/11-process-lifecycle/README.md | 114 + .../11-process-lifecycle/cpp/.clang-format | 8 + examples/11-process-lifecycle/cpp/.clang-tidy | 11 + .../11-process-lifecycle/cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + examples/11-process-lifecycle/cpp/demo.sh | 61 + .../11-process-lifecycle/cpp/src/main.cpp | 201 ++ examples/11-process-lifecycle/demo.sh | 23 + examples/11-process-lifecycle/go/demo.sh | 44 + examples/11-process-lifecycle/go/go.mod | 5 + examples/11-process-lifecycle/go/main.go | 137 + examples/11-process-lifecycle/rust/Cargo.lock | 46 + examples/11-process-lifecycle/rust/Cargo.toml | 11 + examples/11-process-lifecycle/rust/demo.sh | 47 + .../rust/rust-toolchain.toml | 3 + .../11-process-lifecycle/rust/src/main.rs | 132 + examples/11-process-lifecycle/verify.lua | 119 + .../12-signals-and-safe-handling/README.md | 110 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../12-signals-and-safe-handling/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 366 +++ examples/12-signals-and-safe-handling/demo.sh | 23 + .../12-signals-and-safe-handling/go/demo.sh | 23 + .../12-signals-and-safe-handling/go/go.mod | 7 + .../12-signals-and-safe-handling/go/go.sum | 2 + .../12-signals-and-safe-handling/go/main.go | 248 ++ .../rust/Cargo.lock | 99 + .../rust/Cargo.toml | 13 + .../12-signals-and-safe-handling/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 295 ++ .../12-signals-and-safe-handling/verify.lua | 156 + .../13-pidfd-modern-process-mgmt/README.md | 92 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../13-pidfd-modern-process-mgmt/cpp/demo.sh | 41 + .../cpp/src/main.cpp | 447 +++ examples/13-pidfd-modern-process-mgmt/demo.sh | 23 + .../13-pidfd-modern-process-mgmt/go/demo.sh | 23 + .../13-pidfd-modern-process-mgmt/go/go.mod | 7 + .../13-pidfd-modern-process-mgmt/go/go.sum | 2 + .../13-pidfd-modern-process-mgmt/go/main.go | 335 +++ .../rust/Cargo.lock | 98 + .../rust/Cargo.toml | 13 + .../13-pidfd-modern-process-mgmt/rust/demo.sh | 27 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 347 +++ .../13-pidfd-modern-process-mgmt/verify.lua | 168 ++ examples/14-identity-and-privilege/README.md | 119 + .../cpp/.clang-format | 8 + .../14-identity-and-privilege/cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../14-identity-and-privilege/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 248 ++ examples/14-identity-and-privilege/demo.sh | 23 + examples/14-identity-and-privilege/go/demo.sh | 23 + examples/14-identity-and-privilege/go/go.mod | 7 + examples/14-identity-and-privilege/go/go.sum | 2 + examples/14-identity-and-privilege/go/main.go | 191 ++ .../14-identity-and-privilege/rust/Cargo.lock | 16 + .../14-identity-and-privilege/rust/Cargo.toml | 11 + .../14-identity-and-privilege/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 329 +++ examples/14-identity-and-privilege/verify.lua | 88 + examples/15-virtual-memory/README.md | 108 + examples/15-virtual-memory/cpp/.clang-format | 8 + examples/15-virtual-memory/cpp/.clang-tidy | 11 + examples/15-virtual-memory/cpp/CMakeLists.txt | 8 + .../15-virtual-memory/cpp/CMakePresets.json | 62 + examples/15-virtual-memory/cpp/demo.sh | 40 + examples/15-virtual-memory/cpp/src/main.cpp | 524 ++++ examples/15-virtual-memory/demo.sh | 23 + examples/15-virtual-memory/go/demo.sh | 23 + examples/15-virtual-memory/go/go.mod | 7 + examples/15-virtual-memory/go/go.sum | 2 + examples/15-virtual-memory/go/main.go | 419 +++ examples/15-virtual-memory/rust/Cargo.lock | 74 + examples/15-virtual-memory/rust/Cargo.toml | 13 + examples/15-virtual-memory/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + examples/15-virtual-memory/rust/src/main.rs | 435 +++ examples/15-virtual-memory/verify.lua | 158 + .../16-mmap-and-shared-mappings/README.md | 95 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../16-mmap-and-shared-mappings/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 333 +++ examples/16-mmap-and-shared-mappings/demo.sh | 23 + .../16-mmap-and-shared-mappings/go/demo.sh | 23 + .../16-mmap-and-shared-mappings/go/go.mod | 7 + .../16-mmap-and-shared-mappings/go/go.sum | 2 + .../16-mmap-and-shared-mappings/go/main.go | 329 +++ .../rust/Cargo.lock | 25 + .../rust/Cargo.toml | 11 + .../16-mmap-and-shared-mappings/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 261 ++ .../16-mmap-and-shared-mappings/verify.lua | 189 ++ .../17-allocators-and-gc-runtimes/README.md | 126 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../17-allocators-and-gc-runtimes/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 226 ++ .../17-allocators-and-gc-runtimes/demo.sh | 23 + .../17-allocators-and-gc-runtimes/go/demo.sh | 23 + .../17-allocators-and-gc-runtimes/go/go.mod | 7 + .../17-allocators-and-gc-runtimes/go/go.sum | 2 + .../17-allocators-and-gc-runtimes/go/main.go | 214 ++ .../rust/Cargo.lock | 30 + .../rust/Cargo.toml | 13 + .../rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 195 ++ .../17-allocators-and-gc-runtimes/verify.lua | 105 + examples/18-pipes-fifos-splice/README.md | 128 + .../18-pipes-fifos-splice/cpp/.clang-format | 8 + .../18-pipes-fifos-splice/cpp/.clang-tidy | 11 + .../18-pipes-fifos-splice/cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + examples/18-pipes-fifos-splice/cpp/demo.sh | 40 + .../18-pipes-fifos-splice/cpp/src/main.cpp | 805 ++++++ examples/18-pipes-fifos-splice/demo.sh | 23 + examples/18-pipes-fifos-splice/go/demo.sh | 23 + examples/18-pipes-fifos-splice/go/go.mod | 7 + examples/18-pipes-fifos-splice/go/go.sum | 2 + examples/18-pipes-fifos-splice/go/main.go | 601 ++++ .../18-pipes-fifos-splice/rust/Cargo.lock | 98 + .../18-pipes-fifos-splice/rust/Cargo.toml | 13 + examples/18-pipes-fifos-splice/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../18-pipes-fifos-splice/rust/src/main.rs | 686 +++++ examples/18-pipes-fifos-splice/verify.lua | 178 ++ examples/19-unix-sockets-fd-passing/README.md | 90 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../19-unix-sockets-fd-passing/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 588 ++++ examples/19-unix-sockets-fd-passing/demo.sh | 23 + .../19-unix-sockets-fd-passing/go/demo.sh | 23 + examples/19-unix-sockets-fd-passing/go/go.mod | 7 + examples/19-unix-sockets-fd-passing/go/go.sum | 2 + .../19-unix-sockets-fd-passing/go/main.go | 403 +++ .../rust/Cargo.lock | 69 + .../rust/Cargo.toml | 12 + .../19-unix-sockets-fd-passing/rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 420 +++ .../19-unix-sockets-fd-passing/verify.lua | 167 ++ .../20-shared-memory-coordination/README.md | 116 + .../cpp/.clang-format | 8 + .../cpp/.clang-tidy | 11 + .../cpp/CMakeLists.txt | 8 + .../cpp/CMakePresets.json | 62 + .../20-shared-memory-coordination/cpp/demo.sh | 40 + .../cpp/src/main.cpp | 528 ++++ .../20-shared-memory-coordination/demo.sh | 23 + .../20-shared-memory-coordination/go/demo.sh | 23 + .../20-shared-memory-coordination/go/go.mod | 7 + .../20-shared-memory-coordination/go/go.sum | 2 + .../20-shared-memory-coordination/go/main.go | 513 ++++ .../rust/Cargo.lock | 74 + .../rust/Cargo.toml | 13 + .../rust/demo.sh | 26 + .../rust/rust-toolchain.toml | 3 + .../rust/src/main.rs | 513 ++++ .../20-shared-memory-coordination/verify.lua | 163 ++ examples/manifest.yaml | 40 + 225 files changed, 52111 insertions(+) create mode 100644 _docs/11-process-lifecycle.md create mode 100644 _docs/12-signals-and-safe-handling.md create mode 100644 _docs/13-pidfd-modern-process-mgmt.md create mode 100644 _docs/14-identity-and-privilege.md create mode 100644 _docs/15-virtual-memory.md create mode 100644 _docs/16-mmap-and-shared-mappings.md create mode 100644 _docs/17-allocators-and-gc-runtimes.md create mode 100644 _docs/18-pipes-fifos-splice.md create mode 100644 _docs/19-unix-sockets-fd-passing.md create mode 100644 _docs/20-shared-memory-coordination.md create mode 100644 assets/diagrams/11-fork-exec-wait-seq.excalidraw create mode 100644 assets/diagrams/11-fork-exec-wait-seq.svg create mode 100644 assets/diagrams/11-process-lifecycle-states.excalidraw create mode 100644 assets/diagrams/11-process-lifecycle-states.svg create mode 100644 assets/diagrams/12-signal-delivery-paths.excalidraw create mode 100644 assets/diagrams/12-signal-delivery-paths.svg create mode 100644 assets/diagrams/12-supervisor-state-machine.excalidraw create mode 100644 assets/diagrams/12-supervisor-state-machine.svg create mode 100644 assets/diagrams/13-pid-reuse-race-pidfd.excalidraw create mode 100644 assets/diagrams/13-pid-reuse-race-pidfd.svg create mode 100644 assets/diagrams/14-credential-sets-caps.excalidraw create mode 100644 assets/diagrams/14-credential-sets-caps.svg create mode 100644 assets/diagrams/15-address-space-map.excalidraw create mode 100644 assets/diagrams/15-address-space-map.svg create mode 100644 assets/diagrams/15-fault-flow.excalidraw create mode 100644 assets/diagrams/15-fault-flow.svg create mode 100644 assets/diagrams/16-mapping-page-cache.excalidraw create mode 100644 assets/diagrams/16-mapping-page-cache.svg create mode 100644 assets/diagrams/16-shmkv-layout.excalidraw create mode 100644 assets/diagrams/16-shmkv-layout.svg create mode 100644 assets/diagrams/17-arena-vs-gc.excalidraw create mode 100644 assets/diagrams/17-arena-vs-gc.svg create mode 100644 assets/diagrams/18-log-topology.excalidraw create mode 100644 assets/diagrams/18-log-topology.svg create mode 100644 assets/diagrams/18-pipe-internals.excalidraw create mode 100644 assets/diagrams/18-pipe-internals.svg create mode 100644 assets/diagrams/19-scm-rights-transfer.excalidraw create mode 100644 assets/diagrams/19-scm-rights-transfer.svg create mode 100644 assets/diagrams/19-uds-control-plane.excalidraw create mode 100644 assets/diagrams/19-uds-control-plane.svg create mode 100644 assets/diagrams/20-futex-wait-wake.excalidraw create mode 100644 assets/diagrams/20-futex-wait-wake.svg create mode 100644 assets/diagrams/20-ipc-matrix.excalidraw create mode 100644 assets/diagrams/20-ipc-matrix.svg create mode 100644 examples/11-process-lifecycle/README.md create mode 100644 examples/11-process-lifecycle/cpp/.clang-format create mode 100644 examples/11-process-lifecycle/cpp/.clang-tidy create mode 100644 examples/11-process-lifecycle/cpp/CMakeLists.txt create mode 100644 examples/11-process-lifecycle/cpp/CMakePresets.json create mode 100755 examples/11-process-lifecycle/cpp/demo.sh create mode 100644 examples/11-process-lifecycle/cpp/src/main.cpp create mode 100755 examples/11-process-lifecycle/demo.sh create mode 100755 examples/11-process-lifecycle/go/demo.sh create mode 100644 examples/11-process-lifecycle/go/go.mod create mode 100644 examples/11-process-lifecycle/go/main.go create mode 100644 examples/11-process-lifecycle/rust/Cargo.lock create mode 100644 examples/11-process-lifecycle/rust/Cargo.toml create mode 100755 examples/11-process-lifecycle/rust/demo.sh create mode 100644 examples/11-process-lifecycle/rust/rust-toolchain.toml create mode 100644 examples/11-process-lifecycle/rust/src/main.rs create mode 100644 examples/11-process-lifecycle/verify.lua create mode 100644 examples/12-signals-and-safe-handling/README.md create mode 100644 examples/12-signals-and-safe-handling/cpp/.clang-format create mode 100644 examples/12-signals-and-safe-handling/cpp/.clang-tidy create mode 100644 examples/12-signals-and-safe-handling/cpp/CMakeLists.txt create mode 100644 examples/12-signals-and-safe-handling/cpp/CMakePresets.json create mode 100755 examples/12-signals-and-safe-handling/cpp/demo.sh create mode 100644 examples/12-signals-and-safe-handling/cpp/src/main.cpp create mode 100755 examples/12-signals-and-safe-handling/demo.sh create mode 100755 examples/12-signals-and-safe-handling/go/demo.sh create mode 100644 examples/12-signals-and-safe-handling/go/go.mod create mode 100644 examples/12-signals-and-safe-handling/go/go.sum create mode 100644 examples/12-signals-and-safe-handling/go/main.go create mode 100644 examples/12-signals-and-safe-handling/rust/Cargo.lock create mode 100644 examples/12-signals-and-safe-handling/rust/Cargo.toml create mode 100755 examples/12-signals-and-safe-handling/rust/demo.sh create mode 100644 examples/12-signals-and-safe-handling/rust/rust-toolchain.toml create mode 100644 examples/12-signals-and-safe-handling/rust/src/main.rs create mode 100644 examples/12-signals-and-safe-handling/verify.lua create mode 100644 examples/13-pidfd-modern-process-mgmt/README.md create mode 100644 examples/13-pidfd-modern-process-mgmt/cpp/.clang-format create mode 100644 examples/13-pidfd-modern-process-mgmt/cpp/.clang-tidy create mode 100644 examples/13-pidfd-modern-process-mgmt/cpp/CMakeLists.txt create mode 100644 examples/13-pidfd-modern-process-mgmt/cpp/CMakePresets.json create mode 100755 examples/13-pidfd-modern-process-mgmt/cpp/demo.sh create mode 100644 examples/13-pidfd-modern-process-mgmt/cpp/src/main.cpp create mode 100755 examples/13-pidfd-modern-process-mgmt/demo.sh create mode 100755 examples/13-pidfd-modern-process-mgmt/go/demo.sh create mode 100644 examples/13-pidfd-modern-process-mgmt/go/go.mod create mode 100644 examples/13-pidfd-modern-process-mgmt/go/go.sum create mode 100644 examples/13-pidfd-modern-process-mgmt/go/main.go create mode 100644 examples/13-pidfd-modern-process-mgmt/rust/Cargo.lock create mode 100644 examples/13-pidfd-modern-process-mgmt/rust/Cargo.toml create mode 100755 examples/13-pidfd-modern-process-mgmt/rust/demo.sh create mode 100644 examples/13-pidfd-modern-process-mgmt/rust/rust-toolchain.toml create mode 100644 examples/13-pidfd-modern-process-mgmt/rust/src/main.rs create mode 100644 examples/13-pidfd-modern-process-mgmt/verify.lua create mode 100644 examples/14-identity-and-privilege/README.md create mode 100644 examples/14-identity-and-privilege/cpp/.clang-format create mode 100644 examples/14-identity-and-privilege/cpp/.clang-tidy create mode 100644 examples/14-identity-and-privilege/cpp/CMakeLists.txt create mode 100644 examples/14-identity-and-privilege/cpp/CMakePresets.json create mode 100755 examples/14-identity-and-privilege/cpp/demo.sh create mode 100644 examples/14-identity-and-privilege/cpp/src/main.cpp create mode 100755 examples/14-identity-and-privilege/demo.sh create mode 100755 examples/14-identity-and-privilege/go/demo.sh create mode 100644 examples/14-identity-and-privilege/go/go.mod create mode 100644 examples/14-identity-and-privilege/go/go.sum create mode 100644 examples/14-identity-and-privilege/go/main.go create mode 100644 examples/14-identity-and-privilege/rust/Cargo.lock create mode 100644 examples/14-identity-and-privilege/rust/Cargo.toml create mode 100755 examples/14-identity-and-privilege/rust/demo.sh create mode 100644 examples/14-identity-and-privilege/rust/rust-toolchain.toml create mode 100644 examples/14-identity-and-privilege/rust/src/main.rs create mode 100644 examples/14-identity-and-privilege/verify.lua create mode 100644 examples/15-virtual-memory/README.md create mode 100644 examples/15-virtual-memory/cpp/.clang-format create mode 100644 examples/15-virtual-memory/cpp/.clang-tidy create mode 100644 examples/15-virtual-memory/cpp/CMakeLists.txt create mode 100644 examples/15-virtual-memory/cpp/CMakePresets.json create mode 100755 examples/15-virtual-memory/cpp/demo.sh create mode 100644 examples/15-virtual-memory/cpp/src/main.cpp create mode 100755 examples/15-virtual-memory/demo.sh create mode 100755 examples/15-virtual-memory/go/demo.sh create mode 100644 examples/15-virtual-memory/go/go.mod create mode 100644 examples/15-virtual-memory/go/go.sum create mode 100644 examples/15-virtual-memory/go/main.go create mode 100644 examples/15-virtual-memory/rust/Cargo.lock create mode 100644 examples/15-virtual-memory/rust/Cargo.toml create mode 100755 examples/15-virtual-memory/rust/demo.sh create mode 100644 examples/15-virtual-memory/rust/rust-toolchain.toml create mode 100644 examples/15-virtual-memory/rust/src/main.rs create mode 100644 examples/15-virtual-memory/verify.lua create mode 100644 examples/16-mmap-and-shared-mappings/README.md create mode 100644 examples/16-mmap-and-shared-mappings/cpp/.clang-format create mode 100644 examples/16-mmap-and-shared-mappings/cpp/.clang-tidy create mode 100644 examples/16-mmap-and-shared-mappings/cpp/CMakeLists.txt create mode 100644 examples/16-mmap-and-shared-mappings/cpp/CMakePresets.json create mode 100755 examples/16-mmap-and-shared-mappings/cpp/demo.sh create mode 100644 examples/16-mmap-and-shared-mappings/cpp/src/main.cpp create mode 100755 examples/16-mmap-and-shared-mappings/demo.sh create mode 100755 examples/16-mmap-and-shared-mappings/go/demo.sh create mode 100644 examples/16-mmap-and-shared-mappings/go/go.mod create mode 100644 examples/16-mmap-and-shared-mappings/go/go.sum create mode 100644 examples/16-mmap-and-shared-mappings/go/main.go create mode 100644 examples/16-mmap-and-shared-mappings/rust/Cargo.lock create mode 100644 examples/16-mmap-and-shared-mappings/rust/Cargo.toml create mode 100755 examples/16-mmap-and-shared-mappings/rust/demo.sh create mode 100644 examples/16-mmap-and-shared-mappings/rust/rust-toolchain.toml create mode 100644 examples/16-mmap-and-shared-mappings/rust/src/main.rs create mode 100644 examples/16-mmap-and-shared-mappings/verify.lua create mode 100644 examples/17-allocators-and-gc-runtimes/README.md create mode 100644 examples/17-allocators-and-gc-runtimes/cpp/.clang-format create mode 100644 examples/17-allocators-and-gc-runtimes/cpp/.clang-tidy create mode 100644 examples/17-allocators-and-gc-runtimes/cpp/CMakeLists.txt create mode 100644 examples/17-allocators-and-gc-runtimes/cpp/CMakePresets.json create mode 100755 examples/17-allocators-and-gc-runtimes/cpp/demo.sh create mode 100644 examples/17-allocators-and-gc-runtimes/cpp/src/main.cpp create mode 100755 examples/17-allocators-and-gc-runtimes/demo.sh create mode 100755 examples/17-allocators-and-gc-runtimes/go/demo.sh create mode 100644 examples/17-allocators-and-gc-runtimes/go/go.mod create mode 100644 examples/17-allocators-and-gc-runtimes/go/go.sum create mode 100644 examples/17-allocators-and-gc-runtimes/go/main.go create mode 100644 examples/17-allocators-and-gc-runtimes/rust/Cargo.lock create mode 100644 examples/17-allocators-and-gc-runtimes/rust/Cargo.toml create mode 100755 examples/17-allocators-and-gc-runtimes/rust/demo.sh create mode 100644 examples/17-allocators-and-gc-runtimes/rust/rust-toolchain.toml create mode 100644 examples/17-allocators-and-gc-runtimes/rust/src/main.rs create mode 100644 examples/17-allocators-and-gc-runtimes/verify.lua create mode 100644 examples/18-pipes-fifos-splice/README.md create mode 100644 examples/18-pipes-fifos-splice/cpp/.clang-format create mode 100644 examples/18-pipes-fifos-splice/cpp/.clang-tidy create mode 100644 examples/18-pipes-fifos-splice/cpp/CMakeLists.txt create mode 100644 examples/18-pipes-fifos-splice/cpp/CMakePresets.json create mode 100755 examples/18-pipes-fifos-splice/cpp/demo.sh create mode 100644 examples/18-pipes-fifos-splice/cpp/src/main.cpp create mode 100755 examples/18-pipes-fifos-splice/demo.sh create mode 100755 examples/18-pipes-fifos-splice/go/demo.sh create mode 100644 examples/18-pipes-fifos-splice/go/go.mod create mode 100644 examples/18-pipes-fifos-splice/go/go.sum create mode 100644 examples/18-pipes-fifos-splice/go/main.go create mode 100644 examples/18-pipes-fifos-splice/rust/Cargo.lock create mode 100644 examples/18-pipes-fifos-splice/rust/Cargo.toml create mode 100755 examples/18-pipes-fifos-splice/rust/demo.sh create mode 100644 examples/18-pipes-fifos-splice/rust/rust-toolchain.toml create mode 100644 examples/18-pipes-fifos-splice/rust/src/main.rs create mode 100644 examples/18-pipes-fifos-splice/verify.lua create mode 100644 examples/19-unix-sockets-fd-passing/README.md create mode 100644 examples/19-unix-sockets-fd-passing/cpp/.clang-format create mode 100644 examples/19-unix-sockets-fd-passing/cpp/.clang-tidy create mode 100644 examples/19-unix-sockets-fd-passing/cpp/CMakeLists.txt create mode 100644 examples/19-unix-sockets-fd-passing/cpp/CMakePresets.json create mode 100755 examples/19-unix-sockets-fd-passing/cpp/demo.sh create mode 100644 examples/19-unix-sockets-fd-passing/cpp/src/main.cpp create mode 100755 examples/19-unix-sockets-fd-passing/demo.sh create mode 100755 examples/19-unix-sockets-fd-passing/go/demo.sh create mode 100644 examples/19-unix-sockets-fd-passing/go/go.mod create mode 100644 examples/19-unix-sockets-fd-passing/go/go.sum create mode 100644 examples/19-unix-sockets-fd-passing/go/main.go create mode 100644 examples/19-unix-sockets-fd-passing/rust/Cargo.lock create mode 100644 examples/19-unix-sockets-fd-passing/rust/Cargo.toml create mode 100755 examples/19-unix-sockets-fd-passing/rust/demo.sh create mode 100644 examples/19-unix-sockets-fd-passing/rust/rust-toolchain.toml create mode 100644 examples/19-unix-sockets-fd-passing/rust/src/main.rs create mode 100644 examples/19-unix-sockets-fd-passing/verify.lua create mode 100644 examples/20-shared-memory-coordination/README.md create mode 100644 examples/20-shared-memory-coordination/cpp/.clang-format create mode 100644 examples/20-shared-memory-coordination/cpp/.clang-tidy create mode 100644 examples/20-shared-memory-coordination/cpp/CMakeLists.txt create mode 100644 examples/20-shared-memory-coordination/cpp/CMakePresets.json create mode 100755 examples/20-shared-memory-coordination/cpp/demo.sh create mode 100644 examples/20-shared-memory-coordination/cpp/src/main.cpp create mode 100755 examples/20-shared-memory-coordination/demo.sh create mode 100755 examples/20-shared-memory-coordination/go/demo.sh create mode 100644 examples/20-shared-memory-coordination/go/go.mod create mode 100644 examples/20-shared-memory-coordination/go/go.sum create mode 100644 examples/20-shared-memory-coordination/go/main.go create mode 100644 examples/20-shared-memory-coordination/rust/Cargo.lock create mode 100644 examples/20-shared-memory-coordination/rust/Cargo.toml create mode 100755 examples/20-shared-memory-coordination/rust/demo.sh create mode 100644 examples/20-shared-memory-coordination/rust/rust-toolchain.toml create mode 100644 examples/20-shared-memory-coordination/rust/src/main.rs create mode 100644 examples/20-shared-memory-coordination/verify.lua diff --git a/_docs/11-process-lifecycle.md b/_docs/11-process-lifecycle.md new file mode 100644 index 0000000..66df347 --- /dev/null +++ b/_docs/11-process-lifecycle.md @@ -0,0 +1,634 @@ +--- +title: "The process lifecycle: fork, exec, wait" +order: 11 +part: "Processes, Signals, Privilege" +description: "fork, exec, and wait walked end to end as pmon v0 spawns and reaps its first children: copy-on-write, a real zombie caught live in ps, orphans reparented to a subreaper, exit codes vs termination signals, rusage accounting — and the clone flavors os/exec and Command actually issue, read from strace." +duration: "45 minutes" +--- + +This chapter opens the book's third part with a new long-running artifact: +`pmon`, a process monitor that will grow signal forwarding, pidfd-based +supervision, and privilege dropping over the next three chapters. Version 0 +does the irreducible job of every supervisor — spawn a command, wait for it, +report how it died and what it cost — because everything else in this part +builds on one idea: **a process's death is data, and someone must collect +it**. Between a child's last instruction and the parent's `wait`, the kernel +holds the corpse — exit status, resource usage, a pid that cannot be reused — +as a *zombie*, and in this chapter you will create one deliberately, watch it +in `ps`, read its `/proc` entry field by field, and then reap it. Along the +way we take apart what `fork(2)` really copies (almost nothing, thanks to +copy-on-write), why exec failure needs a back channel, and what Go's +`os/exec` and Rust's `Command` are actually doing under their tidy APIs — the +answer, straight from `strace`, involves three different flavors of `clone`. + +The code is in `examples/11-process-lifecycle/`. `./demo.sh` there builds all +three implementations and walks the four child fates — clean exit, `exit 42`, +death by SIGTERM, exec failure — and its `README.md` pins the output contract +and exit-code mirroring all three languages share. + +{% include excalidraw.html + file="11-process-lifecycle-states" + alt="Five process states drawn as boxes: fork/clone3 leads into Running R inside an alive band that also holds Sleeping S and Stopped T with SIGSTOP and SIGCONT arrows between them; an amber exit arrow crosses into an after-exit band holding Zombie Z, annotated defunct in ps f and exit status plus rusage only, with a final amber wait4/waitid arrow down to a dashed reaped box where the pid table slot is free." + caption="Figure 11.1 — the lifecycle as ps STAT letters; the kernel keeps a zombie until someone reaps it, and every annotation in the zombie box is observed later in this chapter" %} + +> **Tools used** — `strace`, `ps`, `kill`, `sh`, `sleep`, `cat` (host); +> `execsnoop`/`exitsnoop` (lab VM, exercised in Part 8). Everything here is +> checked by `scripts/check-host.sh`, ships with Fedora, or is preinstalled +> in the lab VMs. + +## Born twice: fork, then exec + +Unix splits "run a program" into two syscalls with nothing in between but +your own code. `fork(2)` duplicates the calling process — same program, same +open fds, a new pid — and *returns twice*: the child sees `0`, the parent +sees the child's pid. `execve(2)` then replaces the calling process's program +image with a new one, keeping the pid and the fd table. The gap between them +is the whole point: the child can rearrange fds, drop privileges, or change +directory *as ordinary code running in the child*, before the new program +exists. Every shell redirection you have ever typed is three lines of code in +that gap. + +The apparent extravagance of fork — duplicate an entire address space just to +throw it away at exec — is paid for by **copy-on-write**. The kernel copies +page *tables*, not pages: parent and child share every physical page, +read-only, and only a write by either side triggers a fault that copies the +one page written. A fork of a 100 MB process touches kilobytes. You can see +the accounting in this chapter's own evidence: the zombie's +`/proc//stat` below records `minflt` of 103 — one hundred and three +minor faults is the entire page-level cost of forking pmon and execing +`sleep`. The COW story has a sharper corollary for the Go and Rust runtimes, +visible in the cross-check: when the child is going to exec *immediately*, +even copying page tables is waste, so both runtimes ask for +`CLONE_VM|CLONE_VFORK` — share the address space outright and suspend the +parent until the exec — which is fork with the copy amortized to zero. + +{% include excalidraw.html + file="11-fork-exec-wait-seq" + alt="Two lanes, parent pmon and child. Parent creates pipe2 O_CLOEXEC, calls fork drawn as clone SIGCHLD with a COW note, and an arrow labeled returns 0 in the child crosses to the child lane where execvp runs; a dashed arrow back carries EOF on success or errno then exit 127. The child box CMD runs then exits or is killed leads by an amber arrow labeled zombie until reaped, SIGCHLD to the parent's waitpid/wait4 box, then down to report and mirror the fate, status s exits s and signal n exits 128 plus n." + caption="Figure 11.2 — pmon's spine: fork → exec → wait, with the CLOEXEC self-pipe carrying the exec verdict back to the parent" %} + +One hole remains in the two-step model: if `execvp` itself fails — the +command does not exist — the failure happens *in the child*, after fork, and +the parent has no direct way to distinguish "exec failed" from "the program +ran and exited". pmon closes the hole with a **self-pipe**, and chapter 7's +`O_CLOEXEC` discipline is what makes it work. Both pipe ends are opened +close-on-exec, from `examples/11-process-lifecycle/cpp/src/main.cpp`: + +```cpp +// Self-pipe for exec-failure reporting: both ends O_CLOEXEC, so a successful +// exec closes the child's copy and the parent reads EOF. +struct ExecPipe { + Fd read_end; + Fd write_end; +}; + +[[nodiscard]] std::expected make_exec_pipe() { + std::array fds{}; + if (::pipe2(fds.data(), O_CLOEXEC) != 0) { + return std::unexpected(last_error()); + } + return ExecPipe{Fd{fds[0]}, Fd{fds[1]}}; +} +``` + +If exec succeeds, the kernel closes the child's write end atomically at exec +time and the parent's `read` returns 0 — EOF, no child code required. If exec +fails, the child writes the raw `errno` into the pipe and `_exit(127)`s. The +parent reads either nothing or four bytes that name the exact failure. This +trick is not pedagogy: the cross-check shows Go and Rust doing precisely the +same dance inside their standard libraries. + +## Zombies, orphans, and the duty to reap + +When a process exits, the kernel cannot simply delete it: the parent may not +have asked yet how it died, and the pid must not be recycled while an answer +is owed. So the address space, fds, and threads are freed immediately, but +the task record — pid, exit status, rusage — stays as a **zombie** (`Z` in +`ps`, rendered ``) until the parent calls `wait4(2)`/`waitid(2)`. +Reaping *is* that call: the kernel copies the status and rusage out and only +then frees the pid. A parent that spawns children and never waits leaks pids +— a real resource, bounded by `pid_max` — which is why every supervisor loop +in this book pairs each spawn with exactly one reap. + +Orphans travel the other direction: if the *parent* dies first, the child is +not left parentless — the kernel reparents it up the tree to the nearest +**subreaper** (`PR_SET_CHILD_SUBREAPER`, a Part 3 recurring guest) or to pid +1. On this Fedora host the reparent target is not init itself: orphaning a +`sleep` by letting its shell parent exit immediately re-homed it to the user's +`systemd --user` instance, pid 6145 — session managers register as subreapers +precisely so they, not pid 1, inherit the duty to reap. Reparenting is also +why zombies are curable by killing the negligent parent: its zombies get +reparented, and subreapers wait promptly. + +## Exit codes vs termination signals + +"How did it die" is one `int` with two disjoint answers. A process that +called `exit(s)` has `WIFEXITED(status)` true and `WEXITSTATUS(status)` +yields the low 8 bits of `s`. A process killed by a signal never reached +`exit` at all: `WIFSIGNALED(status)` is true and `WTERMSIG(status)` names the +signal. They are different fields of the wait status, and conflating them +loses information — "exited 15" and "killed by SIGTERM" are entirely +different deaths. The shell flattens the pair into one byte with the `128+n` +convention, and pmon mirrors it exactly so it composes with `&&`, `make`, and +CI: status `s` becomes exit `s`, signal `n` becomes exit `128+n` (SIGTERM → +143, SIGKILL → 137), exec failure is the shell's classic `127`, and usage +errors are `2`. Those numbers are asserted, not aspirational: `verify.lua` +checks all of them against live runs, 27 checks per language. + +## How the code works + +The C++ version spells the lifecycle out syscall by syscall; Go and Rust +lean on their runtimes for the spawn and reclaim the low-level facts on the +wait side. Here is the spine, three ways — C++ from `fork` to the mirrored +exit, and the equivalent `monitor` function in Go and Rust: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp + const auto start = std::chrono::steady_clock::now(); + const pid_t pid = ::fork(); + if (pid < 0) { + std::println(stderr, "pmon: fork: {}", last_error().message()); + return 1; + } + + if (pid == 0) { + // Child. On exec success the O_CLOEXEC pipe closes itself; on failure + // ship errno to the parent and die with the shell's 127 convention. + pipe->read_end.reset(); + ::execvp(child_argv[0], child_argv); + const int exec_errno = errno; + (void)::write(pipe->write_end.get(), &exec_errno, sizeof exec_errno); + _exit(127); + } + + // Parent: drop our write end so read() returns the moment exec resolves. + pipe->write_end.reset(); + int exec_errno = 0; + ssize_t nread = 0; + do { + nread = ::read(pipe->read_end.get(), &exec_errno, sizeof exec_errno); + } while (nread < 0 && errno == EINTR); + + const auto status = wait_child(pid); + const auto wall = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start); + if (!status) { + std::println(stderr, "pmon: waitpid: {}", status.error().message()); + return 1; + } + + if (nread == static_cast(sizeof exec_errno)) { + const std::error_code ec{exec_errno, std::system_category()}; + std::println(stderr, "pmon: exec {}: {}", child_argv[0], ec.message()); + return 127; + } + + const auto ru = children_rusage(); + if (!ru) { + std::println(stderr, "pmon: getrusage: {}", ru.error().message()); + return 1; + } + + int code = 1; + if (WIFEXITED(*status)) { + code = WEXITSTATUS(*status); + std::println("pmon: pid {} exited status {}", pid, code); + } else if (WIFSIGNALED(*status)) { + const int sig = WTERMSIG(*status); + code = 128 + sig; + std::println("pmon: pid {} killed by signal {} ({})", pid, sig, signal_name(sig)); + } else { + std::println(stderr, "pmon: unexpected wait status {:#x}", *status); + return 1; + } + + std::println("pmon: rusage maxrss={}KB user={} sys={} wall={}ms", ru->ru_maxrss, + format_cpu(ru->ru_utime), format_cpu(ru->ru_stime), wall.count()); + return code; +} +``` + +```go +func monitor(name string, args []string) int { + cmd := exec.Command(name, args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + + start := time.Now() + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "pmon: exec %s: %s\n", name, reason(err)) + return 127 + } + state, err := waitChild(cmd) + wall := time.Since(start) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + + ws, ok := state.Sys().(syscall.WaitStatus) + if !ok { + fmt.Fprintln(os.Stderr, "pmon: no wait status for child") + return 1 + } + ru, ok := state.SysUsage().(*syscall.Rusage) + if !ok { + fmt.Fprintln(os.Stderr, "pmon: no rusage for child") + return 1 + } + + var code int + switch { + case ws.Exited(): + code = ws.ExitStatus() + fmt.Printf("pmon: pid %d exited status %d\n", state.Pid(), code) + case ws.Signaled(): + sig := int(ws.Signal()) + code = 128 + sig + fmt.Printf("pmon: pid %d killed by signal %d (%s)\n", state.Pid(), sig, sigName(sig)) + default: + fmt.Fprintf(os.Stderr, "pmon: unexpected wait status %#x\n", uint32(ws)) + return 1 + } + + fmt.Printf("pmon: rusage maxrss=%dKB user=%s sys=%s wall=%dms\n", + ru.Maxrss, formatCPU(ru.Utime), formatCPU(ru.Stime), wall.Milliseconds()) + return code +} +``` + +```rust +fn monitor(name: &str, args: &[String]) -> i32 { + let start = Instant::now(); + let child = match Command::new(name).args(args).spawn() { + Ok(child) => child, + Err(err) => { + eprintln!("pmon: exec {name}: {}", reason(&err)); + return 127; + } + }; + let pid = child.id() as i32; + + let (status, rusage) = match wait4(pid) { + Ok(pair) => pair, + Err(err) => { + eprintln!("pmon: wait4: {err}"); + return 1; + } + }; + let wall = start.elapsed().as_millis(); + + let code = match status { + WaitStatus::Exited(_, code) => { + println!("pmon: pid {pid} exited status {code}"); + code + } + WaitStatus::Signaled(_, signal, _) => { + let n = signal as i32; + println!("pmon: pid {pid} killed by signal {n} ({})", sig_name(n)); + 128 + n + } + other => { + eprintln!("pmon: unexpected wait status: {other:?}"); + return 1; + } + }; + + println!( + "pmon: rusage maxrss={}KB user={} sys={} wall={wall}ms", + rusage.ru_maxrss, + format_cpu(rusage.ru_utime), + format_cpu(rusage.ru_stime), + ); + code +} +``` + +Walk the C++ tab first, because everything else is a compressed version of +it. `steady_clock::now()` is taken *before* the fork so wall time covers the +child's whole life, and steady (not system) time so an NTP step cannot make a +child appear to run backwards. `fork` splits the world; the child branch +resets its copy of the read end (chapter 7's move-only `Fd` makes "close +exactly once" a type property, and `ExecPipe` is just two of them), calls +`execvp` — the `p` variant walks `PATH` like a shell — and, only if exec +*returned*, ships `errno` up the pipe and dies with `_exit(127)`. `_exit`, +not `exit`: the child still shares the parent's stdio buffers, and running +the parent's atexit handlers or flushing its buffered output from the child +would emit duplicate bytes. The parent drops its write end — otherwise its +own open fd would hold the pipe open and `read` would block forever, a +self-deadlock — then reads the verdict, retrying on `EINTR`. `wait_child` +loops `waitpid` under the same `EINTR` discipline; the reap must survive a +stray signal. Only then does the status get decoded through the +`WIFEXITED`/`WIFSIGNALED` pair, and the rusage line printed from +`getrusage(RUSAGE_CHILDREN)` — the accumulated cost of all waited-for +children, which equals this child's cost because there is exactly one. + +The Go tab hides the fork behind `cmd.Start`, but nothing else changes +shape. `cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, ...` inherits stdio so +the child's output interleaves naturally — the pid-identity check in +`verify.lua` depends on it, the child echoing `$$` through the shared +descriptor. A `Start` error *is* the self-pipe verdict, surfaced as a value. +`waitChild` needs one subtlety: `cmd.Wait` returns an error for a child that +ran and failed, but an `exec.ExitError` still carries the `ProcessState` we +want — a child exiting 42 is pmon's *success path* — so only non-`ExitError` +failures propagate. The wait-side facts come back out of the portable +wrapper by type assertion: `state.Sys().(syscall.WaitStatus)` is the raw +status int with the same `Exited()`/`Signaled()` split, and +`state.SysUsage().(*syscall.Rusage)` is the per-child rusage the kernel +filled in at reap time. + +Rust's `Command::spawn` similarly returns `Err` on exec failure (same +status-pipe trick internally), but the wait side goes straight to the +syscall, because `wait4(2)` answers both questions in one call — status +*and* rusage for exactly this child: + +```rust +/// wait4(2): reap `pid`, yielding the decoded wait status and its rusage. +fn wait4(pid: i32) -> io::Result<(WaitStatus, libc::rusage)> { + let mut raw_status = 0i32; + // SAFETY: zero-filled rusage is a valid out-param for wait4(2). + let mut rusage: libc::rusage = unsafe { std::mem::zeroed() }; + loop { + // SAFETY: raw_status and rusage are exclusively borrowed, valid + // out-pointers for the duration of the call. + let rc = unsafe { libc::wait4(pid, &mut raw_status, 0, &mut rusage) }; + if rc == pid { + let status = WaitStatus::from_raw(Pid::from_raw(pid), raw_status) + .map_err(|errno| io::Error::from_raw_os_error(errno as i32))?; + return Ok((status, rusage)); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::EINTR) { + return Err(err); + } + } +} +``` + +The two `SAFETY` comments are the whole cost of dropping below +`std::process`: a zeroed `rusage` is a valid out-parameter, and the two +`&mut`s are exclusive for the duration of the call. `nix` decodes the raw +status into a typed `WaitStatus`, giving the `match` in `monitor` the same +two-armed shape as `WIFEXITED`/`WIFSIGNALED`. Note the deliberate asymmetry +with C++: `RUSAGE_CHILDREN` aggregates every reaped child, `wait4` meters +one — identical numbers here, different interfaces, and the distinction will +matter the moment pmon supervises more than one process. + +Two shared fragile bits, stated plainly. The signal names in +`killed by signal 15 (TERM)` come from a hand-rolled 32-entry table, +identical in all three sources — `strsignal(3)` returns locale-dependent +prose ("Terminated") and Go and Rust each have their own spellings, so the +table is the only way three runtimes print one string. And the rusage line's +`%03d` milliseconds formatting is part of the output contract: +`user=0.000s`, not `user=0.0s`, so `verify.lua` can match the shape exactly. + +## Errors, three ways + +The contract for a failed spawn is byte-identical output — `pmon: exec +: No such file or directory` on stderr, exit 127, and *no* fate or +rusage lines, because no child ran. Each language must dig the errno out of +different wreckage. C++ owns the errno directly: the child ships the raw +`int` over the pipe and the parent formats it through +`std::error_code::message()`. Rust gets an `io::Error` from `spawn` and +recovers `raw_os_error()`, rendering it with `nix`'s `Errno::desc` — the +classic `strerror(3)` text without `io::Error`'s "(os error 2)" suffix. Go +is the interesting one, from `examples/11-process-lifecycle/go/main.go`: + +```go +// reason renders a spawn failure in strerror(3) shape. Go's errno strings are +// lowercase ("no such file or directory"), so the first letter is capitalized +// to match what the C++ and Rust versions print; a PATH miss (exec.ErrNotFound) +// is what execvp(3) reports as ENOENT. +func reason(err error) string { + var errno syscall.Errno + if errors.As(err, &errno) { + return capitalize(errno.Error()) + } + if errors.Is(err, exec.ErrNotFound) { + return "No such file or directory" + } + return capitalize(err.Error()) +} +``` + +`errors.As` unwraps to the underlying `syscall.Errno` when there is one, but +Go renders errno strings lowercase, so the first rune is capitalized to +match the C and Rust spellings; and a `PATH` miss surfaces as +`exec.ErrNotFound` rather than an errno at all, which `reason` maps back to +the `ENOENT` text `execvp(3)` reports in the other two. The parity is real: +all three binaries, given `nonexistent-cmd-xyz`, printed the identical line +and exited 127 in this session's runs. + +## Concurrency lens + +`fork` and threads are famously hostile: the child inherits *only the +calling thread*, so any lock another thread held at fork time stays locked +forever in the child — which is why the only safe thing a multithreaded +process can do between fork and exec is call async-signal-safe functions. +pmon's C++ version can fork casually only because it is single-threaded. Go +cannot make that promise — the runtime is multithreaded before `main` runs — +so `os/exec` confines the child to a tiny fixed path of raw syscalls, and +the strace shows the runtime spawning its scheduler threads +(`CLONE_THREAD` clones) before the one process-creating +`clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD)`. That `CLONE_PIDFD` flag +is this part's chapter 13 arriving early: Go receives a *pidfd* for the +child and waits via `waitid(P_PIDFD, ...)`, immune to pid-reuse races — it +identifies the child by fd, not by number. Rust's `CLONE_CLEAR_SIGHAND` +(via `posix_spawn`) solves a different race: the child starts with default +signal dispositions instead of whatever handlers the parent had installed, +exactly the class of inherited-state bug the vfork'd shared address space +would otherwise make lethal. One more accounting subtlety: +`getrusage(RUSAGE_CHILDREN)` is process-wide state — if concurrent code +reaped other children between two calls, the aggregate moves — while +`wait4`'s per-child rusage is race-free by construction. pmon v0 is +single-threaded in all three languages; these flags are the runtimes +defending themselves. + +## Build, run, observe + +```bash +[host]$ cd examples/11-process-lifecycle && ./demo.sh +``` + +Each language builds and walks the four fates. By hand, the runs that +produced this chapter's numbers: + +```bash +[host]$ cpp/build/release/app run -- sh -c 'exit 42' +pmon: pid 2600288 exited status 42 +pmon: rusage maxrss=3936KB user=0.000s sys=0.001s wall=1ms +[host]$ echo $? +42 +[host]$ go/bin/app run -- sh -c 'kill -TERM $$' +pmon: pid 2600305 killed by signal 15 (TERM) +pmon: rusage maxrss=3816KB user=0.000s sys=0.001s wall=1ms +[host]$ echo $? +143 +[host]$ rust/target/release/app run -- sh -c 'kill -KILL $$' +pmon: pid 2613705 killed by signal 9 (KILL) +pmon: rusage maxrss=3576KB user=0.000s sys=0.000s wall=0ms +[host]$ echo $? +137 +[host]$ cpp/build/release/app run -- ./nonexistent +pmon: exec ./nonexistent: No such file or directory +[host]$ echo $? +127 +``` + +Two more observations tie the report lines to reality. A 200 ms sleep +yields `wall=200ms` on the nose (`sleep 0.2` → `maxrss=2120KB user=0.000s +sys=0.000s wall=200ms`), and the pid in the fate line is the child's own: +`run -- sh -c 'echo "child: my pid is $$"'` printed `child: my pid is +2607401` through the inherited stdout, followed by pmon's +`pid 2607401 exited status 0` — same number, one from inside, one from the +wait. The runner +(`python3 scripts/test-all-examples.py --only 11-process-lifecycle`) +reports `PASS PASS PASS` for cpp, go, and rust — 3 passed, 0 failed, 0 +skipped — with `verify.lua` asserting 27 behavioral checks per language. + +## Cross-check, three ways + +**The clone flavors, under `strace -f`.** The claim was that three runtimes +spell "make a child" three ways. Tracing each binary running `/bin/true` +with `strace -f -e trace=clone,clone3,vfork,execve,wait4,waitid`, the +process-creating call and the reap, verbatim per runtime — C++ (glibc +`fork`): + +``` +[host]$ strace -f -e trace=clone,clone3,vfork,execve,wait4,waitid cpp/build/release/app run -- /bin/true +2600465 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fe6aa38ba50) = 2600466 +2600466 execve("/bin/true", ["/bin/true"], 0x7ffd64262d60 /* 96 vars */ +2600465 wait4(2600466 +2600465 <... wait4 resumed>, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 2600466 +``` + +A full copy-on-write fork — `child_stack=NULL` means "same stack, COW" — +then `wait4` with a null rusage pointer (pmon's C++ uses `getrusage` +instead). Go issues: + +``` +2600690 clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD +2600696 execve("/bin/true", ["/bin/true"], 0x2bac423f2388 /* 96 vars */ +2600690 <... clone resumed>, parent_tid=[6]) = 2600696 +2600690 waitid(P_PIDFD, 6 +2600690 <... waitid resumed>, {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2600696, si_uid=1000, si_status=0, si_utime=0, si_stime=0}, WEXITED, {ru_utime={tv_sec=0, tv_usec=0}, ru_stime={tv_sec=0, tv_usec=1058}, ...}) = 0 +``` + +vfork semantics (`CLONE_VM|CLONE_VFORK`: shared address space, parent +suspended until exec), *plus* `CLONE_PIDFD` — the reap is +`waitid(P_PIDFD, 6, ...)`, identifying the child by the returned pidfd, and +the trace even shows Go probing pidfd support first — a +`waitid(P_PIDFD, 4, ...) = -1 ECHILD` self-check, then a throwaway +`CLONE_PIDFD` clone waited on fd 5 — before the real spawn. Rust is the +third flavor: + +``` +2600701 clone3({flags=CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND, exit_signal=SIGCHLD, stack=0x7f7f7c13a000, stack_size=0x9000}, 88 +2600702 execve("/bin/true", ["/bin/true"], 0x7ffec54d8a00 /* 96 vars */ +2600701 <... clone3 resumed>) = 2600702 +2600701 wait4(2600702 +2600701 <... wait4 resumed>, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, {ru_utime={tv_sec=0, tv_usec=0}, ru_stime={tv_sec=0, tv_usec=656}, ...}) = 2600702 +``` + +`clone3(2)` — the modern extensible spelling, here via glibc's +`posix_spawn` — with the same vfork optimization plus +`CLONE_CLEAR_SIGHAND`, and then pmon's own explicit `wait4` with a real +rusage out-parameter, exactly as `wait4()` in `rust/src/main.rs` claims. +Three clone flavors, one lifecycle. + +**A zombie, live in `ps f` and `/proc`.** pmon reaps promptly, so to catch +the zombie window you stop the *parent* mid-wait: start +`app run -- sleep 3`, `kill -STOP` pmon, and let the child die while its +reaper is frozen. Four seconds later, on this host: + +```bash +[host]$ ps f -o pid,ppid,stat,cmd -p 2601639,2601641 + PID PPID STAT CMD +2601639 2601637 TN cpp/build/release/app run -- sleep 3 +2601641 2601639 ZN \_ [sleep] +[host]$ head -3 /proc/2601641/status +Name: sleep +State: Z (zombie) +Tgid: 2601641 +[host]$ cat /proc/2601641/stat +2601641 (sleep) Z 2601639 2601637 2601637 0 -1 4227084 103 0 0 0 0 0 0 0 25 5 1 0 64173304 0 0 ... +``` + +Walk the `stat` fields (the format is `pid (comm) state ...` — parse from +the right of the closing parenthesis, since `comm` may contain spaces): +field 3 is `Z`, the zombie state `ps` renders as ``; field 4 is the +ppid `2601639` — stopped pmon, which `ps f`'s tree draws as the parent of +its own corpse; field 10, `minflt=103`, is the copy-on-write bill discussed +earlier; field 19 is nice `5` (the `N` in both STAT columns — this session +ran niced); and fields 23–24, `vsize` and `rss`, are both `0` — the address +space is already gone, which is why killing a zombie is meaningless: there +is nothing left to kill, only data waiting to be read. `kill -CONT 2601639` +ended the window: pmon's `wait_child` returned at last and it printed +`pmon: pid 2601641 exited status 0` with `wall=4343ms` — wall time measures +until the *reap*, not the death, and the stopped window is in the bill. + +**An orphan, reparented.** The inverse experiment — parent dies first — +takes one line: `sh -c 'sleep 7 >/dev/null 2>&1 & echo $!'` prints the +background child's pid and exits, orphaning it. Half a second later: + +```bash +[host]$ ps -o pid,ppid,stat,cmd -p 2606723 + PID PPID STAT CMD +2606723 6145 S sleep 7 +[host]$ ps -o pid,cmd -p 6145 + PID CMD + 6145 /usr/lib/systemd/systemd --user --deserialize=10 +``` + +The dead shell's pid is gone from the pair; the child's `PPid` is now 6145, +the user's `systemd --user` — a registered subreaper, which will `waitid` +the sleep the moment it exits. No zombie ever forms on this path. + +> **On the lab VM** unverified — +> the fleet-wide view of this chapter is `execsnoop` and `exitsnoop` from +> bcc-tools: every exec and every exit on the box, with the same +> status/signal split pmon decodes, no per-process strace needed. bcc-tools +> need root and a kernel with BTF, so they run on the `systems-target` VM, +> and Part 8's eBPF observation chapter exercises them there against these +> exact binaries. + +## What you learned + +- A process is born in two steps — `fork` (COW: page tables now, pages only + on write; 103 minor faults for a whole spawn) and `exec` (new image, same + pid and fds) — and the gap between them is where fd plumbing and privilege + dropping live; a CLOEXEC self-pipe is how exec failure reports back. +- Death is data: the kernel holds exit status and rusage as a zombie until + `wait4`/`waitid` reaps it, `ps` state `Z`/`` with `rss=0`; orphans + are reparented to a subreaper (`systemd --user` here), never abandoned. +- One wait status, two disjoint deaths: `WIFEXITED`+`WEXITSTATUS` vs + `WIFSIGNALED`+`WTERMSIG`, flattened by the shell convention pmon mirrors — + `s`, `128+n`, or 127 when exec itself failed. +- The tidy APIs are the same machinery: glibc `fork` is + `clone(SIGCHLD)`, Go spawns with `clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK)` + and reaps by pidfd via `waitid(P_PIDFD)`, Rust's `Command` rides + `posix_spawn`'s `clone3(CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND)` — + three flavors of one lifecycle, all read from `strace -f`. + +Next, **safe signals**: what a handler is allowed to do (almost nothing), +why `pmon` will route SIGTERM through a signalfd instead, and how to forward +signals to a child without racing its death. + +--- + +

verified — every number and +output line above was produced on the Fedora 44 reference host this session: +the runner printed 11-process-lifecycle PASS PASS PASS (3 +passed, 0 failed, 0 skipped; 27 verify.lua checks per language); the fate +runs (42/143/137/127, PATH-miss parity, wall=200ms for a 200 ms +sleep, pid 2607401 echoed by the child and reported by the wait) are real +transcripts; the strace excerpts showing clone(SIGCHLD), +clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD) + +waitid(P_PIDFD), and +clone3(CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND) are trimmed +real output; and the zombie window (ZN [sleep] <defunct>, +State: Z (zombie), stat fields walked, reap after +kill -CONT with wall=4343ms) and the orphan +reparented to systemd --user pid 6145 were captured live. The +"On the lab VM" execsnoop/exitsnoop callout is unverified as marked, deferred +to Part 8.

diff --git a/_docs/12-signals-and-safe-handling.md b/_docs/12-signals-and-safe-handling.md new file mode 100644 index 0000000..b08c0fd --- /dev/null +++ b/_docs/12-signals-and-safe-handling.md @@ -0,0 +1,696 @@ +--- +title: "Signals and safe handling" +order: 12 +part: "Processes, Signals, Privilege" +description: "Dispositions, per-thread masks, and the pending set — then three race-free ways to consume signals as pmon v1 becomes a restarting supervisor: signalfd in C++, runtime channels in Go, the self-pipe trick in Rust, with strace catching Go's 114-call rt_sigaction storm and SIGURG preemption in the act." +duration: 50 minutes +--- + +Chapter 11 taught `pmon` to run one child and report how it died. Version 1 +makes it a real supervisor: restart a crashing child with a doubling backoff, +shut down cleanly on SIGTERM or SIGINT, and replace the child on SIGHUP. That +sounds like three `signal()` calls — and that is exactly the trap. The one new +idea of this chapter is that a signal handler is close to lawless territory +where almost no code is safe to run, so well-built programs run *no* meaningful +code there at all: they convert signals into ordinary data — a file descriptor +read, a channel receive, a byte in a pipe — and consume that data in a plain +loop. Each language implements a different one of the three classic +conversions, on purpose, because together they are the lesson. + +The code is in `examples/12-signals-and-safe-handling/`. `./demo.sh` builds +all three implementations and drives a crash-restart-give-up cycle; the +`README.md` there specifies the CLI, the output lines, and the exit codes all +three languages share. + +{% include excalidraw.html + file="12-signal-delivery-paths" + alt="Two bands. Kernel band: kill and child-exit sources feed a per-process disposition table and a per-thread mask, converging on an amber pending set that coalesces one bit per signal, then a dashed delivery box gated on unblocked. User band: three columns for C++ signalfd read, Go runtime handler into a signal.Notify channel, and Rust self-pipe handler setting an AtomicBool and writing a doorbell byte, each ending in the main loop reading plain data." + caption="Figure 12.1 — every signal passes the same three kernel words — disposition, mask, pending — and pmon hears it three ways, none of them in signal context" %} + +> **Tools used** — `strace`, `kill`, `sh`, `python3`, `/proc` (host); +> `bpftrace` (lab VM, exercised in Part 8). Everything here is checked by +> `scripts/check-host.sh`, ships with Fedora, or is preinstalled in the lab +> VMs. + +## Disposition, mask, pending + +The kernel's whole signal model fits in three words, and every bug in this +area comes from mixing them up. + +The **disposition** is per-process: for each signal, one of `SIG_DFL` (the +default — terminate, ignore, stop, or dump core, depending on the signal), +`SIG_IGN`, or a handler function installed with `sigaction(2)`. Dispositions +survive `fork`; across `execve` a handler resets to `SIG_DFL` (the function +pointer would be meaningless in the new image) while `SIG_IGN` survives. + +The **mask** is per-*thread*: the set of signals whose delivery is postponed. +A blocked signal is not lost — it waits. The mask survives both `fork` *and* +`exec`, which is a supervisor-shaped landmine: `pmon supervise` blocks +SIGTERM, and a forked child inherits that mask straight through `execvp`, so +the supervised program would start life unable to be terminated. This is why +`spawn` in all three implementations resets the mask in the child, between +`fork` and `exec` — the C++ spelling, from +`examples/12-signals-and-safe-handling/cpp/src/main.cpp`: + +```cpp + if (pid == 0) { + sigset_t none; + sigemptyset(&none); + ::sigprocmask(SIG_SETMASK, &none, nullptr); + ::execvp(argv[0], argv.data()); + constexpr char msg[] = "pmon: error: exec failed\n"; + [[maybe_unused]] auto n = ::write(STDERR_FILENO, msg, sizeof msg - 1); + ::_exit(127); + } +``` + +The **pending set** is where a generated-but-undelivered signal sits, and for +standard signals it is literally one bit each: a second SIGCHLD arriving +while one is already pending *coalesces* into the same bit. This is the core +of SIGCHLD semantics: the signal means "one **or more** children changed +state", never "exactly one". The robust consumer treats SIGCHLD as a doorbell +and asks `waitpid(…, WNOHANG)` what actually happened; `pmon` has exactly one +child at a time, so a targeted `waitpid(child, …, WNOHANG)` is the whole +answer — the general reap-until-empty loop arrives with pmon's later growth. +Delivery itself happens only when a signal is pending *and* unblocked in some +thread, at an arbitrary instant between two instructions — which is the +problem the next section is about. + +## Why handlers are a minefield + +A handler interrupts your program wherever it happens to be — possibly +halfway through `malloc`, holding the allocator's lock. If the handler then +calls `malloc` (or `printf`, or anything that might), the same thread waits +on a lock it already holds: deadlock, rarely, under load, in production. +That is why POSIX defines **async-signal-safety**: the short list of +functions guaranteed re-entrant against this kind of interruption — +`write`, `_exit`, `sigprocmask`, `kill`, `waitpid`, and a few dozen friends +(`man 7 signal-safety`). Not `malloc`. Not `printf`. Not most of any +language's runtime: locking a mutex, growing a Go stack, or formatting a +Rust string are all off the list. The compiler will not warn you; the +program will just deadlock or corrupt state on the unlucky interleaving. + +The chapter's three implementations all draw the same conclusion: get out of +signal context immediately, running either nothing there (C++), or only +handler code someone already proved safe (Go's runtime, Rust's +`signal-hook`). The only place `pmon` itself must obey the list is the child +branch of `spawn` above — after `fork` in a program that may have threads +(Go always does), the child runs with only async-signal-safe calls until +`execvp`: `sigprocmask`, `execvp`, `write`, `_exit`. All four are on the +list, and nothing else happens there. + +## The self-pipe trick, and signalfd as its resolution + +The classic escape is the **self-pipe trick**: at startup, create a pipe; +the handler does exactly one safe thing — `write` one byte to it — and the +main loop, already blocked in `poll`, wakes up, drains the pipe, and handles +the event as ordinary code. The pipe is the bridge from signal context to +loop context, and `write(2)` is async-signal-safe. The Rust implementation +is deliberately the historical exhibit, with the unsafe parts delegated to +`signal-hook` — each signal is registered twice, a flag and a doorbell, from +`examples/12-signals-and-safe-handling/rust/src/main.rs`: + +```rust +/// One registered signal: the flag half of the flag + doorbell pair. The +/// handler owns its dup of the pipe's write end as an OwnedFd; signal-hook +/// makes it non-blocking so the handler can never wedge on a full pipe. +fn flag_for(signal: i32, doorbell: &PipeWriter) -> Result> { + let flag = Arc::new(AtomicBool::new(false)); + signal_hook::flag::register(signal, Arc::clone(&flag)).context("flag::register")?; + let owned: OwnedFd = doorbell.as_fd().try_clone_to_owned().context("dup")?; + signal_hook::low_level::pipe::register_raw(signal, owned) + .context("pipe::register_raw")?; + Ok(flag) +} +``` + +Linux then folded the trick into the kernel: `signalfd(2)` gives you a file +descriptor that *is* the pipe, with structured `signalfd_siginfo` records +instead of anonymous doorbell bytes. You block the signals with +`sigprocmask` — so normal delivery never happens — and read them like any +other fd. The C++ implementation opens with exactly that pair: + +```cpp + // Block the signals we care about, then receive them as fd reads. + sigset_t mask; + sigemptyset(&mask); + for (const int signo : {SIGTERM, SIGINT, SIGHUP, SIGCHLD}) { + sigaddset(&mask, signo); + } + if (::sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) { + die("sigprocmask", errno_ec()); + } + UniqueFd sfd(::signalfd(-1, &mask, SFD_CLOEXEC)); + if (!sfd.valid()) { + die("signalfd", errno_ec()); + } +``` + +No handler is ever installed; nothing asynchronous ever runs. Go is the +third design point: the runtime owns real handlers for almost every signal +(you will count them in the cross-check), and those handlers — written once, +by people who proved them safe — forward into the scheduler, where +`signal.Notify` turns delivery into a channel send: + +```go + sigCh := make(chan os.Signal, 8) + signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT, unix.SIGHUP) + + // One in-flight wait goroutine per live child; the buffered channel lets + // it finish even when the supervisor consumes the exit via stopChild. + waitCh := make(chan childExit, 1) +``` + +Three mechanisms, one shape: signal context shrinks to zero (C++), or to a +`write` and an atomic store (Rust), or to code you never wrote (Go), and +everything meaningful happens in a loop that reads plain values. + +## Forwarding, and who gets Ctrl-C + +`pmon` forwards shutdown explicitly: `kill(child_pid, SIGTERM)`, one target. +But when you press Ctrl-C in a terminal, the kernel sends SIGINT to the +foreground **process group** — pmon *and* its child both receive it, because +`spawn` leaves the child in pmon's group. That is a deliberate v1 choice, +and it composes: the child dies of its default SIGINT disposition, pmon +consumes its copy through its mechanism, reaps, prints the shutdown line — +the same path `kill -INT ` exercises alone. The alternative +design, `setpgid` the child into its own group and signal `-pgid` to reach a +whole tree, is what shells do to implement job control; pmon does not need +it yet, and neither spelling can catch a child's *grandchildren* that +daemonize out of the group — the real fix for that is cgroups, in the +containers part. + +{% include excalidraw.html + file="12-supervisor-state-machine" + alt="State machine: an amber child-running state with infinite poll timeout, a backoff-wait state with deadline poll timeout, an amber transition between them labeled with the doubling backoff, a return transition when the backoff elapses, exits to exit 0 on clean exit or SIGTERM/SIGINT and to exit 1 when restarts reach the maximum, and a dashed SIGHUP reload box that stops the child, resets the budget, and spawns a replacement." + caption="Figure 12.2 — pmon supervise as a two-state machine; every transition is triggered by an ordinary value read in the main loop" %} + +## How the code works + +The supervisor state fits in four variables, identical in all three +languages: the current child's pid (or handle), a `running` flag (Go spells +it "is there a timer"), a restart counter checked against `--max-restarts`, +and the current backoff, which starts at `--backoff-ms` and doubles after +every crash. A crash moves the machine from *running* to *backoff wait*; the +backoff elapsing moves it back by spawning a fresh child; a clean exit, +SIGTERM, or SIGINT leaves through exit 0; exhausting the budget leaves +through exit 1; SIGHUP stays in *running* but swaps the child and resets +both counter and backoff — a reload should not inherit a crashing child's +penalties. Here is the consumption loop, three ways: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp + for (;;) { + int timeout = -1; + if (!running) { + const auto left = deadline - steady_clock::now(); + timeout = + left <= 0ns + ? 0 + : static_cast( + duration_cast(left).count()) + + 1; + } + pollfd pfd{.fd = sfd.get(), .events = POLLIN, .revents = 0}; + const int nready = ::poll(&pfd, 1, timeout); + if (nready < 0) { + if (errno == EINTR) { + continue; + } + die("poll", errno_ec()); + } + if (nready == 0) { + // Backoff elapsed: bring the child back. + child = start(); + running = true; + continue; + } + + signalfd_siginfo si{}; + const ssize_t nread = ::read(sfd.get(), &si, sizeof si); + if (nread != static_cast(sizeof si)) { + die("read signalfd", errno_ec()); + } + + switch (si.ssi_signo) { + case SIGTERM: + case SIGINT: { + if (running) { + stop_child(child); + } + std::println("pmon: shutting down ({})", + si.ssi_signo == SIGTERM ? "SIGTERM" : "SIGINT"); + return 0; + } + case SIGHUP: { + std::println("pmon: reload requested"); + if (running) { + stop_child(child); + } + restarts = 0; + backoff = opts.backoff_ms; + child = start(); + running = true; + break; + } + case SIGCHLD: { + int wstatus = 0; + const pid_t reaped = ::waitpid(child, &wstatus, WNOHANG); + if (!running || reaped != child) { + break; // stale notification (e.g. already reaped on reload) + } + const ChildExit ce = decode(wstatus); + report(child, ce); + if (!ce.signaled && ce.status == 0) { + return 0; + } + if (restarts >= opts.max_restarts) { + std::println("pmon: giving up after {} restarts", + opts.max_restarts); + return 1; + } + ++restarts; + std::println("pmon: restart #{} (backoff {}ms)", restarts, backoff); + deadline = steady_clock::now() + milliseconds(backoff); + backoff *= 2; + running = false; + break; + } + default: + break; + } + } +``` + +```go + for { + var timerCh <-chan time.Time + if timer != nil { + timerCh = timer.C + } + select { + case s := <-sigCh: + switch s { + case unix.SIGTERM, unix.SIGINT: + if timer == nil { + stopChild(cmd) + } + fmt.Printf("pmon: shutting down (%s)\n", signame(s)) + return 0 + case unix.SIGHUP: + fmt.Println("pmon: reload requested") + if timer == nil { + stopChild(cmd) + } else { + timer.Stop() + timer = nil + } + restarts = 0 + backoff = opts.backoffMs + cmd = start() + } + case ce := <-waitCh: + // The child exited on its own. + report(cmd.Process.Pid, ce) + if !ce.signaled && ce.status == 0 { + return 0 + } + if restarts >= opts.maxRestarts { + fmt.Printf("pmon: giving up after %d restarts\n", opts.maxRestarts) + return 1 + } + restarts++ + fmt.Printf("pmon: restart #%d (backoff %dms)\n", restarts, backoff) + timer = time.NewTimer(time.Duration(backoff) * time.Millisecond) + backoff *= 2 + case <-timerCh: + // Backoff elapsed: bring the child back. + timer = nil + cmd = start() + } + } +``` + +```rust + loop { + let timeout = if running { + PollTimeout::NONE + } else { + let left = deadline.saturating_duration_since(Instant::now()); + let ms = left.as_millis().saturating_add(1).min(u16::MAX as u128); + if left.is_zero() { PollTimeout::ZERO } else { PollTimeout::from(ms as u16) } + }; + let nready = { + let mut fds = [PollFd::new(reader.as_fd(), PollFlags::POLLIN)]; + match poll(&mut fds, timeout) { + Ok(n) => n, + Err(Errno::EINTR) => continue, + Err(e) => return Err(e).context("poll"), + } + }; + if nready == 0 { + if Instant::now() < deadline { + continue; // clamped timeout; keep waiting + } + // Backoff elapsed: bring the child back. + child = spawn(&opts.argv)?; + running = true; + continue; + } + + // Drain the doorbell (poll said readable; coalesced bytes are fine). + let mut buf = [0u8; 64]; + let _ = reader.read(&mut buf).context("read doorbell")?; + + let term = got_term.swap(false, Ordering::SeqCst); + let intr = got_int.swap(false, Ordering::SeqCst); + if term || intr { + if running { + stop_child(child)?; + } + println!( + "pmon: shutting down ({})", + if term { "SIGTERM" } else { "SIGINT" } + ); + return Ok(0); + } + + if got_hup.swap(false, Ordering::SeqCst) { + println!("pmon: reload requested"); + if running { + stop_child(child)?; + } + restarts = 0; + backoff = opts.backoff_ms; + child = spawn(&opts.argv)?; + running = true; + continue; + } + + // SIGCHLD (or a stale doorbell byte): see whether our child exited. + if !running { + continue; + } + match waitpid(Pid::from_raw(child), Some(WaitPidFlag::WNOHANG)) { + Ok(ws) => { + let Some(ce) = decode_wait(ws) else { continue }; + report(child, ce); + if ce.clean() { + return Ok(0); + } + if restarts >= opts.max_restarts { + println!("pmon: giving up after {} restarts", opts.max_restarts); + return Ok(1); + } + restarts += 1; + println!("pmon: restart #{restarts} (backoff {backoff}ms)"); + deadline = Instant::now() + Duration::from_millis(backoff); + backoff *= 2; + running = false; + } + Err(Errno::EINTR) | Err(Errno::ECHILD) => continue, + Err(e) => return Err(e).context("waitpid"), + } + } +``` + +Structure first: all three block on *one* wait point — `poll` on the +signalfd, `select` over three channels, `poll` on the doorbell — and the +backoff is not a `sleep` but that wait's *timeout* (Go uses a timer channel +for the same effect). That single decision keeps the supervisor responsive: +a SIGTERM arriving mid-backoff interrupts the wait instead of queuing behind +a sleep. In C++ and Rust the timeout is recomputed each iteration from an +absolute deadline — poll can return early for unrelated reasons, and +re-deriving "time left" from a deadline is immune to that; the `+ 1` +guards the truncation of a fractional millisecond so the loop cannot spin at +zero-timeout just before the deadline. + +Three per-language details deserve a closer look. The C++ SIGCHLD arm +checks `reaped != child` and drops the record: on a reload, `stop_child` +already reaped the old child synchronously, but the SIGCHLD it generated is +still queued in the signalfd — a *stale notification*. The state machine +must be driven by `waitpid`'s answer, never by the signal's arrival; the +signal is a doorbell, remember, not a fact. Go's version of the same care +is `waitCh` having buffer size 1 with one wait-goroutine per child: +`stopChild` consumes the exit synchronously from the channel, so a reload +cannot leave a goroutine's send racing the next child's. Rust's is the +drain-then-check ordering — read up to 64 doorbell bytes (several signals +may have coalesced into one wakeup, exactly like the pending set), *then* +swap each `AtomicBool` to false, then let `waitpid(WNOHANG)` say whether +SIGCHLD meant anything; a doorbell byte with nothing behind it (`StillAlive`) +just loops. + +Fragile bits, stated plainly: the doubling backoff is uncapped — with +`--backoff-ms 1` and `--max-restarts 40` the final wait would be 2³⁹ ms, +about seventeen years — so `--max-restarts` is the only brake, and a +production supervisor would clamp the backoff itself; Rust's `PollTimeout` +is a `u16`, so waits past 65.5 s are clamped and re-entered (the +`Instant::now() < deadline` check); and `run` (unchanged from v0) reaps with +the default mask and dispositions, so Ctrl-C during `pmon run` kills both +processes — supervision semantics belong to `supervise` only. + +## Errors, three ways + +The contract is unchanged from chapter 11 — `pmon: error: …` on stderr and +exit 1 for runtime failures, usage on exit 2 — but signals sharpen the +`EINTR` policy from chapter 4 into three distinct spellings. C++ retries +`poll` on `EINTR` explicitly, and `reap_blocking` loops `waitpid` the same +way; under `supervise` the mask makes those interruptions nearly impossible, +but `run` reaps with the default mask, so the loop is load-bearing there. +Rust matches `Errno::EINTR` as a value in both `poll` and `waitpid` — its +handlers really do interrupt syscalls, as the cross-check will show — and +additionally tolerates `Errno::ECHILD` ("no child to reap") as a stale +doorbell rather than an error. Go never sees `EINTR` at all: the runtime +installs every handler with `SA_RESTART` and retries interrupted waits +inside `cmd.Wait`, so user code only ever receives the final verdict. +One deliberate acceptance shows up in all three `stop_child`s: `ESRCH` from +`kill` means the child is already gone mid-shutdown — a race you expect, +handled at the call site, while everything else carries context up to one +printer, exactly the chapter-5 policy. + +## Concurrency lens + +Go's runtime is the loudest signal user in this book, and now we can catch +it working. Its handlers are installed with `sa_mask=~[]` (all signals +blocked while handling) and `SA_ONSTACK` (an alternate stack, so a handler +survives even a blown goroutine stack), and the runtime *sends itself* +signals: since Go 1.14, a goroutine stuck in a tight loop is preempted by a +`tgkill(…, SIGURG)` from another runtime thread. An idle supervisor never +needs that, but GC pressure forces it — `GOGC=1` makes collections constant, +and the trace shows runtime threads interrupting each other: + +```bash +[host]$ GOGC=1 strace -f -o urg.trace -e trace=tgkill ./go/bin/app supervise \ + --max-restarts 8 --backoff-ms 1 -- sh -c 'exit 1' >/dev/null +[host]$ grep -m1 -A1 'SIGURG' urg.trace +2622691 tgkill(2622690, 2622690, SIGURG) = 0 +2622690 --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=2622690, si_uid=1000} --- +[host]$ grep -c -- '--- SIGURG' urg.trace +4 +``` + +Four SIGURG deliveries in a supervisor that never asked for any signal — +your Go programs live in this weather all the time, which is why `os/exec` +retries `EINTR` for you and why you must never install a C signal handler +into a Go process. The same lens explains a signalfd caveat: the mask is +per-thread, and signalfd only sees signals *blocked* in every thread that +might otherwise take delivery. The C++ code blocks the mask before any +thread could exist, so the invariant holds trivially; in a threaded program +you must block-then-spawn so children inherit the mask. Rust's mechanism is +the only one where user-adjacent code truly runs in signal context, and its +concurrency budget is exactly one atomic store plus one non-blocking `write` +— a full doorbell drops the byte harmlessly, because bytes, like pending +bits, are allowed to coalesce. + +## Build, run, observe + +```bash +[host]$ cd examples/12-signals-and-safe-handling && ./demo.sh +``` + +Each language builds and runs the crash loop. By hand, from this session — +`run` still propagates verdicts exactly as v0 did: + +```bash +[host]$ ./cpp/build/release/app run -- sh -c 'exit 3'; echo "exit=$?" +pmon: started pid 2616819 +pmon: child 2616819 exited status=3 +exit=3 +[host]$ ./cpp/build/release/app run -- sh -c 'kill -9 $$'; echo "exit=$?" +pmon: started pid 2616821 +pmon: child 2616821 killed signal=9 +exit=137 +[host]$ ./cpp/build/release/app supervise --max-restarts 2 --backoff-ms 60 -- sh -c 'exit 1' +pmon: started pid 2616933 +pmon: child 2616933 exited status=1 +pmon: restart #1 (backoff 60ms) +pmon: started pid 2616934 +pmon: child 2616934 exited status=1 +pmon: restart #2 (backoff 120ms) +pmon: started pid 2616935 +pmon: child 2616935 exited status=1 +pmon: giving up after 2 restarts +``` + +Three distinct pids, the backoff printed doubling, exit 1. Driving the Go +build with signals from another shell produces the reload-then-shutdown +story — note the replacement pid: + +```bash +[host]$ ./go/bin/app supervise -- sleep 300 & +pmon: started pid 2625064 +[host]$ kill -HUP +pmon: reload requested +pmon: started pid 2625068 +[host]$ kill -TERM +pmon: shutting down (SIGTERM) +``` + +`verify.lua` asserts all of this behaviorally — 31 checks per language, +including measured backoff elapsed time and post-reload shutdown — and this +session's runner (`python3 scripts/test-all-examples.py --only +12-signals-and-safe-handling`) reports `PASS PASS PASS` for cpp, go, and +rust, with a standalone `LSP_LANG=cpp` run printing `PASS 31 / FAIL 0`. + +## Cross-check, three ways + +**The syscall shape, under `strace`.** If the three designs are real, the +traces must be unmistakably different. The C++ supervisor's *entire* signal +footprint for a one-restart run is ten lines — zero `rt_sigaction`, one +`signalfd4`: + +```bash +[host]$ strace -o cpp.trace -e trace=rt_sigaction,rt_sigprocmask,signalfd4,poll,ppoll \ + ./cpp/build/release/app supervise --max-restarts 1 --backoff-ms 30 -- sh -c 'exit 1' >/dev/null +[host]$ cat cpp.trace +rt_sigprocmask(SIG_BLOCK, [HUP INT TERM CHLD], NULL, 8) = 0 +signalfd4(-1, [HUP INT TERM CHLD], 8, SFD_CLOEXEC) = 3 +rt_sigprocmask(SIG_BLOCK, ~[], [HUP INT TERM CHLD], 8) = 0 +rt_sigprocmask(SIG_SETMASK, [HUP INT TERM CHLD], NULL, 8) = 0 +poll([{fd=3, events=POLLIN}], 1, -1) = 1 ([{fd=3, revents=POLLIN}]) +poll([{fd=3, events=POLLIN}], 1, 30) = 0 (Timeout) +rt_sigprocmask(SIG_BLOCK, ~[], [HUP INT TERM CHLD], 8) = 0 +rt_sigprocmask(SIG_SETMASK, [HUP INT TERM CHLD], NULL, 8) = 0 +poll([{fd=3, events=POLLIN}], 1, -1) = 1 ([{fd=3, revents=POLLIN}]) ++++ exited with 1 +++ +``` + +(The block-all/restore pairs are glibc protecting its own `fork` internals — +not our code.) No `--- SIGCHLD ---` stops ever appear: blocked signals are +never delivered, they are *read*, and the 30 ms poll timeout is the backoff. +The same command with `-f` on the Go binary tells the opposite story: this +session's supervisor pid 2617196 issued exactly **114 `rt_sigaction` +calls** — 58 probes reading old dispositions plus 56 installs of the runtime +handler (`sa_handler=0x484d40`, `sa_mask=~[]`, +`SA_ONSTACK|SA_RESTART|SA_SIGINFO`) covering SIGHUP through `SIGRT_32` — the +identical 114-call storm chapter 4 counted for a program that never touched +`os/signal`, because it is runtime initialization, not `signal.Notify`. Zero +`signalfd4`, zero `poll`/`ppoll` (the runtime waits in `futex` and its +netpoller). Rust sits between: 13 `rt_sigaction` — probe+install pairs for +our four signals via `signal-hook`, plus `SIGPIPE` set to ignore and a +`SIGSEGV`/`SIGBUS` pair that std installs for stack-overflow reporting — a +`pipe2([3, 4], O_CLOEXEC)` for the doorbell, and then real deliveries +interrupting the wait, self-pipe mechanics live on the wire: + +``` +poll([{fd=3, events=POLLIN}], 1, -1) = ? ERESTART_RESTARTBLOCK (Interrupted by signal) +--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2624617, si_uid=1000, si_status=1, ...} --- +poll([{fd=3, events=POLLIN}], 1, -1) = 1 ([{fd=3, revents=POLLIN}]) +``` + +**The kernel's bookkeeping, in `/proc`.** `/proc//status` prints the +three words of this chapter as hex bitmasks — bit *n* is signal *n + 1*. A +live C++ supervisor from this session: + +```bash +[host]$ ./cpp/build/release/app supervise -- sleep 30 & +pmon: started pid 2622949 +[host]$ grep -E '^Sig(Pnd|Blk|Ign|Cgt)' /proc//status +SigPnd: 0000000000000000 +SigBlk: 0000000000014003 +SigIgn: 0000000000000006 +SigCgt: 0000000000000000 +``` + +Decode `SigBlk` by hand: `0x14003` = bits 0, 1, 14, 16 = signals 1, 2, 15, +17 — SIGHUP, SIGINT, SIGTERM, SIGCHLD, precisely the `sigprocmask` set. And +`SigCgt: 0`: this process catches *nothing* — the whole design in one zero. +(`SigIgn: 6` — SIGINT and SIGQUIT — was inherited from the non-interactive +shell that launched this background job: dispositions cross `fork` and +`exec`, as promised.) The other two, decoded with three lines of Python +against the same live technique: + +```bash +[host]$ python3 -c ' +import signal +for name, hexmask in [("go SigCgt", "fffffffd7fc1feff"), ("rust SigCgt", "14443")]: + m = int(hexmask, 16) + sigs = [signal.Signals(b + 1).name for b in range(31) if m >> b & 1] + print(name, "->", bin(m).count("1"), "caught;", " ".join(sigs[:6]), "...")' +go SigCgt -> 56 caught; SIGHUP SIGINT SIGQUIT SIGILL SIGTRAP SIGABRT ... +rust SigCgt -> 6 caught; SIGHUP SIGINT SIGBUS SIGSEGV SIGTERM SIGCHLD ... +``` + +Go catches 56 of 64 signals — everything except SIGKILL and SIGSTOP (which +no one may catch), the job-control quartet SIGCONT/SIGTSTP/SIGTTIN/SIGTTOU, +and two reserved realtime slots — while its `SigBlk` line +(`fffffffc3bba3a00` on this run) belongs to the main thread only; the mask +is per-thread, and the runtime retunes it constantly, which is the +`rt_sigprocmask` chatter in its trace. Rust's `0x14443` is our four signals +(`0x14003`, the same bits as the C++ *blocked* set — caught in one design, +blocked in the other) plus bits 6 and 10: SIGBUS and SIGSEGV, the std +stack-overflow guard the strace already showed. Three processes, one hex +format, and each design legible straight from the kernel's ledger. + +> **On the lab VM** unverified — +> the kernel-side view of this chapter is `bpftrace` on +> `tracepoint:signal:signal_generate` and `signal_deliver`, watching the +> pending-set handoff fleet-wide instead of per-process: which pid raised +> each SIGCHLD, where each SIGURG landed. That needs root and a disposable +> kernel, so it is not run here; chapter 30 (Debugging part) exercises +> exactly this on `systems-target`. + +## What you learned + +- A signal's life is three kernel words: a per-process **disposition**, a + per-thread **mask** (which survives fork *and* exec — reset it in the + child), and a coalescing **pending set** — which is why SIGCHLD means "one + or more" and `waitpid(WNOHANG)` is the only source of truth. +- Handlers interrupt arbitrary code, so only the async-signal-safe list may + run there; the professional designs shrink signal context to nothing: + signalfd reads (`SigCgt: 0`), runtime-owned handlers feeding channels, or + a self-pipe doorbell plus atomic flags. +- One blocking wait with the backoff as its *timeout* keeps a supervisor + responsive to shutdown mid-backoff, and stale SIGCHLD notifications must + be discarded by checking `waitpid`'s answer, not the signal's arrival. +- The three designs are visible from outside: 0 vs 114 vs 13 `rt_sigaction` + calls, `signalfd4` vs a runtime `tgkill(…, SIGURG)` storm vs `--- SIGCHLD + ---` interrupting `poll` — and `/proc//status` decodes each one in + hex. + +Next, **pidfds**: the pid-reuse race this supervisor still carries — a +recycled pid can send our forwarded SIGTERM to a stranger — and the file +descriptor that pins a process's identity for good. + +--- + +

verified — every number and +output excerpt above was produced on the Fedora 44 reference host this +session: the runner printed 12-signals-and-safe-handling PASS PASS +PASS (3 passed, 0 failed, 0 skipped) and a standalone +LSP_LANG=cpp run printed PASS 31 / FAIL 0; the +run/supervise transcripts (exit 3 → 3, SIGKILL → +137, restarts at 60/120 ms, reload pid 2625064 → 2625068) are unedited +session output; the ten-line C++ strace is the complete filtered trace +(signalfd4 = fd 3, zero rt_sigaction); the Go +supervisor pid made exactly 114 rt_sigaction calls (58 probes + +56 installs of 0x484d40) and, under GOGC=1, 4 +tgkill(…, SIGURG) sends with 4 matching deliveries; the Rust +trace showed 13 rt_sigaction, pipe2(O_CLOEXEC), +and ERESTART_RESTARTBLOCK on poll; and the +/proc/<pid>/status masks (SigBlk 14003 / +SigCgt 0, SigCgt fffffffd7fc1feff, +SigCgt 14443) were read from live supervisors and decoded with +the Python shown. The "On the lab VM" bpftrace callout is unverified as +marked and deferred to chapter 30.

diff --git a/_docs/13-pidfd-modern-process-mgmt.md b/_docs/13-pidfd-modern-process-mgmt.md new file mode 100644 index 0000000..2543143 --- /dev/null +++ b/_docs/13-pidfd-modern-process-mgmt.md @@ -0,0 +1,504 @@ +--- +title: "pidfd: modern process management" +order: 13 +part: "Processes, Signals, Privilege" +description: "The pid-reuse race constructed step by step, then closed for good: pidfd_open, clone3(CLONE_PIDFD), poll on a process, waitid(P_PIDFD), and pidfd_send_signal become pmon v2's default engine — with strace evidence of what the Go runtime and the Rust stdlib really do with pidfds today." +duration: "45 minutes" +--- + +`pmon` v1 ended the last chapter supervising a crashing child correctly: no +async handlers, SIGCHLD arriving as data through a signalfd, a `WNOHANG` reap +classifying every death. But it still carries a lie it inherited from 1970s +UNIX — it names its child by an *integer*, and integers get reused. Between +the moment a child dies and the moment you act on its pid, that number can +come to mean a completely different process, and `kill(2)` will cheerfully +signal the stranger. The one new idea in this chapter is that a process can +be a *file descriptor*: `pidfd_open(2)` hands you an fd that refers to the +process itself, not to its number — pollable in exactly the event-loop style +Chapter 9 built, reapable with `waitid(2)`, signalable with +`pidfd_send_signal(2)`, and immune to reuse by construction. `pmon` v2 makes +that its default engine and keeps the v1 sigchld engine beside it, +byte-identical in output, so you can strace the difference. + +The code is in `examples/13-pidfd-modern-process-mgmt/`. `./demo.sh` there +builds all three implementations and runs a short supervision demo; its +`README.md` specifies the CLI, the output contract, and the exit codes all +three languages share. + +{% include excalidraw.html + file="13-pid-reuse-race-pidfd" + alt="Two horizontal timeline lanes. Top lane, five numbered steps: spawn remembering pid 4242, child crashes leaving a zombie, a wait reaps it freeing the pid, the kernel recycles 4242 for an unrelated process, and kill 4242 delivers SIGTERM to the stranger, with an amber dashed arrow noting same integer different process. Bottom lane, four numbered steps: spawn plus pidfd_open pinning child 4242 to pidfd 4, the child exit turning the fd readable under poll, waitid P_PIDFD reaping exactly the process behind the fd, and pidfd_send_signal returning ESRCH after death instead of hitting a recycled pid." + caption="Figure 13.1 — the pid-reuse race in five numbered steps, and the pidfd lane that closes it; the fdinfo and strace annotations are from this chapter's real runs" %} + +> **Tools used** — `strace`, `pgrep`, `man`, `ls`/`cat` over `/proc`, +> `gcc`, `rustc` (host); `exitsnoop`/`killsnoop` bcc-tools (lab VM, exercised +> in Part 8). Everything here is checked by `scripts/check-host.sh`, ships +> with Fedora, or is preinstalled in the lab VMs. + +## The pid-reuse race, in five steps + +Build the failure concretely, because every step is ordinary. (1) You spawn +a worker and remember its pid — say 4242. A pid is an index into a kernel +table, nothing more; on this host the table wraps at +`/proc/sys/kernel/pid_max` = 4194304. (2) The worker crashes. It becomes a +zombie: the pid stays reserved *only* until someone collects the exit +status. (3) Someone does — and here is the trap: in any program bigger than +a toy, "someone" is not always you. A library thread that spawns helpers, a +language runtime that reaps everything (Chapter 11 showed Go's runtime +holding exactly this power), a signal handler someone else installed — any +`wait()` in the process can free the pid. (4) The kernel recycles 4242 for +an unrelated process. Under default settings that needs the counter to wrap, +but a busy build server wraps in hours, and a container with a small +`pid_max` wraps in minutes. (5) You finally act on your saved integer — +`kill(4242, SIGTERM)` — and the signal lands on the stranger. Steps 3→5 are +the window in Figure 13.1; nothing you hold distinguishes old 4242 from new +4242, because an integer carries no identity. + +One subtlety makes supervisors *seem* safe and is worth stating precisely: a +zombie's pid cannot be recycled, so as long as *you* are the parent and +*you* have not reaped yet, your pid is still good. The race needs a reap you +don't control — which is exactly what runtimes, PAM modules, and "helpful" +libraries do. And any pid-based operation on a process you are *not* the +parent of (a `kill` from a shell script, a `/proc/` poke from a +monitoring agent) has the race with no mitigation at all. + +## Four syscalls that make a process a file + +The fix arrived in a burst across Linux 5.1–5.4, and this host's manpages +(kernel `7.1.3-200.fc44`) document each piece. `pidfd_open(2)` — HISTORY: +"Linux 5.3" — "creates a file descriptor that refers to the process whose +PID is specified in pid"; the close-on-exec flag is set automatically, so +the Chapter 7 discipline costs nothing here. The fd's superpower is stated +under NOTES: it "can be monitored using poll(2), select(2), and epoll(7). +When the process that it refers to terminates, these interfaces indicate the +file descriptor as readable" — a process exit becomes an ordinary readiness +event, composable with every other fd your Chapter 9 event loop owns. There +is nothing to actually read: `read(2)` on a pidfd fails with `EINVAL`. The +manpage also warns that an fd obtained by opening `/proc/` — the older +trick — "is not pollable and can't be waited on with waitid(2)". + +For your *own* children there is a spelling with no window at all: +`clone(2)`/`clone3(2)` with `CLONE_PIDFD` (since Linux 5.2) delivers the +pidfd atomically at spawn time, via `parent_tid` for `clone` or +`cl_args.pidfd` for `clone3` — the same "set the property at creation, not +after" move as `O_CLOEXEC` in Chapter 7. Acting through the fd: +`pidfd_send_signal(2)` (Linux 5.1) "sends the signal sig to the target +process referred to by pidfd", and `waitid(2)` grew an ID type +`P_PIDFD` (Linux 5.4) to "wait for the child referred to by the PID file +descriptor". Once the process behind the fd has been reaped, signalling it +yields `ESRCH` — a clean error instead of a delivered signal to a recycled +pid, which is the whole point of the bottom lane of Figure 13.1. + +One documentation trap, caught live: this host's `pidfd_open` manpage still +says "glibc provides no wrapper for pidfd_open(), necessitating the use of +syscall(2)" — but a one-line probe built with `gcc` against the installed +glibc 2.43 (`#include ` … `pidfd_open(1,0)`) compiles *and +links*. +Manpages lag libraries. The C++ example keeps the raw +`syscall(SYS_pidfd_open, …)` anyway so it builds against older glibc too, +and because seeing the syscall spelled out is the point of the chapter. + +## What the runtimes do today + +Claims about runtime internals rot fast, so here is only what `strace -f` +showed on *this* host, running *these* binaries. Go 1.26.5 is a pidfd +enthusiast: before its first spawn, the runtime probes the whole feature +set — `pidfd_open` on its own pid, a `waitid(P_PIDFD, …)` that must fail +`ECHILD`, a `pidfd_send_signal(fd, 0, NULL, 0)` no-op, and then a throwaway +no-op child cloned with `CLONE_PIDFD` and reaped through its fd. Every +subsequent `exec.Cmd` child is spawned as +`clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD, parent_tid=[6])` — the +runtime keeps its *own* pidfd for every child it starts, acquired atomically +at clone time, and `os.Process` documents using it for `Wait` and `Signal` +when available. Rust 1.97.1's stdlib, by contrast, spawns with a plain +`fork`-flavored `clone(CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)` and +issues no pidfd calls of its own; the integration exists +(`Command::create_pidfd`, `std::os::linux::process::PidFd`) but compiling a +probe against it on this stable toolchain fails with `E0658: use of unstable +library feature 'linux_pidfd'`, pointing at tracking issue #82971. That is +the verified line: Go's runtime rides pidfds invisibly; stable Rust makes +you call `pidfd_open` yourself (our example uses `rustix`); where either +lands next is a research note, not something this chapter will claim. + +## How the code works + +The structure is shared across the three implementations. A `ChildStatus` +(`{exited, value}`) is the reaped truth about one child; `report_exit` +formats it identically everywhere and computes the mirrored exit code +(`status` as-is, `128+signo` for a signal death). An `Engine` selects one of +two wait strategies behind one restart policy; an `Outcome` says which of +the two things a wait produced — a reaped status, or a stop request +(`signal` or `timeout`). C++ spells that as +`std::variant`, Rust as an `enum Outcome`, and Go — with +`select` as its native "whichever happens first" — doesn't need the type at +all. The C++ `Fd` owner is the same class Chapter 7 built; Rust gets +`OwnedFd` straight from `rustix::process::pidfd_open`, and Go holds the raw +integer from `unix.PidfdOpen` with an explicit `unix.Close`. + +Spawning carries one landmine the sigchld engine planted: the supervisor +blocks SIGINT/SIGTERM (and SIGCHLD, in the sigchld engine) process-wide so +its signalfd can deliver them as data — and a blocked mask is *inherited* +across fork and exec. A supervised `sleep` that inherits a blocked SIGTERM +can never be stopped. Each language defuses it differently: C++ +`posix_spawnp` with `POSIX_SPAWN_SETSIGMASK` resets the child's mask +declaratively; Rust clears it in a `pre_exec` closure (`sigprocmask` is +async-signal-safe, so the closure is legal); Go's runtime resets the mask +for `exec.Cmd` children on its own. + +The pidfd engine's wait is where the chapter's idea becomes code: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +{% raw %}// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +[[nodiscard]] std::expected reap_pidfd(const Fd& pidfd) { + siginfo_t si{}; + while (::waitid(P_PIDFD, static_cast(pidfd.get()), &si, WEXITED) != 0) { + if (errno != EINTR) { + return std::unexpected(last_error()); + } + } + return status_from(si); +} + +// pidfd engine: the child's exit is just readability on a file descriptor. +[[nodiscard]] std::expected +wait_pidfd(const Fd& pidfd, const Fd& sigfd, steady_clock::time_point deadline) { + for (;;) { + std::array fds{{ + {.fd = pidfd.get(), .events = POLLIN, .revents = 0}, + {.fd = sigfd.get(), .events = POLLIN, .revents = 0}, + }}; + const int n = ::poll(fds.data(), fds.size(), remaining_ms(deadline)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(last_error()); + } + if (n == 0) { + return Stop::timeout; + } + if ((fds[1].revents & POLLIN) != 0) { + signalfd_siginfo info{}; + std::ignore = ::read(sigfd.get(), &info, sizeof info); + return Stop::signal; + } + if (fds[0].revents != 0) { + return reap_pidfd(pidfd).transform([](ChildStatus st) { return Outcome{st}; }); + } + } +}{% endraw %} +``` + +```go +{% raw %} if engine == "pidfd" { + pidfd, err := unix.PidfdOpen(pid, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: pidfd_open: %v\n", err) + return 1 + } + fmt.Fprintf(os.Stderr, "pmon: engine=pidfd child=%d pidfd=%d\n", pid, pidfd) + exited := make(chan struct{}) + go func() { // the pidfd becomes readable exactly when the child exits + fds := []unix.PollFd{{Fd: int32(pidfd), Events: unix.POLLIN}} + for { + if _, err := unix.Poll(fds, -1); err == unix.EINTR { + continue + } + close(exited) + return + } + }() + select { + case <-exited: + case <-stop: + stopped = "signal" + case <-deadline: + stopped = "timeout" + } + if stopped != "" { + _ = unix.PidfdSendSignal(pidfd, unix.SIGTERM, nil, 0) + <-exited + } + st, err = reapPidfd(pidfd) + unix.Close(pidfd) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + }{% endraw %} +``` + +```rust +/// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +fn reap_pidfd(pidfd: &impl AsFd) -> Result { + let status = waitid(WaitId::PidFd(pidfd.as_fd()), WaitIdOptions::EXITED) + .context("waitid(P_PIDFD)")? + .context("waitid(P_PIDFD): no status")?; + Ok(status_from_waitid(status)) +} + +/// pidfd engine: the child's exit is just readability on a file descriptor. +fn wait_pidfd(pidfd: &impl AsFd, sigfd: &SignalFd, deadline: Instant) -> Result { + loop { + let mut fds = [ + PollFd::new(pidfd, PollFlags::IN), + PollFd::new(sigfd, PollFlags::IN), + ]; + let n = match poll(&mut fds, Some(&remaining(deadline))) { + Err(rustix::io::Errno::INTR) => continue, + other => other.context("poll")?, + }; + if n == 0 { + return Ok(Outcome::Stopped(Stop::Timeout)); + } + if fds[1].revents().contains(PollFlags::IN) { + let _ = sigfd.read_signal(); + return Ok(Outcome::Stopped(Stop::Signal)); + } + if !fds[0].revents().is_empty() { + return Ok(Outcome::Reaped(reap_pidfd(pidfd)?)); + } + } +} +``` + +Walk the C++ and Rust versions first, because they are the same function. +`poll` watches two fds — the pidfd and the signalfd — with a timeout derived +from the supervision deadline. `remaining_ms`/`remaining` recompute the +budget on every iteration precisely because `poll` can return early +(`EINTR`, a signalfd wakeup that turns out to be SIGCHLD-free); a fixed +timeout would stretch the deadline every retry. A return of 0 is the +deadline itself. The signalfd is checked *before* the pidfd deliberately: if +the operator's Ctrl-C and the child's exit arrive together, we prefer to +report a stop — the reap still happens on the stop path, so nothing is lost. +Only then does readability on `fds[0]` mean "the child terminated", and +`reap_pidfd` collects the status with `waitid(P_PIDFD, fd, …, WEXITED)` — +naming the child *by the fd*, so even in a process where some library reaps +promiscuously, this call can never collect a stranger. `status_from` +classifies `si_code == CLD_EXITED` versus killed/dumped, the same siginfo +plumbing as Chapter 11. + +Go cannot put a channel in a `poll` set, so it inverts the bridge: one +goroutine parks in `unix.Poll` on the pidfd alone, infinite timeout, and +`close(exited)` converts readability into channel-readability. The `select` +then merges three event sources — exit, `signal.Notify` delivery, deadline — +exactly as the C++/Rust `poll` merged two fds and a timeout. On a stop, the +`<-exited` after `PidfdSendSignal` is load-bearing twice over: it waits for +the child to actually die (the pidfd goes readable), and it guarantees the +goroutine has finished touching `pidfd` before `unix.Close(pidfd)` runs — +close an fd a goroutine is still polling and the slot number could be +recycled mid-poll, which is Chapter 7's double-close lesson wearing a +concurrency hat. + +The stop path is the syscall the diagram promised: +`pidfd_send_signal(pidfd, SIGTERM, …)` in all three languages, result +deliberately ignored. If the child died between the deadline and the signal, +the target is a zombie (signal discarded, returns 0) or already reaped +(`ESRCH`) — both harmless, and `reap_pidfd` right after collects the truth +either way. The restart policy above all this is unchanged from v1: a clean +`status=0` ends supervision, anything else burns one of `--max-restarts`, +and the counter lines (`restart 1/2` … `giving up after 2 restarts`) are +byte-identical across engines and languages — `verify.lua` literally diffs +the sequences against each other. + +One fragile bit, documented rather than hidden: `pidfd_open` after spawn has +a theoretical window that `CLONE_PIDFD` at clone time would close. It is +safe *here* because `pmon` is the parent and reaps only through the fd, so +the child's pid is pinned (zombie at worst) until after the pidfd exists — +but the moment you take pidfds of processes you did not spawn, insist on +verifying identity (the cross-check below shows `fdinfo`'s `Pid:` line) or +get the fd at clone time the way Go's runtime does. + +## Errors, three ways + +The retry policy from Chapter 5 lands on three calls here: `poll` and +`waitid` retry `EINTR` in a loop in C++ (`errno != EINTR` breaks), Go +(`errno == unix.EINTR { continue }` around `Syscall6`, and the poller +goroutine's `err == unix.EINTR`), and Rust +(`Err(rustix::io::Errno::INTR) => continue`). Expected failures are policy +at the call site: `pidfd_send_signal` on the stop path ignores its result +(zombie or `ESRCH`, both fine), and the sigchld engine treats a `WNOHANG` +reap that returns nobody as "coalesced SIGCHLD, keep waiting" — Chapter 12's +lesson encoded in all three languages. Everything unexpected carries context +up to one printer with the raw error preserved: C++ wraps `errno` in +`std::error_code` inside `std::expected`, Go wraps with `%w` so +`errors.Is`/`errors.As` still see the errno underneath, Rust chains +`.context("waitid(P_PIDFD)")` onto the `rustix` errno with `anyhow`. And one +error is a *feature test*: Go's runtime probe expects `waitid(P_PIDFD)` on +itself to fail `ECHILD` — using a precise errno as a yes/no answer about +kernel support, the same trick your own code can use on kernels older than +5.4. + +## Concurrency lens + +C++ and Rust run this whole chapter on one thread: one `poll`, two fds, no +shared state, no reentrancy — the safest concurrency is none. Go runs the +same logic as a small concurrent system: the runtime's own reaper machinery, +a poller goroutine per spawn, and a `select` merging channels. That shape +has two sharp edges the code steps around. First, fd lifetime across +goroutines — covered above; the `<-exited` handshake is what makes +`unix.Close` safe. Second, process-wide signal state: the blocked mask that +powers the C++/Rust signalfd is shared by *all* threads, which is why the +child-side mask reset (`POSIX_SPAWN_SETSIGMASK`, `pre_exec`) is +non-negotiable — any thread may be the one that spawns. Note also what the +strace shows about fd-table behavior under concurrency: every spawn prints +`pidfd=4` because the previous pidfd was closed before the next +`pidfd_open`, and Chapter 7's lowest-free-slot rule hands the number back — +while in Go the runtime's *own* pidfd for the same child sits one slot over. +Two fds, one process, no interference: identity multiplied is still +identity. + +## Build, run, observe + +```bash +[host]$ cd examples/13-pidfd-modern-process-mgmt && ./demo.sh +``` + +Each language builds and supervises a short-lived child. The chapter's two +canonical runs, from this host — a crash loop that exhausts its restart +budget: + +```bash +[host]$ ./demo.sh cpp run supervise --max-restarts 2 -- sh -c 'exit 1' +pmon: engine=pidfd child=2621527 pidfd=4 +pmon: child=2621527 exited status=1 +pmon: restart 1/2 +pmon: engine=pidfd child=2621528 pidfd=4 +pmon: child=2621528 exited status=1 +pmon: restart 2/2 +pmon: engine=pidfd child=2621529 pidfd=4 +pmon: child=2621529 exited status=1 +pmon: giving up after 2 restarts +``` + +— exit code 1, three spawns, `pidfd=4` every time. And a timeout stop that +must not leak the child: + +```bash +[host]$ ./demo.sh go run supervise --timeout-ms 400 -- sleep 9.99 +pmon: engine=pidfd child=2621961 pidfd=4 +pmon: child=2621961 killed signal=15 +pmon: exiting (timeout) +[host]$ pgrep -af 'sleep 9[.]99'; echo "pgrep exit=$?" +pgrep exit=1 +``` + +The `pgrep` exit 1 is the observable that matters: no supervised `sleep` +outlived its supervisor. (The `[.]` keeps `pgrep -f` from matching its own +command line — a pid-namespace-sized footgun in miniature.) The Rust binary +produces the same crash-loop shape (children 2622026–2622028 on this host), +and the runner — `python3 scripts/test-all-examples.py --only +13-pidfd-modern-process-mgmt` — reports `PASS PASS PASS` with `verify.lua`'s +behavioral checks at `PASS 37 / FAIL 0` per language. + +## Cross-check, three ways + +**The syscalls, under `strace`.** Filtering each engine to the calls this +chapter claims it makes. C++ (glibc's `posix_spawnp` arrives as `clone3`): + +```bash +[host]$ strace -f -e trace=pidfd_open,clone3,waitid,poll \ + ./cpp/build/release/app supervise --max-restarts 0 -- sh -c 'exit 1' +clone3({flags=CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND, exit_signal=SIGCHLD, ...}, 88) = 2622912 +pidfd_open(2622912, 0) = 4 +poll([{fd=4, events=POLLIN}, {fd=3, events=POLLIN}], 2, 9999) = 1 ([{fd=4, revents=POLLIN}]) +waitid(P_PIDFD, 4, {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2622912, si_uid=1000, si_status=1, ...}, WEXITED, NULL) = 0 +``` + +The stop path adds the last syscall of the set — +`poll(..., 299) = 0 (Timeout)`, then `pidfd_send_signal(4, SIGTERM, NULL, 0) += 0`, then the same `waitid(P_PIDFD, 4, …)` now reporting `CLD_KILLED` with +`si_status=SIGTERM`. Go shows the runtime's pidfd life underneath ours +(trimmed to the interesting lines, thread pids left in): + +``` +2623684 pidfd_open(2623681, 0) = 4 ← probe: pidfd of itself +2623684 waitid(P_PIDFD, 4, ...) = -1 ECHILD ← probe: waitid support +2623684 pidfd_send_signal(4, 0, NULL, 0) = 0 ← probe: signal 0 +2623684 clone(child_stack=NULL, flags=CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD, parent_tid=[6]) = 2623689 +2623684 pidfd_open(2623689, 0) = 4 ← ours, via unix.PidfdOpen +2623684 waitid(P_PIDFD, 4, {..., si_pid=2623689, si_status=1, ...}, WEXITED, NULL) = 0 +``` + +`parent_tid=[6]` is the runtime's own pidfd for the child, delivered +atomically by `CLONE_PIDFD`; fd 4 is ours. Rust spawns with a plain +`clone(CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD)` — no stdlib pidfd — +followed by our `pidfd_open(2623708, 0) = 4` and the identical +`waitid(P_PIDFD, …)`. Three runtimes, three spawn spellings, one kernel API. + +**The fd itself, in `/proc`.** Pause a live supervision +(`supervise --timeout-ms 8000 -- sleep 6.66`, supervisor pid 2624388) and +read the fd table the way Chapter 7 taught: + +```bash +[host]$ ls -l /proc/2624388/fd | tail -2 +lrwx------ 1 rsedor rsedor 64 Jul 18 23:59 3 -> anon_inode:[signalfd] +lrwx------ 1 rsedor rsedor 64 Jul 18 23:59 4 -> anon_inode:[pidfd] +[host]$ cat /proc/2624388/fdinfo/4 +pos: 0 +flags: 02000002 +mnt_id: 5 +ino: 2596278 +Pid: 2624390 +NSpid: 2624390 +``` + +The `Pid: 2624390` line is the payoff quoted directly from the kernel: the +open file description *itself* records which process it refers to, and it +matches the `child=2624390` that `pmon` announced. `flags: 02000002` is +`O_RDWR|O_CLOEXEC` — the close-on-exec bit the manpage promised, set with no +code on our side. (`NSpid` previews Part 10: inside a pid namespace the same +process has a different number, but the same pidfd.) + +> **On the lab VM** unverified — +> the fleet-wide view of this chapter is bcc-tools: `exitsnoop` timestamping +> every process exit and `killsnoop` logging who signalled whom, which would +> show `pmon`'s SIGTERM (and would *not* show a pid-reuse mishit, because +> there is none). bcc-tools need root and kernel headers, so they are not run +> on this host; Chapter 30 in the Debugging part exercises them on the +> `systems-target` VM. + +## What you learned + +- A pid is an index that gets recycled; the reuse race is five ordinary + steps, and the dangerous reap in step 3 is often performed by a runtime or + library you don't control. +- `pidfd_open` (or `CLONE_PIDFD` at clone time) pins process identity to an + fd: exit becomes `poll` readability — Chapter 9's event-loop model now + covers processes — `waitid(P_PIDFD)` reaps exactly that process, and + `pidfd_send_signal` yields `ESRCH` rather than signalling a stranger; + `/proc//fdinfo/`'s `Pid:` line names the pinned process. +- The runtimes diverge today, verified by strace on this host: Go 1.26 + probes and adopts `CLONE_PIDFD` for every child it spawns, while stable + Rust 1.97 spawns with fork-style `clone` and keeps its pidfd API behind + the unstable `linux_pidfd` feature — so our Rust and C++ engines call the + syscalls themselves. +- Signal masks are inherited across exec: a supervisor that blocks signals + for a signalfd must reset the child's mask (`POSIX_SPAWN_SETSIGMASK`, + `pre_exec`) or its children become unstoppable. + +Next, **identity and privilege**: who a process *is* — real, effective, and +saved uids, capability sets, and how `pmon` drops root without dropping the +ability to do its job. + +--- + +

verified — every number and +output excerpt above was produced on the Fedora 44 reference host +(kernel 7.1.3-200.fc44) this session: the runner printed +13-pidfd-modern-process-mgmt PASS PASS PASS (3 passed, 0 +failed, 0 skipped) and a direct verify.lua run showed +PASS 37 / FAIL 0; the crash-loop and timeout transcripts are +real output (children 2621527–2621529, 2622026–2622028, 2621961), with the +no-orphan pgrep exit 1 reproduced; the strace excerpts show the +real clone3/clone(CLONE_PIDFD)/fork-style spawn +paths, Go's four-step runtime probe, pidfd_open returning 4, +poll readiness, waitid(P_PIDFD) siginfo, and +pidfd_send_signal on the stop path; the live +fdinfo read out Pid: 2624390 matching the +announced child; the glibc-wrapper probe compiled and linked against glibc +2.43, and the create_pidfd probe failed with E0658 on rustc +1.97.1. Manpage quotes are from this host's pages. The bcc-tools callout is +unverified as marked and deferred to Part 8.

diff --git a/_docs/14-identity-and-privilege.md b/_docs/14-identity-and-privilege.md new file mode 100644 index 0000000..f691277 --- /dev/null +++ b/_docs/14-identity-and-privilege.md @@ -0,0 +1,633 @@ +--- +title: "Identity and privilege" +order: 14 +part: "Processes, Signals, Privilege" +description: "Real, effective, and saved uids as one state machine that setresuid finally makes explicit; umask and the file bits; then capabilities done properly — file sets vs process sets, why ambient exists, and the KEEPCAPS ordering trap — proven live as pmon v3 binds :80 at uid 65534 carrying exactly one capability." +duration: "50 minutes" +--- + +`pmon` can spawn a child, shepherd it through signals, and supervise it +race-free through a pidfd. Version 3 faces the question every real supervisor +meets next: it starts as root — it has to, to become another user — and +everything it launches must *not* be root. The one new idea is that identity +on Linux is not one number but a handful of small state machines — three uids +and five capability sets — and the transitions between them are +order-sensitive. The same five syscalls in the wrong order either leave the +child privileged (you proved nothing) or strip the one capability it needed +(the child cannot do its job). `pmon drop` performs the transition correctly, +carrying exactly `CAP_NET_BIND_SERVICE` across a full identity change and an +`execve(2)`; `pmon bindprobe` is the witness, printing one line that is only +possible if the drop worked: an unprivileged uid bound to port 80. + +The code is in `examples/14-identity-and-privilege/`. This is a **VM +example** — the drop path needs real root, so `./demo.sh run` deploys +to the `systems-target` guest when `TARGET` is set; the `README.md` there +specifies the CLI, the exit-code contract, and the paired +positive/negative verification. + +{% include excalidraw.html + file="14-credential-sets-caps" + alt="Two bands. The top band shows the three uids — real, effective, saved — each moving 0 to 65534, with a note that setresuid moves all three at once and the saved uid is the way back that the drop deliberately destroys. The bottom band shows pmon's kept-cap path as six boxes: pmon as root with full permitted and effective sets, prctl PR_SET_KEEPCAPS, setresuid to 65534 with permitted kept, capset pinning 0x400 into permitted inheritable and effective, prctl AMBIENT_RAISE putting 0x400 into ambient, and execve into bindprobe bound to port 80 at uid 65534. A dashed negative-control row underneath skips the prctl and capset steps: setresuid scrubs permitted to zero and execve reaches bindprobe with every set zero and bind denied, exit 3. Notes carry the real /proc status values: the kept-cap child reads 0000000000000400 in CapInh CapPrm CapEff CapAmb with CapBnd 000001ffffffffff." + caption="Figure 14.1 — the three-uid state machine and the five capability sets across pmon's drop; every hex value comes from /proc/<pid>/status on systems-target, read during the cross-check below" %} + +> **Tools used** — `ssh`/`scp` and the Python runner +> (`scripts/test-all-examples.py`) on the host; `umask`, `touch`, `ls` +> (host); `getpcaps`, `capsh`, `getcap`, `sysctl`, `pgrep`, `bpftrace` +> (systems-target VM). Everything here is checked by +> `scripts/check-host.sh`, ships with Fedora's `libcap`, or is preinstalled +> in the lab VMs by cloud-init. + +## Three uids, one state machine + +Every process carries three user ids, and `/proc` shows them side by side — +this is the dropped child from the cross-check later in the chapter: + +```bash +[vm]$ grep Uid /proc/8623/status +Uid: 65534 65534 65534 65534 +``` + +Left to right: **real**, **effective**, **saved**, and filesystem uid (a +Linux relic that shadows the effective uid; NFS servers use it, nothing in +this book will). The *real* uid is who you are for accounting and for who +may signal you. The *effective* uid is the one that matters: every +permission check the kernel makes reads it. The *saved* uid is the escape +hatch — it exists so a set-uid program can drop its effective uid to do +unprivileged work and later climb back to the saved copy. + +That escape hatch is also the source of thirty years of confusion, because +the classic calls mutate the machine implicitly. `setuid(2)` sets all three +ids if you are privileged but only the effective uid if you are not — the +same call is either a permanent drop or a temporary one depending on who +runs it. `seteuid(2)` moves only the middle value. `setreuid(2)` updates +the saved uid as a *side effect*, under rules baroque enough that the +manual page documents them as conditions. Daemons of the 1990s performed a +"setuid dance" across these calls and regularly got it wrong in one +direction — the direction where root comes back. + +`setresuid(2)` ends the dance by making the state machine explicit: you +name all three values in one call (`-1` means leave that one alone), the +kernel applies them atomically, and the call either fully succeeds or +changes nothing. `pmon` calls `setresuid(uid, uid, uid)` — all three to +65534 — which is deliberately irreversible: with no root left in *any* of +the three slots, there is no privileged state left to return to, and a +later `setresuid(0,0,0)` fails. One ordering rule already applies here: +groups first. `setgroups(2)` and `setresgid(2)` are themselves privileged +operations, so they must run while the effective uid is still 0 — drop the +uid first and you are locked out of fixing the gids, leaving the child in +root's groups. + +## umask and the file bits, quickly + +File permission bits are the *other* identity mechanism, and one detail of +them belongs in this chapter because it silently edits every file you +create: the umask. A `creat`/`open(O_CREAT)` mode is filtered as +`mode & ~umask` before it ever reaches the inode. On the reference host: + +```bash +[host]$ umask +022 +[host]$ touch umask-demo.txt && ls -l umask-demo.txt +-rw-r--r-- 1 rsedor rsedor 0 Jul 18 23:56 umask-demo.txt +[host]$ (umask 077 && touch umask-demo-077.txt && ls -l umask-demo-077.txt) +-rw------- 1 rsedor rsedor 0 Jul 18 23:56 umask-demo-077.txt +``` + +Same `touch`, same requested 0666: umask 022 clears group/other write +(0644), umask 077 clears everything but owner (0600). The umask is +inherited across `fork` and `execve` like the uids are, which is why the +verify script `chmod 755`s the binary it stages on the guest instead of +trusting whatever mode survived `scp` — permissions you did not state +explicitly are permissions someone else chose. And mode bits are exactly +why this chapter stages that copy in `/tmp` at all: `/home/fedora` is mode +`700`, so once `pmon` becomes `nobody`, a binary under it is unreachable — +the drop is real enough to lock the child out of its own image. + +## Capabilities: five process sets, three file sets + +Root's power is not one bit; since Linux 2.2 it is ~41 distinct +**capabilities** — `CAP_NET_BIND_SERVICE` is the one that lets a process +bind ports below `net.ipv4.ip_unprivileged_port_start` (1024 on the guest, +confirmed below). Each *process* carries five sets, visible as the `Cap*` +lines of `/proc//status`: + +- **Permitted** — the ceiling: capabilities the process may use or raise. +- **Effective** — the subset the kernel actually consults in checks. +- **Inheritable** — offered across `execve`, but only into binaries whose + *file* inheritable set accepts them. +- **Bounding** — a mask over what `execve` can ever grant. On the guest it + reads `000001ffffffffff` for every process, root or not: it masks, it + never grants. +- **Ambient** — the modern one, added in Linux 4.3: capabilities that + survive an `execve` by a non-root process into a plain, uncapped binary. + +Each *file* can carry three pieces in its `security.capability` xattr +(permitted, inheritable, and a one-bit effective flag — set with +`setcap(8)`, read with `getcap(8)`), and `execve` combines the two sides: +the new permitted set is the file's permitted masked by bounding, plus the +intersection of the process and file inheritable sets, plus ambient. + +Why does ambient exist? Because the inheritable set is a trap. Before 4.3, +a supervisor that wanted to pass one capability to an unprivileged child +had to intersect its inheritable set with the *file's* inheritable set — +and ordinary binaries have no file capabilities at all, so the +intersection was empty and the capability died at `execve`. Your options +were `setcap` on every binary you might run (a fleet-management problem +and useless for scripts) or keeping root. Ambient is the bridge: a +capability raised into the ambient set — legal only while it is in both +permitted and inheritable — flows through a non-root `execve` of an +uncapped binary intact. The cross-check proves the child's capability came +this way and not from the file: `getcap` on the staged binary prints +nothing. + +Root barely notices this machinery, and that is by design: at `execve`, +a uid-0 process is treated as if the file granted everything, which is why +a root shell on the guest shows `CapPrm` and `CapEff` of +`000001ffffffffff` with `CapInh` and `CapAmb` both zero. That empty +inheritable set matters in the next section. + +## The ordering trap + +The whole lesson of `pmon drop` is that five correct syscalls in the wrong +order fail silently. The correct sequence, as the C++ header comment +states it: + +1. `prctl(PR_SET_KEEPCAPS, 1)` — **before** the uid change. When a + process's uids go from 0 to all-nonzero, the kernel scrubs the + permitted and effective sets; `KEEPCAPS` preserves permitted (effective + is cleared regardless). +2. `setgroups` / `setresgid` / `setresuid` — become the target user, + groups and gid while still privileged. +3. `capset(2)` — pin `CAP_NET_BIND_SERVICE` into permitted **and** + inheritable, re-raise it into effective (cleared by step 2), and drop + every other capability KEEPCAPS retained. +4. `prctl(PR_CAP_AMBIENT_RAISE, CAP_NET_BIND_SERVICE)` — legal only + because step 3 put the cap in both permitted and inheritable. +5. `execvp(3)` — the ambient set is the only one that survives this for a + non-root process running an uncapped binary. + +Each step is a fence the others depend on. Skip step 1 and step 2 scrubs +permitted — steps 3 and 4 then have nothing to work with. Run step 3 +before step 2 and you have dropped `CAP_SETUID`/`CAP_SETGID`, so the uid +change itself fails. Try step 4 without step 3 and `prctl` returns `EPERM` +because root's inheritable set is empty — the raise requires permitted ∩ +inheritable. Skip step 4 and everything is technically held right up to +`execve`, where permitted and effective are recomputed from file caps the +binary does not have, and the capability evaporates. The negative control +run is exactly this failure family made observable: without `--keep-cap`, +steps 1, 3, and 4 never happen and the child arrives with every set zero. + +## How the code works + +The error plumbing comes first because every step uses it. C++ and Rust +declare the same two-field failure record — `SysErr { what, errno }` — and +a `checked(rc, what)` adapter that turns a negative syscall return into +`std::expected` / `Result` carrying the captured errno. All five steps +thread through it with `?`-style propagation, and exactly one printer at +the top formats `drop: : `. There is no `perror` scattered +mid-sequence: a failed step must abort the whole drop, because continuing +past, say, a failed `setresuid` would `execvp` a root child. + +The heart is `arrange_and_exec`, and the three languages split into two +philosophies: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +// Perform the identity change (and optional cap retention), then execvp CMD. +// Returns only on failure; on success control never comes back. +std::expected arrange_and_exec(const passwd& pw, bool keep_cap, + std::span cmd) { + // (1) Retain the permitted set across the coming uid change. Must precede + // setresuid — after it, it is too late. + if (keep_cap) { + if (auto r = checked(::prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), "PR_SET_KEEPCAPS"); + !r) + return r; + } + + // (2) Become the target user: groups first, gid before uid — once uid drops + // we can no longer change gids. + gid_t gid = pw.pw_gid; + if (auto r = checked(::setgroups(1, &gid), "setgroups"); !r) return r; + if (auto r = checked(::setresgid(gid, gid, gid), "setresgid"); !r) return r; + if (auto r = checked(::setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid), "setresuid"); + !r) + return r; + + // (3) Pin exactly CAP_NET_BIND_SERVICE into permitted+inheritable (ambient + // demands the cap live in both), dropping every other retained cap. + if (keep_cap) { + __user_cap_header_struct hdr{}; + hdr.version = _LINUX_CAPABILITY_VERSION_3; + hdr.pid = 0; + std::array<__user_cap_data_struct, 2> data{}; + const std::uint32_t bit = 1u << CAP_NET_BIND_SERVICE; // cap 10, word 0 + data[0].effective = bit; + data[0].permitted = bit; + data[0].inheritable = bit; + if (auto r = checked(::syscall(SYS_capset, &hdr, data.data()), "capset"); !r) + return r; + + // (4) Raise it into the ambient set — the only set an unprivileged execve + // of a file without file-caps carries forward. + if (auto r = checked(::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, + CAP_NET_BIND_SERVICE, 0, 0), + "PR_CAP_AMBIENT_RAISE"); + !r) + return r; + } + + // (5) Hand off. CMD now runs as , keeping :80 only if we raised it. + std::vector args(cmd.begin(), cmd.end()); + args.push_back(nullptr); + ::execvp(args[0], args.data()); + return std::unexpected(SysErr{std::string("execvp ") + args[0], errno}); +} +``` + +```go + u, err := user.Lookup(name) + if err != nil { + fmt.Fprintf(os.Stderr, "drop: unknown user: %s\n", name) + return 1 + } + uid, _ := strconv.Atoi(u.Uid) + gid, _ := strconv.Atoi(u.Gid) + + c := exec.Command(cmd[0], cmd[1:]...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + // The runtime's fork/exec child applies these in the kernel-correct order: + // PR_SET_KEEPCAPS, setgroups/setgid, setuid, capset, PR_CAP_AMBIENT_RAISE. + c.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, + } + if keepCap { + c.SysProcAttr.AmbientCaps = []uintptr{unix.CAP_NET_BIND_SERVICE} + } + + if err := c.Run(); err != nil { + var ee *exec.ExitError + if errors.As(err, &ee) { + return ee.ExitCode() // propagate CMD's own exit status (e.g. 3) + } + fmt.Fprintf(os.Stderr, "drop: %v\n", err) + return 1 + } + return 0 +``` + +```rust +#[repr(C)] +struct CapHeader { + version: u32, + pid: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct CapData { + effective: u32, + permitted: u32, + inheritable: u32, +} + +/// Drop to `uid`/`gid`, optionally retaining CAP_NET_BIND_SERVICE, then exec. +/// Returns only on failure; on success control never comes back. +fn arrange_and_exec( + uid: libc::uid_t, + gid: libc::gid_t, + keep_cap: bool, + cmd: &[String], +) -> Result<(), SysErr> { + // (1) Retain the permitted set across the uid change (must precede it). + if keep_cap { + check( + unsafe { libc::prctl(libc::PR_SET_KEEPCAPS, 1, 0, 0, 0) } as i64, + "PR_SET_KEEPCAPS", + )?; + } + + // (2) Become the target user: groups, then gid, then uid. + check(unsafe { libc::setgroups(1, &gid) } as i64, "setgroups")?; + check( + unsafe { libc::setresgid(gid, gid, gid) } as i64, + "setresgid", + )?; + check( + unsafe { libc::setresuid(uid, uid, uid) } as i64, + "setresuid", + )?; + + // (3) Pin exactly CAP_NET_BIND_SERVICE into permitted+inheritable. + if keep_cap { + let hdr = CapHeader { + version: LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let bit = 1u32 << CAP_NET_BIND_SERVICE; // cap 10, word 0 + let data = [ + CapData { + effective: bit, + permitted: bit, + inheritable: bit, + }, + CapData { + effective: 0, + permitted: 0, + inheritable: 0, + }, + ]; + check( + unsafe { libc::syscall(libc::SYS_capset, &hdr as *const CapHeader, data.as_ptr()) }, + "capset", + )?; + + // (4) Raise it into the ambient set. + check( + unsafe { + libc::prctl( + libc::PR_CAP_AMBIENT, + libc::PR_CAP_AMBIENT_RAISE as libc::c_ulong, + CAP_NET_BIND_SERVICE as libc::c_ulong, + 0, + 0, + ) + } as i64, + "PR_CAP_AMBIENT_RAISE", + )?; + } + + // (5) Hand off to CMD. + let prog = CString::new(cmd[0].as_str()).map_err(|_| SysErr { + what: "bad CMD".into(), + errno: libc::EINVAL, + })?; + let cargs: Vec = cmd + .iter() + .map(|s| CString::new(s.as_str()).unwrap_or_default()) + .collect(); + let mut ptrs: Vec<*const c_char> = cargs.iter().map(|c| c.as_ptr()).collect(); + ptrs.push(std::ptr::null()); + unsafe { libc::execvp(prog.as_ptr(), ptrs.as_ptr()) }; + Err(SysErr { + what: format!("execvp {}", cmd[0]), + errno: last_errno(), + }) +} +``` + +C++ and Rust do the dance **in-process** with raw syscalls — no libcap on +purpose, so every step is visible. The `capset` ABI deserves a close look +because glibc does not even wrap it (the "public" API is libcap's; we call +`syscall(SYS_capset, …)` directly). The header names ABI version 3 +(`0x20080522`) and pid 0, meaning "me"; version 3 represents the 64-bit +capability space as **two 32-bit words**, which is why `data` is a +two-element array. `CAP_NET_BIND_SERVICE` is capability number 10, so its +bit lives in word 0 and word 1 is deliberately all zero — writing zeros +there is what drops the *other* nine retained-by-KEEPCAPS capabilities +above 31. Setting `effective = bit` in the same call does double duty: the +uid change cleared the effective set, and this is what re-raises it. Rust +mirrors the C++ byte for byte, down to `#[repr(C)]` on the two structs so +their layout matches the kernel's, and defines the two constants the +`libc` crate omits (`LINUX_CAPABILITY_VERSION_3`, `CAP_NET_BIND_SERVICE`) +locally — both are stable kernel ABI. + +Go is the deliberate inversion, and the reason is a rule worth memorizing: +**credentials are per-thread at the kernel level**, and a Go program is +multithreaded before `main` runs. Instead of attempting the dance +in-process, the Go build describes the drop declaratively — +`SysProcAttr.Credential` for step 2, `AmbientCaps` for steps 1, 3, and 4 — +and the runtime's fork/exec child, where exactly one thread exists, +replays the same kernel-correct order between `fork` and `exec`. Same +observable behavior; the ordering lives in the standard library instead of +in our code. The `ExitError` branch matters too: `drop` promises to +propagate CMD's own exit status, which is how the negative control's +exit 3 travels back through `pmon` to the verify script intact. + +`bindprobe` is short by design: socket, `SO_REUSEADDR` (so rapid repeat +runs do not trip over `TIME_WAIT`), bind on `INADDR_ANY`, then one line — +`bindprobe: uid= bound :` — and exit 0, or the bind failure with +`strerror` text on stderr and exit 3. It prints `getuid()` precisely so +one line carries both facts the verification needs: *who* the process was +and *what* it was allowed to do. + +Fragile bits, stated plainly: `--keep-cap` accepts only +`net_bind_service` — the cap number and its word-0 position are hardcoded, +and generalizing to caps ≥ 32 means touching word 1; the staged binary +must live somewhere the dropped user can read (`/tmp`, not `/home/fedora`); +and `pmon` does not call `setsid` or close inherited fds — real daemons +layer those on, and chapter 7's `O_CLOEXEC` discipline is half of why they +can skip the fd audit. + +## Errors, three ways + +The output contract is byte-identical diagnostics across languages, and +capabilities add a twist: the interesting failure happens in the *child*, +after `execve`, where `pmon`'s error plumbing no longer exists. So the +contract splits in two. Setup failures — not root, unknown user, a failed +step — flow through `SysErr`: C++ formats `std::expected` errors with +`std::strerror`, Rust calls `libc::strerror` through a safe wrapper, and +both exit 1 from a single printer. The child's bind failure is reported by +`bindprobe` itself with exit 3, and Go's `drop` forwards that code via +`errors.As(err, &ee)` → `ee.ExitCode()`. The subtle one is Go's +`errnoText`: Go spells errno strings lower-case (`permission denied`) +while `strerror(3)` capitalizes (`Permission denied`), so the Go build +re-capitalizes the first letter — without it, `verify.lua`'s +byte-for-byte match on `bindprobe: bind :80: Permission denied` would fail +in one language out of three. Usage errors exit 2 everywhere, before any +privileged call is attempted. + +## Concurrency lens + +`setresuid(2)` and `capset(2)` change the credentials of the **calling +thread**, not the process. Single-threaded C and C-like programs never +notice; glibc papers over it for threaded ones with its setxid machinery, +signaling every thread to run the same `setresuid` so the process moves in +lockstep. But `pmon`'s C++ and Rust builds bypass glibc for `capset` +(raw `syscall`), so their safety argument is structural instead: both are +**deliberately single-threaded** — no thread pool, no async runtime — +because a privilege drop must cover every task, and `execve` replaces the +whole process anyway. Go cannot make that promise: the runtime spawns +threads before `main`, and a raw in-process dance would leave sibling +threads still root — a real vulnerability class, not a theoretical one. +That is exactly why the Go build pushes the entire sequence into the +fork/exec child. The general rule: change identity either before threads +exist or in a fresh child — never "around" a running thread pool. + +## Build, run, observe + +```bash +[host]$ LIBVIRT_DEFAULT_URI=qemu:///system \ + python3 scripts/test-all-examples.py --only 14-identity-and-privilege --mode vm +``` + +On this host with the lab up, the runner built all three languages and +verified each one against `systems-target` (Fedora 44, kernel +6.19.10-300.fc44): + +``` +example cpp go rust +14-identity-and-privilege PASS PASS PASS + +3 passed, 0 failed, 0 skipped +``` + +Each language's verify run asserts six things, and the two that carry the +chapter are the paired runs. By hand (the verify script stages +`/tmp/pmon-verify`, a world-readable copy of the binary, first): + +```bash +[host]$ cd examples/14-identity-and-privilege +[host]$ export LIBVIRT_DEFAULT_URI=qemu:///system TARGET=systems-target +[host]$ SUDO=1 ./demo.sh cpp run drop --user nobody --keep-cap net_bind_service -- /tmp/pmon-verify bindprobe --port 80 +→ copying app to fedora@192.168.124.7:/home/fedora/app +→ running on systems-target (Ctrl-C to stop): +bindprobe: uid=65534 bound :80 +[host]$ echo $? +0 +``` + +That single line is the positive proof: uid 65534 (`nobody`) is not root, +yet the bind on :80 succeeded. And the negative control — the same drop +with `--keep-cap` removed: + +```bash +[host]$ SUDO=1 ./demo.sh cpp run drop --user nobody -- /tmp/pmon-verify bindprobe --port 80 +→ copying app to fedora@192.168.124.7:/home/fedora/app +→ running on systems-target (Ctrl-C to stop): +bindprobe: bind :80: Permission denied +[host]$ echo $? +3 +``` + +(An ssh pseudo-terminal warning line is trimmed from both transcripts.) +The Go and Rust binaries produce these exact same two lines and exit +codes — that is what `verify.lua` asserts per language. The pair matters +more than either half: without the negative control, a lowered +`ip_unprivileged_port_start` or a stray root uid would make the positive +run pass while proving nothing about capabilities. + +## Cross-check on systems-target + +`bindprobe` exits as soon as it has bound, so to inspect the dropped +child's credential state directly, run the same drop around a `sleep` and +read the child while it lives — three independent tools against the same +process: + +```bash +[vm]$ sudo /home/fedora/app drop --user nobody --keep-cap net_bind_service -- sleep 120 & +[vm]$ pgrep -u nobody sleep +8623 +[vm]$ getpcaps 8623 +8623: cap_net_bind_service=eip +[vm]$ grep Cap /proc/8623/status +CapInh: 0000000000000400 +CapPrm: 0000000000000400 +CapEff: 0000000000000400 +CapBnd: 000001ffffffffff +CapAmb: 0000000000000400 +[vm]$ capsh --decode=0000000000000400 +0x0000000000000400=cap_net_bind_service +``` + +Read the layers against Figure 14.1: `getpcaps` names the one capability +with `=eip` (effective, inheritable, permitted); the raw `/proc` view +shows the same bit — `0x400` is `1 << 10` — in all four transferable sets +including ambient, which is the set that carried it through `execve`; and +`capsh --decode` closes the loop by translating the hex back to the name. +`CapBnd` is still the full mask because `pmon` never touched the bounding +set. The negative-control child, inspected the same way, is the mirror +image: + +```bash +[vm]$ sudo /home/fedora/app drop --user nobody -- sleep 120 & +[vm]$ getpcaps 8746 +8746: = +[vm]$ grep Cap /proc/8746/status +CapInh: 0000000000000000 +CapPrm: 0000000000000000 +CapEff: 0000000000000000 +CapBnd: 000001ffffffffff +CapAmb: 0000000000000000 +``` + +Two more readings pin down where the capability did *not* come from. +`getcap /tmp/pmon-verify` prints nothing — no file capabilities, so the +ambient path is the only route the bit had. And +`sysctl net.ipv4.ip_unprivileged_port_start` reports `1024`, ruling out +the sysctl shortcut that would have made :80 free for everyone. For +contrast, a root shell on the same guest reads +`CapPrm`/`CapEff` `000001ffffffffff` with `CapInh` and `CapAmb` zero — +full power, nothing inheritable, nothing ambient: the default state the +drop starts from. + +> **On the lab VM** — you can also watch the kernel make the decision +> itself. `bpftrace` on the `cap_capable` hook, filtered to the probe +> process, during both runs: +> +> ```bash +> [vm]$ sudo bpftrace -e 'kprobe:cap_capable /comm=="pmon-verify"/ { @cap[tid]=arg2 } kretprobe:cap_capable /@cap[tid]/ { printf("%s uid=%d cap=%d ret=%d\n", comm, uid, @cap[tid], retval); delete(@cap[tid]); }' +> pmon-verify uid=65534 cap=10 ret=0 +> pmon-verify uid=65534 cap=10 ret=-1 +> ``` +> +> Capability 10 is `CAP_NET_BIND_SERVICE`: the kept-cap run's check +> returns 0 (granted) and the negative control's returns -1 (`EPERM`) — +> the same verdict the bind lines reported, observed at the kernel hook +> that issued it. (Both runs also emit a burst of `cap=21` — +> `CAP_SYS_ADMIN` — denials from unrelated kernel probing; only the +> `cap=10` verdicts differ. bcc-tools' `capable` would show the same +> stream with symbolic names, but it failed to compile against kernel +> 6.19 on this guest; Part 8 works through that toolchain properly.) + +One deliberate non-observation: you cannot follow the drop with `strace` +from outside, because ptrace attachment is severed at a privilege +transition. The port bind *is* the observation surface here — and there is +no dashboard panel by design; this example emits no telemetry. + +## What you learned + +- The three uids are a state machine, and `setresuid(u, u, u)` is the one + transition that says what it does: all three slots move atomically, the + saved-uid escape hatch is destroyed, and the drop is irreversible — + groups and gid must move first, while you are still privileged. +- Capabilities live in five process sets and three file sets; `execve` + recombines them, and the ambient set exists so a non-root process can + carry a capability into an ordinary uncapped binary — the case the + inheritable set was never able to serve. +- Ordering is the whole game: `PR_SET_KEEPCAPS` before `setresuid`, + `capset` into permitted *and* inheritable after it, `PR_CAP_AMBIENT_RAISE` + last — and in Go the same sequence must run in the fork/exec child, + because credentials are per-thread and the runtime is threaded before + `main`. +- A privilege claim needs a negative control: `uid=65534 bound :80` + proves nothing until the same drop without the cap yields + `Permission denied` — paired evidence, checked by `getpcaps`, + `/proc//status` + `capsh --decode`, and the kernel's own + `cap_capable` verdict. + +Next, **virtual memory**: what an address actually is, why `pmon`'s +children each believe they own the whole address space, and what a page +fault costs. + +--- + +

verified — every transcript +and number above was produced this session against the +systems-target guest (Fedora 44, kernel 6.19.10-300.fc44): +the runner printed 14-identity-and-privilege PASS PASS PASS +(3 passed, 0 failed, 0 skipped) in vm mode; the positive run emitted +bindprobe: uid=65534 bound :80 (exit 0) and the negative +control bindprobe: bind :80: Permission denied (exit 3), both +byte-identical across cpp/go/rust; the kept-cap sleep child +(pid 8623) read cap_net_bind_service=eip from +getpcaps and 0000000000000400 in +CapInh/CapPrm/CapEff/CapAmb with CapBnd 000001ffffffffff, +decoded by capsh; the negative child (pid 8746) read all-zero +sets; getcap on the staged binary printed nothing; +ip_unprivileged_port_start was 1024; and the +bpftrace cap_capable trace showed +cap=10 ret=0 vs ret=-1 live on the guest. The +umask lines were run on the reference host. bcc's capable +failing to build on kernel 6.19 was also observed, as stated.

diff --git a/_docs/15-virtual-memory.md b/_docs/15-virtual-memory.md new file mode 100644 index 0000000..d338be4 --- /dev/null +++ b/_docs/15-virtual-memory.md @@ -0,0 +1,526 @@ +--- +title: "Virtual memory" +order: 15 +part: "Memory" +description: "The address space read line by line from /proc/self/maps, brk versus mmap in each language's allocator under strace, minor and major page faults measured with getrusage and perf, RSS versus VSZ versus PSS from a live smaps_rollup, overcommit and OOM-killer policy, and a tour of madvise — with memmap, a self-inspecting memory tool in all three languages." +duration: 45 minutes +--- + +Part 3 closed with `pmon` dropping privileges in chapter 14; this part turns +inward, to the memory every one of those processes lives in. The one new idea +is that **an address is a promise, not a page**: when `malloc`, `make`, or +`Vec` hands you 64 MiB, the kernel has recorded a range in a bookkeeping +structure (a *virtual memory area*) and given you nothing else — no RAM moves +until you touch a page and the resulting fault forces the kernel to make good +on the promise, one 4 KiB installment at a time. Everything in this chapter — +the `/proc/self/maps` layout, the brk/mmap split, minor versus major faults, +RSS versus VSZ versus PSS, overcommit — is a different view of that +promise-then-pay machinery, and this part's artifact, `shmkv`, will spend the +next chapters building a shared key-value store directly on top of it. + +The code is in `examples/15-virtual-memory/`: `memmap`, a tool that performs +one kind of allocation (stack, heap, anonymous mmap, file mmap, or a stepped +fault walk), touches the pages, and then reports what the kernel now says +about *its own* address space. `./demo.sh` builds all three implementations; +the `README.md` pins the CLI, the output contract, and the exit codes. + +{% include excalidraw.html + file="15-address-space-map" + alt="A vertical stack of address-space regions from high to low: vsyscall, the stack growing down, an unmapped gap, the mmap region containing vdso and vvar pages, ld-linux and libc segments, and an accented 64 MiB anonymous rw-p mapping, another gap, the brk-grown heap, data and relro segments, and the executable text at the bottom, each labeled with a real address from this chapter's runs." + caption="Figure 15.1 — one x86-64 process address space, high addresses at the top; every address label comes from the /proc maps output walked below" %} + +> **Tools used** — `strace`, `perf`, `dd`, `ps`, `pgrep`, and `cat`/`grep` +> over `/proc` and `/sys` (host); `oomkill`/`bpftrace` (lab VM, exercised in +> Part 8). Everything here is checked by `scripts/check-host.sh` or ships +> with Fedora. + +## The address space, line by line + +`/proc/self/maps` lists every promise the kernel is currently holding for a +process — one line per VMA. The cleanest specimen is `cat` reading its own +maps, because whatever process opens that file sees *itself*: + +```bash +[host]$ cat /proc/self/maps +55a5a49ff000-55a5a4a05000 r-xp 00000000 00:24 28358601 /usr/bin/cat +55a5a4a05000-55a5a4a08000 r--p 00006000 00:24 28358601 /usr/bin/cat +55a5a4a08000-55a5a4a09000 r--p 00008000 00:24 28358601 /usr/bin/cat +55a5a4a09000-55a5a4a0a000 rw-p 00009000 00:24 28358601 /usr/bin/cat +55a5cdb86000-55a5cdba7000 rw-p 00000000 00:00 0 [heap] +7f7c5ac00000-7f7c68a92000 r--p 00000000 00:24 29927626 /usr/lib/locale/locale-archive +7f7c68b11000-7f7c68b56000 rw-p 00000000 00:00 0 +7f7c68b56000-7f7c68cc8000 r-xp 00000000 00:24 29927031 /usr/lib64/libc.so.6 +7f7c68cc8000-7f7c68d41000 r--p 00172000 00:24 29927031 /usr/lib64/libc.so.6 +7f7c68d41000-7f7c68d45000 r--p 001eb000 00:24 29927031 /usr/lib64/libc.so.6 +7f7c68d45000-7f7c68d47000 rw-p 001ef000 00:24 29927031 /usr/lib64/libc.so.6 +7f7c68d47000-7f7c68d4f000 rw-p 00000000 00:00 0 +7f7c68d6b000-7f7c68d6d000 rw-p 00000000 00:00 0 +7f7c68d6d000-7f7c68d71000 r--p 00000000 00:00 0 [vvar] +7f7c68d71000-7f7c68d73000 r--p 00000000 00:00 0 [vvar_vclock] +7f7c68d73000-7f7c68d75000 r-xp 00000000 00:00 0 [vdso] +7f7c68d75000-7f7c68d9f000 r-xp 00000000 00:24 29927028 /usr/lib64/ld-linux-x86-64.so.2 +7f7c68d9f000-7f7c68dab000 r--p 0002a000 00:24 29927028 /usr/lib64/ld-linux-x86-64.so.2 +7f7c68dab000-7f7c68dad000 r--p 00036000 00:24 29927028 /usr/lib64/ld-linux-x86-64.so.2 +7f7c68dad000-7f7c68dae000 rw-p 00038000 00:24 29927028 /usr/lib64/ld-linux-x86-64.so.2 +7f7c68dae000-7f7c68daf000 rw-p 00000000 00:00 0 +7ffcda504000-7ffcda526000 rw-p 00000000 00:00 0 [stack] +ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] +``` + +(Whitespace compressed; the columns are start–end, permissions, file offset, +device, inode, and path.) The fourth permission character is the one people +miss: `p` means *private* — writes trigger copy-on-write and never reach the +file — while `s` would mean *shared*, the mode chapter 16 is built on. Read +the file top to bottom and the classic layout falls out. The binary itself +appears four times — `r-xp` text, `r--p` read-only data, a second `r--p` that +is the relro region (writable during relocation, sealed read-only before +`main`), and `rw-p` data/bss — all file-backed at increasing offsets, all +shareable between every process running `cat`. Above it sits `[heap]`, the +one region the legacy `brk(2)` syscall grows. Then a jump to the high +`7f7c…` range: the **mmap region**, where the kernel hands out addresses +top-down — the locale archive `cat` mapped read-only, `libc` in the same +four-segment pattern as the binary, anonymous `rw-p` slabs with inode 0 +(allocator arenas and thread machinery), and the kernel-provided `[vvar]` + +`[vdso]` pages that let chapter 4's `clock_gettime` skip the syscall +entirely. Near the top, `[stack]` grows downward toward the mmap region; the +`[vsyscall]` page at the very top is a frozen compatibility relic. The gaps +between regions are the point, not an accident: unmapped gigabytes that turn +stray pointers into segfaults instead of silent corruption, with layout +randomized per-exec (ASLR) — rerun the command and every address shifts. + +## brk or mmap? Ask strace + +Two syscalls create writable anonymous memory: `brk`, which moves the single +high-water mark of `[heap]`, and `mmap`, which drops an independent VMA +anywhere in the mmap region. Which one does a 64 MiB allocation use? The +brief answer for all three languages is *mmap* — but the trace is more +instructive than the answer: + +```bash +[host]$ strace -f -e trace=mmap,brk,madvise ./cpp/build/release/app --mode heap +brk(NULL) = 0x2504b000 +brk(0x2507e000) = 0x2507e000 +mmap(NULL, 67112960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f44e00ef000 +``` + +(Loader mmaps trimmed.) glibc's malloc probes `brk(NULL)` for the current +break, grows it by 204 KiB (`0x2504b000` → `0x2507e000`) for its startup +arena — that is small allocations living in `[heap]` — and then satisfies the +64 MiB `std::vector` with a single `mmap` of 67,112,960 bytes: our 67,108,864 +plus one 4 KiB page for the chunk header. The dividing line is malloc's +`M_MMAP_THRESHOLD`, 128 KiB by default (dynamically adjusted): below it, +carve from the arena; above it, a dedicated mapping that `free` can return to +the kernel instantly with `munmap` instead of hoping the arena top shrinks. +Rust tells the same story with the same syscalls — its default global +allocator *is* glibc malloc, and its trace shows the identical `brk` pair +(`0x55cf01269000` → `0x55cf0128a000`) followed by the same 67,112,960-byte +`mmap`. Go is the outlier: zero `brk` calls, ever. Its runtime reserves heap +arenas with `PROT_NONE` mappings and commits pages as needed — the +cross-check below shows that dance. This is why `memmap --mode heap` prints a +target line with *no* `[heap]` tag: the region backing a big allocation is an +anonymous mapping in the mmap region, in every language. The `[heap]` name in +maps is a fossil; the working heap is wherever the allocator put it. + +## Page faults, minor and major + +{% include excalidraw.html + file="15-fault-flow" + alt="A left-to-right flow: a user write to buf, then an MMU miss with a CPU trap, then the kernel page-fault handler looking up the VMA, fanning out to four outcomes: no VMA or bad permissions raising SIGSEGV, an anonymous first write getting a new zero-filled frame as a minor fault, a file page found in the page cache mapped with fault-around as a minor fault, and an uncached file page requiring a disk read as a major fault. Notes list the measured counts from this chapter's runs." + caption="Figure 15.2 — one touched page's journey through the fault handler; the counts in the notes are this chapter's getrusage measurements" %} + +The touch is where the promise gets paid. The MMU finds no page-table entry, +the CPU traps, and the kernel's fault handler asks: is this address inside a +VMA, with compatible permissions? No → `SIGSEGV`. Yes → resolve it. If the +page can be produced without I/O — a zero-filled frame for a first anonymous +write, or a file page already sitting in the page cache — that is a **minor +fault**: microseconds, bookkeeping only. If the kernel must read the disk, +the process sleeps through a **major fault**: the same event, three to six +orders of magnitude slower. `memmap` measures both with `getrusage(2)` deltas +around the touch loop. Writing 64 MiB of heap costs `minor=16385 major=0` — +one fault per touched 4 KiB page, plus one for the chunk-header page. The +read-side is cheaper than you would guess: `fault-walk` writes a 16 MiB temp +file, maps it read-only, and touches its 4,096 pages in eight steps of 512 — +and every step reports `minor=32`, not 512. That is **fault-around**: for +file pages already in the page cache, one fault maps a batch of 16 +surrounding pages, so sequential reads of cached files fault 16× less often +than anonymous writes (which must give each page its own private zeroed +frame). And `major=0` throughout, because the walk file was written moments +earlier and still sits in the page cache — a major fault would mean the +machine actually went to disk. + +## Three sizes and a policy: VSZ, RSS, PSS, and overcommit + +Promise-then-pay means every process has at least three sizes. **VSZ** is +promises: the sum of all VMAs. **RSS** is payments: pages currently resident. +The gap is routinely enormous — + +```bash +[host]$ ps -o pid,vsz,rss,comm $$ + PID VSZ RSS COMMAND +2620509 233356 4808 zsh +``` + +— a 227 MiB address space with 4.7 MiB resident. But RSS double-counts: +every process using `libc` counts its shared pages at full price. **PSS** +(proportional set size, from `/proc//smaps_rollup`) divides each shared +page by the number of processes sharing it, so PSS sums to something +meaningful across a fleet; the cross-check below watches `memmap`'s PSS jump +from 403 kB to 65,948 kB as it dirties private pages no one else can share. + +Unbacked promises need a policy, and it lives in `/proc/sys/vm` — read these, +do **not** write them: + +```bash +[host]$ cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio +0 +50 +[host]$ grep -E 'CommitLimit|Committed_AS' /proc/meminfo +CommitLimit: 41161532 kB +Committed_AS: 48534240 kB +``` + +Mode `0` is the default heuristic: refuse only absurd single allocations, +otherwise assume most promises are never fully touched. The proof is in the +second read: this 64 GiB host has *already* promised 48.5 GiB against a +nominal `CommitLimit` of 41.2 GiB (swap + 50% of RAM, per +`overcommit_ratio`), and nothing is wrong — in mode 0 the limit is +advisory. Mode `2` enforces it (`malloc` starts returning `NULL` for real); +mode `1` promises anything. The price of overcommit is paid at touch time: if +a fault needs a frame and there is nothing left to reclaim, the **OOM +killer** picks the process with the highest badness score — roughly its +share of RAM, adjusted by `/proc//oom_score_adj` (−1000 exempts, +1000 +volunteers). This shell runs at `100` — `cat /proc/self/oom_score_adj` — a +nudge from systemd making user sessions die before system services. The +lesson for a systems programmer: on mode-0 Linux, allocation failure mostly +does not arrive as a clean error at the call site; it arrives as `SIGKILL` +at the worst touch. Chapter 22's cgroup work sharpens this with per-service +memory limits. + +The last piece of the VMA toolkit is `madvise(2)` — hints on a range you +already own. `MADV_WILLNEED` starts readahead; `MADV_SEQUENTIAL` and +`MADV_RANDOM` tune it; `MADV_DONTNEED` drops pages *now* (anonymous memory +reads back as zeros — it is destructive, not advisory, despite the name); +`MADV_FREE` marks pages reclaimable-when-convenient, cheaper because the +kernel may never bother; `MADV_HUGEPAGE`/`MADV_NOHUGEPAGE` opt a range in or +out of transparent huge pages — which matters here because this host's THP +mode is `madvise` (`cat /sys/kernel/mm/transparent_hugepage/enabled` prints +`always [madvise] never`), meaning 2 MiB pages only where explicitly +requested. This is not exotic: the Go runtime in the cross-check below calls +`madvise(…, MADV_NOHUGEPAGE)` on its own metadata as a matter of course, and +chapter 17 uses `MADV_DONTNEED` semantics to explain what "freeing" memory +to an allocator actually returns to the kernel. + +## How the code works + +Three small structures carry `memmap`. A `Baseline` — `{rss_kb, minor, +major}` — is captured *before* each allocation because `/proc/self/status`'s +`VmRSS` and `getrusage`'s `ru_minflt`/`ru_majflt` are cumulative since +process start; only deltas isolate the window we care about. A `Mapping` +owns an mmap'd range exactly the way chapter 7's `Fd` owns a descriptor — +move-only in C++, `Drop` calls `munmap` in Rust, `defer unix.Munmap(m)` in +Go — so no error path leaks a VMA. A `TempFile` unlinks its path on +destruction, which is why `verify.lua` can assert the walk file is gone even +when a run fails halfway. + +The probes are deliberately boring text-parsing: `vmrss_kb()` scans +`/proc/self/status` for the `VmRSS:` line; `fault_counts()` wraps +`getrusage(RUSAGE_SELF)`; and the maps excerpt re-reads `/proc/self/maps`, +hex-parses each line's range, and prints those overlapping the target — the +entire selection logic is one interval test, C++ spelling: + +```cpp + if (start < hi && end > lo) { + std::println("memmap: {} <-- target (mode={})", line, + mode_name(mode)); + } +``` + +The touch loops write (or read) one byte per page, and each language must +actively defend that loop against its optimizer, because a loop whose stores +are never read is dead code: C++ uses an empty `asm volatile` barrier taking +the pointer, Rust `std::hint::black_box`, Go a package-level `sink` variable. +Delete the barrier and `--mode heap` still *allocates*, but release-build +touch loops vanish and the fault counts collapse — the tool would silently +measure nothing. + +The mode worth reading three times is `stack`, because each language's +runtime forces a different shape: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +// Baseline is captured by the caller so that any page the compiler touches +// while setting up this 4 MiB frame still lands inside the measured window. +[[gnu::noinline]] std::expected +stack_worker(std::size_t bytes, std::size_t page, const Baseline& base) { + std::array buf{}; + touch_writable(std::span{buf.data(), bytes}, page); + return report(reinterpret_cast(buf.data()), bytes, + Mode::stack, base); +} + +[[nodiscard]] std::expected +run_stack(std::size_t bytes, std::size_t page) { + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + return stack_worker(bytes, page, *base); +} +``` + +```go +// runStack touches a large local buffer on a dedicated goroutine so the +// growth happens on a fresh stack; the result comes back over a channel. +// The baseline is captured before the goroutine starts so the runtime work +// of growing (or relocating) that stack is part of the measured window. +func runStack(bytes, page int) error { + base, err := takeBaseline() + if err != nil { + return err + } + errc := make(chan error, 1) + go func() { + errc <- stackWorker(bytes, page, base) + }() + return <-errc +} + +//go:noinline +func stackWorker(n, page int, base baseline) error { + var buf [stackCapMb * mib]byte + touchWritable(buf[:n], page) + return report(uintptr(unsafe.Pointer(&buf[0])), uintptr(n), "stack", base) +} +``` + +```rust +// Baseline is captured by the caller: the compiler's stack probing touches +// every page of this 4 MiB frame at function entry, so faults counted from +// inside the frame would miss the allocation itself. +#[inline(never)] +fn stack_worker(bytes: usize, page: usize, base: &Baseline) -> Result<()> { + let mut buf = [0u8; STACK_CAP_MB * MIB]; + touch_writable(&mut buf[..bytes], page); + report(buf.as_ptr() as usize, bytes, "stack", base) +} + +fn run_stack(bytes: usize, page: usize) -> Result<()> { + let base = take_baseline()?; + stack_worker(bytes, page, &base) +} +``` + +The shared discipline: the worker is `noinline` so the 4 MiB frame really +exists as a frame, and the baseline is taken by the *caller*, because the +compiler (stack probing in C++/Rust, stack growth in Go) touches frame pages +at function entry — measure from inside and you miss the very faults you came +for. The divergence: C++ and Rust run the worker on the OS thread, so their +excerpt shows the `[stack]` VMA growing; Go runs it on a fresh goroutine +whose stack is runtime-managed heap spans, so its excerpt shows an anonymous +region — same contract, different mechanism, and the accurate place to see +that difference is the output itself, which is why the maps line is part of +the contract. The wiring is a strict flag parser +(exit 2 with a usage line on anything malformed), a per-mode header +computing `bytes`/`pages` (with `stack` clamped to 4 MiB to stay inside the +8 MiB rlimit, and `mmap-file` sized by `fstat` instead of `--mb`), and the +common `report()` tail. Fragile bits, stated plainly: the page size comes +from `sysconf` at runtime (4096 here; 16 KiB Apple-style kernels would change +every count); `fault-walk`'s step quota is `pages/8` with the remainder +folded into step 8, so `--mb` values whose page count is not divisible by 8 +print an uneven last step; and the fault window unavoidably includes the +probe's own work — one page of malloc header here, a few hundred runtime +pages in Go — which is why the verifier asserts *floors*, not exact counts. + +## Errors, three ways + +`memmap` splits failures on one line: *usage* errors (unknown mode, bad +`--mb`, stray argument) print the usage string and exit 2 before anything is +allocated; *runtime* errors print `memmap: error: …` and exit 1. The +interesting decision is what the contract does **not** pin. Point each +implementation at a missing file: + +``` +memmap: error: /no/such/file: No such file or directory (C++, exit 1) +memmap: error: open /no/such/file: no such file or directory (Go, exit 1) +memmap: error: /no/such/file: No such file or directory (os error 2) (Rust, exit 1) +``` + +Three renderings of the same `ENOENT`, because each rides its language's +native machinery — `std::error_code::message()` inside `std::expected` +plumbing, a `%w`-wrapped `*os.PathError`, `anyhow`'s `{:#}` chain over +`io::Error`. Unlike chapter 5's `fwatch`, this contract pins the prefix and +the exit code but leaves message text native, and `verify.lua` asserts +exactly that split — while errors the *program* generates, like mapping an +empty file (`mmap` of length 0 would fail anyway, so `open_for_map` checks +first), produce the identical `memmap: error: empty.bin: file is empty` in +all three, exit 1, byte for byte. + +## Concurrency lens + +All three `memmap`s are logically single-threaded, but the measurements say +otherwise, and the noise is the lesson. Rust reports `minor=16384` — exact, +one fault per page, because `vec![0; n]` allocates through `calloc`, which +knows fresh mmap pages are already zero and skips touching them; the touch +loop pays for every page itself. C++ reports `16385`: `std::vector` +zero-fills eagerly during construction, so the faults land in the `memset` +(plus the header page), and the later touch loop faults nothing. Go reports +`16598`: the same 16,384 plus a few hundred faults from runtime machinery — +GC metadata, spans, a second OS thread growing something — because +`getrusage(RUSAGE_SELF)` sums *every thread in the process*, and a Go +program is never really alone. The same reasoning explains why `perf stat` +whole-process counts (below) always exceed the in-window deltas, and why +`verify.lua` asserts `minor > 0` and RSS floors instead of equalities. +One more thread-shaped trap from the stack tabs: Go's worker goroutine could +be *rescheduled onto another OS thread* mid-measurement, which is fine for +fault counting precisely because `RUSAGE_SELF` is process-wide — the +per-thread `RUSAGE_THREAD` would be the wrong tool here. + +## Build, run, observe + +```bash +[host]$ cd examples/15-virtual-memory && ./demo.sh cpp run --mode heap +memmap: mode=heap bytes=67108864 pages=16384 +memmap: maps excerpt +memmap: 7f7e52806000-7f7e56807000 rw-p 00000000 00:00 0 <-- target (mode=heap) +memmap: vmrss_before=3872KB vmrss_after=69412KB +memmap: faults minor=16385 major=0 +``` + +Read it against the concepts: the target is an anonymous `rw-p` region (not +`[heap]`), spanning `0x4001000` bytes — 64 MiB plus the header page — and +the RSS delta is 65,540 kB, the *same* number, because every promised page +was touched. Go printed 2508 → 68728 (`minor=16598`) and Rust 2128 → 67668 +(`minor=16384`) on the same host. Then contrast the modes: `--mode stack` +clamps to `bytes=4194304 pages=1024` and faults `minor=1024` into the +`[stack]` VMA for C++/Rust but into an anonymous region for Go; +`--mode mmap-anon --mb 32` faults `minor=8192` for 8,192 pages; and the +file-backed modes break the one-fault-per-page pattern: + +```bash +[host]$ dd if=/dev/urandom of=sample.bin bs=1M count=2 +[host]$ ./cpp/build/release/app --mode mmap-file sample.bin +memmap: mode=mmap-file bytes=2097152 pages=512 +memmap: maps excerpt +memmap: 7fe01c400000-7fe01c600000 r--p 00000000 00:24 24538803 …/sample.bin <-- target (mode=mmap-file) +memmap: vmrss_before=4052KB vmrss_after=6100KB +memmap: faults minor=32 major=0 +``` + +512 pages read, 32 faults — fault-around's 16× batching, live. `--mode +fault-walk --mb 16` shows the same ratio held across all eight steps +(`step=1/8 pages=512 minor=32 major=0` … eight times), with a final RSS +delta of 16,384 kB: 4,096 resident pages bought with 256 faults. The full +matrix is one command: `python3 scripts/test-all-examples.py --only +15-virtual-memory` builds and behavior-checks all three (`PASS PASS PASS`, +58 `verify.lua` checks per language on this host). + +## Cross-check, three ways + +**The allocators' syscalls, across all three.** The C++ trace appeared +above; the other two confirm the split. Rust, under `strace -f -e +trace=mmap,brk,madvise`, shows glibc's fingerprint — `brk(NULL)`, a 132 KiB +break growth, then `mmap(NULL, 67112960, …)`. Go shows a different species +entirely: + +```bash +[host]$ strace -f -e trace=mmap,brk,madvise ./go/bin/app --mode heap +mmap(0x2ed440000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2ed440000000 +mmap(0x2ed443400000, 67108864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x2ed443400000 +madvise(0x7fee9f000000, 33554432, MADV_NOHUGEPAGE) = 0 +``` + +(Trimmed to the shape; pid columns dropped.) Reserve arenas with +`PROT_NONE` — promises so weak even a *read* faults fatally — then commit +the needed span read-write with `MAP_FIXED`, and `madvise` runtime metadata +out of transparent huge pages. Zero `brk` calls in the entire trace. + +**`smaps_rollup`, before and after, on a live process.** `memmap` reads +`/proc/self/status` (openat #6, pre-allocation) and `/proc/self/maps` +(openat #7, post-touch); injecting an 8-second delay into those opens holds +it still at exactly the right two moments: + +```bash +[host]$ strace -o /dev/null -e trace=openat -e inject=openat:delay_enter=8000000:when=6+ \ + ./cpp/build/release/app --mode heap > /dev/null & +[host]$ APP=$(pgrep -n -x app) +[host]$ grep -E '^(Rss|Pss|Private_Dirty):' /proc/$APP/smaps_rollup # pause 1 +Rss: 3736 kB +Pss: 403 kB +Private_Dirty: 224 kB +[host]$ grep -E '^(Rss|Pss|Private_Dirty):' /proc/$APP/smaps_rollup # pause 2, 8s later +Rss: 69344 kB +Pss: 65948 kB +Private_Dirty: 65768 kB +``` + +Before: 3.7 MB resident but only 403 kB PSS — nearly everything is `libc` +and loader pages shared with the whole system, billed proportionally. After: +the deltas are private dirty pages nobody can share, so RSS and PSS converge. +`VmSize` in `/proc/$APP/status` moved 6860 → 72400 kB across the same pause +— a delta of 65,540 kB, agreeing to the kilobyte with the 67,112,960-byte +`mmap` strace showed and with the tool's own printed RSS growth. + +**`perf stat`, against the tool's own numbers.** An independent counter over +the whole process lifetime: + +```bash +[host]$ perf stat -e page-faults ./cpp/build/release/app --mode heap +memmap: faults minor=16385 major=0 + 16,520 page-faults:u + 0.030148349 seconds time elapsed +``` + +Go: 16,879 against its in-window 16,606; Rust: 16,458 against 16,384. In +each pair perf sits a stable one-to-three hundred faults above the +`getrusage` delta — precisely the loader, runtime startup, and probe pages +that fall outside the measured window. Three observers (the kernel's rusage +accounting, `/proc` VMA accounting, the perf software event), one consistent +story. + +> **On the lab VM** unverified — +> the fleet-wide view of this chapter is bcc-tools territory: `oomkill` +> logging every OOM-killer decision with the victim's badness score, and +> `bpftrace` histograms over the `page_fault_user` tracepoint replacing +> per-process `getrusage`. Those need root on a kernel you can afford to +> pressure — chapter 30, on the `systems-target` VM, does exactly that. + +## What you learned + +- An address is a promise: allocation creates VMAs (visible line by line in + `/proc/self/maps`, gaps and all), and pages arrive only at first touch — + which is why VSZ, RSS, and PSS are three different truths, and why + `smaps_rollup`'s PSS is the number worth summing across processes. +- All three languages satisfy a 64 MiB allocation with `mmap`, not `brk`: + glibc (C++ and Rust alike) via `M_MMAP_THRESHOLD`, Go via `PROT_NONE` + arena reservations committed with `MAP_FIXED` — the `[heap]` VMA is where + small glibc allocations live, nothing more. +- Minor faults are bookkeeping (one per anonymous page written: 16,384–16,385 + measured; 16 pages per fault for cached file reads, thanks to + fault-around), major faults are disk reads, and `getrusage`, `perf stat`, + and `smaps_rollup` triangulate the same counts from three directions. +- Overcommit mode 0 means `Committed_AS` may exceed `CommitLimit` (it does, + on this host); the bill arrives at touch time via reclaim or the OOM + killer's `oom_score_adj`-weighted choice, and `madvise` is your lever for + telling the kernel what a range is really for. + +Next, **mmap and shared mappings**: `MAP_SHARED`, `msync`, and two processes +seeing one page — the machinery `shmkv` v0 is built from. + +--- + +

verified — every number and +excerpt above was produced on the Fedora 44 reference host (kernel +7.1.3-200.fc44, THP=madvise) this session: the runner printed +15-virtual-memory PASS PASS PASS (3 passed, 0 failed, 0 +skipped; 58 verify.lua checks per language); the heap runs reported +minor=16385/16598/16384 (C++/Go/Rust) with the anonymous +rw-p target lines and RSS growth shown; the maps listing is a +real cat /proc/self/maps; the strace excerpts show glibc's +brk+67,112,960-byte mmap for C++ and Rust and Go's +PROT_NONE/MAP_FIXED/MADV_NOHUGEPAGE +sequence with zero brk; the delay-injection pause yielded the live +smaps_rollup readings (Pss 403 kB → 65,948 kB, VmSize delta +65,540 kB matching the mmap size); perf stat counted 16,520/16,879/16,458 +page-faults; and the overcommit, THP, and oom_score_adj values +were read (never written) from /proc and /sys. The +"On the lab VM" bcc-tools callout is unverified as marked and is exercised +in Part 8.

diff --git a/_docs/16-mmap-and-shared-mappings.md b/_docs/16-mmap-and-shared-mappings.md new file mode 100644 index 0000000..7bd10ab --- /dev/null +++ b/_docs/16-mmap-and-shared-mappings.md @@ -0,0 +1,496 @@ +--- +title: "mmap and shared mappings" +order: 16 +part: "Memory" +description: "File-backed vs anonymous and MAP_SHARED vs MAP_PRIVATE copy-on-write, msync guarantees against page-cache writeback, the ftruncate+mmap lifecycle, and memfd sealing — proven by shmkv v0, one byte-exact store format that the C++, Go, and Rust binaries create, write, and read interchangeably." +duration: 50 minutes +--- + +The previous chapter took the address space apart — VMAs, page tables, faults — +without ever asking where a mapping's bytes come from. This chapter answers +that with the book's next artifact: `shmkv`, a key/value store whose entire +database is one `mmap`'d file. The one new idea is that `MAP_SHARED` collapses +three things you have treated as separate — your memory, the page cache, and +the file on disk — into a single set of physical pages. A store instruction in +one process is a load result in another and a dirty page the kernel will +eventually write out, with no `read`, no `write`, and no copy in between. +Version 0 leans on that hard: three binaries in three languages agree on every +byte of one on-disk format, so the Rust binary can create a store, the Go +binary can write into it, and the C++ binary reads the value back. + +The code is in `examples/16-mmap-and-shared-mappings/`. `./demo.sh` there +builds all three implementations; its `README.md` specifies the byte-exact +format, the CLI, and the exit codes all three languages share. + +{% include excalidraw.html + file="16-mapping-page-cache" + alt="Two user-space process bands above a kernel band and a disk band. Process A holds an mmap MAP_SHARED node marked rw-s and a dashed MAP_PRIVATE node; process B holds its own MAP_SHARED node. Amber and grey arrows from both mappings converge on one page-cache node for store.kv in the kernel band; a dashed arrow back to the private node is labeled first write copies the page; an amber msync MS_SYNC arrow and a dashed writeback-eventually arrow lead down to the store.kv-on-disk node." + caption="Figure 16.1 — one file, one set of page-cache pages, many mappings: A's store and B's load meet in the same physical memory, msync forces those pages to disk, and a MAP_PRIVATE view forks its own copy on first write" %} + +> **Tools used** — `strace`, `od`, `cmp`, `stat`, `python3` (host); +> `bpftrace` (lab VM, exercised in Part 8). Everything here is checked by +> `scripts/check-host.sh`, ships with Fedora, or is preinstalled in the lab +> VMs. + +## The mapping quadrant + +Every `mmap` call picks one cell of a 2×2. The first axis is **what backs the +pages**: a *file-backed* mapping names an fd and an offset, so its pages are +literally page-cache pages of that file; an *anonymous* mapping +(`MAP_ANONYMOUS`) is backed by nothing but zero-fill — this is what your +allocator asks for when it wants a fresh arena, which is chapter 17's whole +subject. The second axis is **who sees your writes**. `MAP_SHARED` means +writes go to the backing object itself: for a file, straight into the shared +page cache, visible immediately to every other process mapping the same range. +`MAP_PRIVATE` means copy-on-write: your mapping starts out referencing the +very same page-cache pages — reads are free and shared — but the page-table +entries are write-protected, and the first store to a page faults, at which +point the kernel copies that page and points only *your* mapping at the copy. +Your writes never reach the file, and after the copy, other processes' writes +to that page stop reaching you. That is exactly how the loader maps your +binary: code and read-only data `MAP_PRIVATE` from the executable, shared by +every running instance until someone (a debugger planting a breakpoint) dirties +a page. + +Copy-on-write is observable in one screenful. With a store file present, map +it privately and deface the magic: + +```python +import mmap, os +fd = os.open("demo.kv", os.O_RDWR) +size = os.fstat(fd).st_size +priv = mmap.mmap(fd, size, flags=mmap.MAP_PRIVATE) +priv[0:5] = b"XXXXX" # scribble over the magic... privately +print("private view :", bytes(priv[0:5])) +print("file on disk :", open("demo.kv", "rb").read(5)) +``` + +```console +[host]$ python3 private_cow.py +private view : b'XXXXX' +file on disk : b'SHKV1' +``` + +The write landed in a COW copy; the file — and every `MAP_SHARED` mapper — +still sees `SHKV1`. `shmkv` therefore uses `MAP_SHARED` everywhere: the page +cache *is* the database. + +## What `msync` promises — and what writeback already does + +Two guarantees get conflated here, and they have different owners. +**Visibility** between processes needs no syscall at all: two `MAP_SHARED` +mappings of the same file range resolve to the same physical pages, so a store +in one is a load in the other at ordinary memory speed. **Durability** is a +separate question: your store dirtied a page-cache page, and the kernel's +writeback machinery will get it to disk *eventually* — on Fedora's defaults, +dirty pages start aging out after 30 seconds. If the machine loses power +before that, the store never happened. `msync(addr, len, MS_SYNC)` closes the +gap: it blocks until the dirty pages in that range are written to the backing +file, the mapped-memory analogue of `fsync`'s promise from the durability +chapter (`MS_ASYNC`, by contrast, is close to a no-op on modern Linux — it +just nudges writeback). `shmkv` calls `msync(MS_SYNC)` at the end of every +`create` and `set`, so "`set alpha`" on stdout means the bytes are on disk, +not merely in the cache. The trade-off is the same one `fsync` bought: each +write now costs a device round-trip. + +## The `ftruncate` + `mmap` lifecycle + +A file-backed mapping does not size itself. `create` performs a fixed ritual: +`open(O_RDWR|O_CREAT|O_TRUNC)` so a reused path starts at zero bytes, then +`ftruncate(fd, 10 + slots*256)` to extend the file, then `mmap` for exactly +that length. Order matters twice. First, the mapping's length is fixed at map +time — mapping a still-empty file and truncating afterwards would leave pages +you can touch but a file too short to back them, and touching a page past EOF +delivers `SIGBUS`, not an error return. Second, `ftruncate` extension is +guaranteed zero-fill, and the format defines `key[0] == 0` as "empty slot" — +so a fresh store needs no initialization loop at all; every slot is born empty +because the kernel says extended bytes are zero. The header write and one +`msync` finish the job. `open` for an existing store is the mirror image: +`fstat` for the size, refuse anything smaller than a header, map exactly +`st_size` bytes, then validate magic and the size equation before trusting a +single slot. + +### Aside: `memfd_create` and sealing, live + +Sometimes you want the mapping semantics without any filesystem name. +`memfd_create(2)` returns an fd to an anonymous tmpfs file — mappable, +`ftruncate`-able, passable over a Unix socket — and, uniquely, **sealable**: + +```python +fd = os.memfd_create("shmkv-demo", os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING) +os.ftruncate(fd, 4096) +print(os.readlink(f"/proc/self/fd/{fd}")) # an anonymous "file" +m = mmap.mmap(fd, 4096, flags=mmap.MAP_SHARED) +m[:6] = b"SHKV1\0" +fcntl.fcntl(fd, fcntl.F_ADD_SEALS, + fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_GROW | fcntl.F_SEAL_FUTURE_WRITE) +``` + +```console +[host]$ python3 memfd_seal.py +/memfd:shmkv-demo (deleted) +seals: 22 +new writable map: [Errno 1] Operation not permitted +``` + +Run this session: the fd's `/proc` link shows `/memfd:shmkv-demo (deleted)` — +a file with no name anywhere; `F_GET_SEALS` reads back 22 +(`F_SEAL_SHRINK`=2 + `F_SEAL_GROW`=4 + `F_SEAL_FUTURE_WRITE`=16); the existing +mapping keeps writing, but a *new* writable `MAP_SHARED` attempt fails with +`EPERM`. Sealing is what makes shared memory safe to accept from an untrusted +peer — the `SIGBUS`-via-truncation attack below becomes impossible once +`F_SEAL_SHRINK` is set. The IPC part passes exactly such fds between +processes. + +## One layout, three languages + +Interop works because the format is defined in bytes, not in types: magic +`"SHKV1\0"` at offset 0, a **u32 little-endian** slot count at offset 6, then +256-byte slots — key NUL-padded in `[0..64)`, value in `[64..256)`. Note that +offset 6 is deliberately *misaligned* for a 4-byte integer, and none of the +three implementations care, because none of them ever casts a pointer into the +mapping: C++ assembles the u32 from four bytes with shifts, Go uses +`binary.LittleEndian.PutUint32`, Rust `u32::to_le_bytes` — byte moves have no +alignment requirements and no host-endianness dependence. + +The tempting alternative — declare a struct and cast the mapping to it — +is where every language would break differently. A C++ +`struct { char magic[6]; uint32_t slots; }` inserts two padding bytes to align +`slots`, silently making the header 12 bytes (and the unaligned cast would be +undefined behavior besides); `repr(Rust)` makes no layout promise at all — +Rust is free to reorder fields — so only `#[repr(C, packed)]` would be +defensible; Go specifies no struct padding contract and has no packed +annotation, which is precisely why `encoding/binary` exists. And all three +would still inherit the CPU's byte order. Stating "u32 LE at offset 6" in the +format and copying bytes is boring, and boring is why `cmp` can prove two +stores identical. + +{% include excalidraw.html + file="16-shmkv-layout" + alt="A byte-layout band shows store.kv as 1034 bytes: a dark magic node with bytes 53 48 4b 56 31 00, an amber u32 little-endian slot_count node with bytes 04 00 00 00, and four 256-byte slot boxes. A zoom band below splits one slot into a key field covering bytes 0 to 64 and a value field covering 64 to 256, both NUL-padded. Three language nodes — C++ writes it, Go reads it, Rust reads it — point up into the layout." + caption="Figure 16.2 — the shmkv v0 format, byte for byte; the header bytes shown are the real od output from this chapter's run, and cmp proves a Rust-built and a C++-built store file identical" %} + +## How the code works + +Each implementation is one `Store` abstraction over two resources — the fd and +the mapping — plus four commands. The mapping is where the languages differ +most, so that is the excerpt: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +// RAII shared mapping: mmap on construction, munmap in the destructor, +// msync(MS_SYNC) on demand. MAP_SHARED means stores are visible to every +// other process mapping the same file, and reach the page cache (and disk, +// after msync) rather than staying private to this address space. +class Mapping { + public: + static Result map_shared(const Fd& fd, std::size_t len) { + void* p = ::mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0); + if (p == MAP_FAILED) { + return std::unexpected(fail(1, "shmkv: mmap failed")); + } + return Mapping{static_cast(p), len}; + } + Mapping(const Mapping&) = delete; + Mapping& operator=(const Mapping&) = delete; + Mapping(Mapping&& other) noexcept + : data_(std::exchange(other.data_, nullptr)), len_(std::exchange(other.len_, 0)) {} + Mapping& operator=(Mapping&& other) noexcept { + if (this != &other) { + reset(); + data_ = std::exchange(other.data_, nullptr); + len_ = std::exchange(other.len_, 0); + } + return *this; + } + ~Mapping() { reset(); } + + [[nodiscard]] unsigned char* data() const noexcept { return data_; } + [[nodiscard]] std::size_t size() const noexcept { return len_; } + + [[nodiscard]] Result<> sync() const { + if (::msync(data_, len_, MS_SYNC) != 0) { + return std::unexpected(fail(1, "shmkv: msync failed")); + } + return {}; + } + + private: + Mapping(unsigned char* data, std::size_t len) noexcept : data_(data), len_(len) {} + void reset() noexcept { + if (data_ != nullptr) ::munmap(data_, len_); + data_ = nullptr; + len_ = 0; + } + unsigned char* data_ = nullptr; + std::size_t len_ = 0; +}; +``` + +```go +// store owns the fd and the shared mapping; close() unmaps then closes, +// mirroring the RAII types in the C++ and Rust versions. +type store struct { + fd int + data []byte + slots uint32 +} + +func (s *store) close() { + if s.data != nil { + _ = unix.Munmap(s.data) // []byte discipline: never touch s.data after this + s.data = nil + } + if s.fd >= 0 { + _ = unix.Close(s.fd) + s.fd = -1 + } +} + +func mapShared(fd int, size int) ([]byte, error) { + data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) + if err != nil { + return nil, failf(1, fmt.Errorf("mmap: %w", err), "shmkv: mmap failed") + } + return data, nil +} +``` + +```rust +/// Map `file` MAP_SHARED, read/write. +/// +/// ---- unsafe boundary ------------------------------------------------- +/// `MmapMut::map_mut` is `unsafe` because the compiler cannot see other +/// writers of the underlying file: another process truncating it would +/// turn our in-bounds loads into SIGBUS, and concurrent mutation would be +/// a data race the borrow checker never observed. Why the call is sound +/// here: +/// * every access goes through `&self.map[..]` slices bounded by the +/// length we mapped, never raw pointers; +/// * we size the mapping from ftruncate/fstat in the same process and +/// no shmkv command ever shrinks an existing store file; +/// * the demo runs shmkv commands sequentially, so cross-process writes +/// are ordered by process exit (and each writer msyncs before exit) — +/// the concurrent-writer race the type system cannot rule out is +/// excluded by the tool's usage model, not by the compiler. +/// Everything outside this function stays in safe Rust. +/// ---------------------------------------------------------------------- +fn map_shared(file: &std::fs::File) -> Result { + unsafe { MmapMut::map_mut(file) }.map_err(|_| Fail::new(1, "shmkv: mmap failed")) +} +``` + +Read the three as one argument about **who admits the danger**. A shared +mapping is memory whose contents can be changed — or whose backing can be +yanked — by something the compiler cannot see. Rust's `memmap2` makes you sign +for that: `MmapMut::map_mut` is `unsafe`, and the boundary comment is the +signature — the promises (in-bounds slices, nobody shrinks the file, writers +are sequenced) are exactly the conditions under which treating the mapping as +`&mut [u8]` is sound, and everything outside that one function is safe Rust. +C++ silently allows the same thing: `mmap` hands back `void*`, and nothing +distinguishes a pointer into a shared, externally mutable, `SIGBUS`-capable +region from a pointer into a private heap block — the `Mapping` class adds +lifetime discipline (move-only, `munmap` exactly once, in `Store` declared +*after* `Fd` so it unmaps before the fd closes) but the type system never +learns what the pointer means. Go hides it behind familiarity: +`unix.Mmap` returns an ordinary-looking `[]byte` whose backing array is not +GC-managed memory at all; the slice header outlives the mapping's validity the +moment `Munmap` runs, and the one-line discipline comment in `close` is all +that stands between you and a use-after-unmap fault the runtime will report as +a plain `SIGSEGV`/`SIGBUS`. + +Above that boundary the three programs are deliberately the same. `create` +runs the truncate-extend-map-stamp ritual and reports +`created FILE: N slots, SIZE bytes`. `open` validates before trusting: +size ≥ 10, magic bytes equal, and `size == 10 + slots*256` exactly — the last +check is what turns "some file that starts with SHKV1" into "a store whose +every slot index is in bounds", so the slot arithmetic +(`offset = 10 + i*256`) can never leave the mapping. `set` is a linear probe +that prefers the slot already holding the key (overwrite burns no new slot), +else the first empty one, else fails with exit 5; it clears the whole 256-byte +slot before copying key and value so a shorter value never leaves a longer +predecessor's tail bytes behind — without that `memset`, the two "identical" +stores in the cross-check would differ in the padding and `cmp` would say so. +`get` and `dump` scan slots through a `field()` helper that stops at the first +NUL, and `dump` sorts pairs bytewise by key so slot order — an artifact of +insertion history — never leaks into output. One fragile bit is worth naming: +C++ flushes stdout before printing `dump`'s stderr summary, because stdout to +a pipe is fully buffered and the combined-stream interleaving must match +Go and Rust byte for byte. + +## Errors, three ways + +The contract is a fixed stderr line plus a fixed exit code: 1 for +open/mmap/format failures, 2 for usage and oversized keys (>63 bytes) or +values (>191 bytes), 4 for `get` on a missing key, 5 for `set` on a full +store. C++ carries `CliError{code, msg}` through `std::expected`, and `main` +is the single printer. Rust's `Fail` is the same shape with one twist: an +empty message *means* "print the usage line", so `--slots 0`, +`--slots 12x`, and `--slots 99999999999` all funnel through `parse_slots` +(digits only, range [1, u32 max] — identical rules in all three languages) +into the same exit-2 usage path. Go is the most instructive: `cliError` pins +`code` and `msg` but also wraps an underlying cause, so `errors.As` routes it +in `main` while the `%w` chain keeps `mmap: ...` diagnosable — and a sentinel +`errUsage` plays Rust's empty-message role via `errors.Is`. Reproduced this +session: a junk file gets `shmkv: bad.kv: not a shmkv v0 store` and exit 1, a +missing file `shmkv: cannot open missing.kv` and exit 1, the third `set` into +a 2-slot store `shmkv: store full (2 slots)` and exit 5, a 64-byte key +`shmkv: key too long (max 63 bytes)` and exit 2 — identically from all three +binaries, which is what 48 `verify.lua` checks per language assert. + +## Concurrency lens + +`MAP_SHARED` gives you visibility, and visibility is not synchronization. Two +`set` processes running *concurrently* could both probe, both find slot 2 +empty, and both write it — last `memcpy` wins, one key silently lost; worse, a +`get` racing a `set` can read a slot mid-`memset` and see a torn key. Nothing +in this chapter prevents that: `msync` is a durability barrier, not a memory +barrier, and the kernel orders nothing between mappers. shmkv v0's defense is +its usage model — commands are whole processes run sequentially, so every +write is ordered by process exit — and the Rust unsafe-boundary comment is +where that assumption is written down rather than left implicit. This is the +cliff-hanger by design: chapter 20 puts atomics and futexes *inside* a shared +mapping to make concurrent access actually safe. Two language-specific hazards +round it out. Go's `[]byte` view means the race detector and GC offer no help +across processes — and the slice must never be touched after `Munmap`, which +`close` enforces by nil-ing it. And any mapper can be killed by a peer that +truncates the file — in-bounds loads become `SIGBUS` — which is exactly the +hole `F_SEAL_SHRINK` sealed in the memfd aside. + +## Build, run, observe + +```bash +[host]$ cd examples/16-mmap-and-shared-mappings && ./demo.sh +``` + +The full matrix is one runner line — from this session: + +```console +[host]$ python3 scripts/test-all-examples.py --only 16-mmap-and-shared-mappings +example cpp go rust +16-mmap-and-shared-mappings PASS PASS PASS +3 passed, 0 failed, 0 skipped +``` + +Driving the interop by hand is more instructive. Rust creates and writes, Go +writes into the same store, C++ reads everything back: + +```bash +[host]$ ./rust/target/release/app create demo.kv --slots 4 +created demo.kv: 4 slots, 1034 bytes +[host]$ od -An -tx1 -N10 demo.kv + 53 48 4b 56 31 00 04 00 00 00 +[host]$ ./rust/target/release/app set demo.kv zeta z1 && ./rust/target/release/app set demo.kv alpha a1 +set zeta +set alpha +[host]$ ./go/bin/app set demo.kv gadd g1 +set gadd +[host]$ ./cpp/build/release/app get demo.kv gadd +g1 +[host]$ ./go/bin/app set demo.kv alpha a2 && ./cpp/build/release/app get demo.kv alpha +set alpha +a2 +[host]$ ./rust/target/release/app dump demo.kv +alpha=a2 +gadd=g1 +zeta=z1 +shmkv: 3/4 slots used +``` + +The `od` line is Figure 16.2 in the flesh: `53 48 4b 56 31 00` is +`SHKV1\0`, and `04 00 00 00` is 4 as a u32 LE. The overwrite of `alpha` +reused its slot (3/4 used, not 4/4), and the dump is key-sorted regardless of +which language inserted what. + +## Cross-check, three ways + +**The syscalls, under `strace`.** One C++ `set`, filtered to the calls the +source claims (loader noise trimmed): + +```console +[host]$ strace -e trace=openat,mmap,msync,munmap ./cpp/build/release/app set demo.kv alpha a2 +openat(AT_FDCWD, "demo.kv", O_RDWR) = 3 +mmap(NULL, 1034, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7ff82e437000 +msync(0x7ff82e437000, 1034, MS_SYNC) = 0 +munmap(0x7ff82e437000, 1034) = 0 +``` + +Exactly the lifecycle the code promises: map 1034 bytes `MAP_SHARED`, one +`MS_SYNC` over the whole store before success is reported, unmap before the +fd closes. The Go binary's trace shows the identical four lines (its fd is 4 — +the runtime holds extras). + +**The mapping, in `/proc`.** `set` exits too fast to inspect, so inject a 3 s +delay into `msync` and read the paused process's map table: + +```console +[host]$ strace -o /dev/null -e trace=msync -e inject=msync:delay_enter=3000000 \ + ./cpp/build/release/app set demo.kv alpha a2 & +[host]$ grep demo.kv /proc//maps +7ff342434000-7ff342435000 rw-s 00000000 00:24 24538261 .../demo.kv +``` + +The permissions column is the whole chapter in four characters: `rw-s` — +readable, writable, **s**hared. A `MAP_PRIVATE` mapping shows `p` in that +position (look at your own binary's lines in the same file). The 1034-byte +store occupies one 4096-byte page range: mappings are page-granular, and the +file's last partial page is zero-padded in memory. + +**The format, by `cmp`.** A second store built by the *C++* binary replaying +the identical operations (`create --slots 4`, then the same four `set`s) is +byte-identical to the Rust-created, Go-amended one — `cmp demo.kv twin.kv` +prints nothing and exits 0, zero padding included. That, plus the three +byte-identical `dump` outputs, is what `verify.lua` mechanizes: 48 checks per +language, `PASS 48 / FAIL 0` for cpp, go, and rust in this session's run. + +> **On the lab VM** unverified — +> the eBPF view of this chapter is watching page faults and writeback +> fleet-wide (`bpftrace` on the `page-faults` and `writeback` tracepoints) +> instead of per-process `strace`; bcc-tools are not runnable on this host +> without privileges, and the Debugging part exercises exactly that on the +> `systems-target` VM. + +## What you learned + +- `mmap` is a 2×2 — file-backed vs anonymous, `MAP_SHARED` vs `MAP_PRIVATE` — + and `MAP_PRIVATE`'s copy-on-write means your first store to a page forks it: + the file keeps `SHKV1` while your view says `XXXXX`. +- Visibility and durability are separate: shared mappers see each other's + stores instantly through common page-cache pages, but only + `msync(MS_SYNC)` — visible in the strace as one call over the store — + guarantees the bytes reached disk before success is reported. +- The `ftruncate`-then-`mmap` ritual sizes the mapping, gets zero-filled + "empty" slots for free, and avoids `SIGBUS` past EOF; `memfd_create` plus + `F_SEAL_SHRINK`/`F_SEAL_GROW`/`F_SEAL_FUTURE_WRITE` (seals read back as 22) + makes an anonymous, truncation-proof variant. +- Cross-language interop comes from defining bytes, not structs: stated + offsets, NUL padding, and a u32 LE assembled bytewise dodge every repr, + padding, and endianness rule — and `cmp` proving two stores byte-identical + is the test that format survived three implementations. +- The unsafe boundary is the same fact three ways: Rust makes you write down + the promises (`unsafe MmapMut::map_mut`), C++ lets a `void*` into shared + memory look like any other pointer, and Go disguises the mapping as a + `[]byte` you must never touch after `Munmap`. + +Next, **allocators and GC runtimes**: what actually happens between your +`new`/`make`/`Box` and the anonymous mappings this chapter taught you to see. + +--- + +

verified — every number and +output excerpt above was produced on the Fedora 44 reference host this +session: the runner printed 16-mmap-and-shared-mappings PASS PASS +PASS (3 passed, 0 failed, 0 skipped) and verify.lua +logged PASS 48 / FAIL 0 for each language; the interop +transcript is real (created demo.kv: 4 slots, 1034 bytes, the +od header 53 48 4b 56 31 00 04 00 00 00, Go's +gadd read back as g1 by C++, the key-sorted dump +with 3/4 slots used); cmp confirmed the Rust-built +and C++-built store files byte-identical; the strace excerpt shows the real +mmap(..., MAP_SHARED, 3, 0) / msync(..., MS_SYNC) +pair; the delay-injected run yielded the live rw-s line from +/proc/<pid>/maps; and both asides (MAP_PRIVATE COW, +memfd_create + sealing with seals 22 and EPERM on a new writable map) ran +live under python3. The "On the lab VM" eBPF callout is unverified as +marked, as are writeback-timing defaults on other kernels.

diff --git a/_docs/17-allocators-and-gc-runtimes.md b/_docs/17-allocators-and-gc-runtimes.md new file mode 100644 index 0000000..9ebb241 --- /dev/null +++ b/_docs/17-allocators-and-gc-runtimes.md @@ -0,0 +1,478 @@ +--- +title: "Allocators and GC runtimes" +order: 17 +part: "Memory" +description: "What actually happens between your allocation and the kernel's pages: glibc malloc's arenas and tcache, what pmr and bumpalo really change, sync.Pool as Go's arena substitute, and the Go GC's pacer driven live with GOGC and GOMEMLIMIT — cross-checked with massif, /usr/bin/time, ltrace, and pprof." +duration: 50 minutes +--- + +The last two chapters dealt in wholesale memory: pages from the kernel, mapped +and shared with `mmap`. This chapter is retail. Between your `std::string`, +your `strconv.Itoa`, your `format!` and those pages sits an allocator — glibc +malloc for C++ *and* (this surprises people) for Rust, and a garbage-collected +runtime heap for Go — and the one new idea here is that the axis that +separates them is not allocation but **reclamation**: free every object +individually, free a whole batch at once, or free nothing and let a collector +trace what survived. `allocbench` runs the same allocation-heavy workload +through all three strategies in all three languages and prints the two numbers +that let you compare them: the process's peak RSS and the wall time — plus, +for Go only, how many GC cycles the churn cost. + +The code is in `examples/17-allocators-and-gc-runtimes/`. `./demo.sh` builds +all three implementations and runs the workload; the `README.md` there +specifies the CLI, the one-line report format, and the exact behavior contract +the three binaries share. + +{% include excalidraw.html + file="17-arena-vs-gc" + alt="Three columns compare reclamation strategies for the same 200,000 short-lived strings. Left, free one by one: objects flow into glibc's main arena and tcache with 64 cached chunks per size class, pages stay with the process (mallinfo2 reports arena 6340KB after everything is freed), peak RSS 3908KB for C++ and 2548KB for Rust with one free per object. Middle, arena and pool: a reusable 256KiB slab with a bump pointer, survivors copied out, the whole batch reclaimed in O(1) by reset, peak RSS 4240KB C++ and 2544KB Rust with zero per-object frees. Right, tracing GC: the churn becomes garbage the collector must find, the pacer triggers on GOGC growth or the GOMEMLIMIT soft limit, concurrent mark and sweep runs 9 cycles by default for 8064KB peak, or 47 cycles at 5676KB with GOMEMLIMIT=8MiB." + caption="Figure 17.1 — three ways to take memory back: per-object free through glibc malloc, wholesale arena release, and traced collection under a pacer; every number is from an allocbench run on the reference host" %} + +> **Tools used** — `valgrind --tool=massif` + `ms_print`, `/usr/bin/time`, +> `ltrace`, `nm`, `gcc`, `go tool pprof`, `go doc` (host); `memleak` from +> bcc-tools (lab VM, exercised in Part 8). All host tools are checked by +> `scripts/check-host.sh` or ship with the pinned toolchains. + +## What malloc does with your free + +`free(3)` almost never gives memory back to the kernel. glibc's allocator +carves the pages it got from `brk`/`mmap` into an **arena** — a heap region +plus the bookkeeping to subdivide it — and a freed chunk goes onto a list to +be handed out again: first into the calling thread's **tcache** (a tiny +per-thread stash, by default up to 64 chunks in each of 64 size classes, +touchable without any lock), then into the arena's small/large bins. The pages +themselves stay with the process, which is exactly what RSS measures and why +"my program freed everything but RSS didn't drop" is normal, not a leak. + +You can watch this bookkeeping directly with `mallinfo2(3)` — a ten-line probe +(allocate 100,000 48-byte chunks, free every other one, then the rest) prints, +on this host: + +```console +[host]$ gcc -O2 -o minfo minfo.c && ./minfo +start: arena=0KB in-use=0KB free-on-list=0KB +allocated: arena=6340KB in-use=6254KB free-on-list=85KB +holey: arena=6340KB in-use=3130KB free-on-list=3209KB +all freed: arena=6340KB in-use=5KB free-on-list=6334KB +``` + +The `holey` line is **fragmentation** in one row: half the bytes are free, but +they are interleaved hole-by-hole with live chunks, so the arena cannot shrink +— 6340KB of address space held to service 3130KB of live data. This recycling +is also why the C++ default variant below stays so flat: 200,000 short-lived +strings churn through the *same* few chunks, tcache-fast, and peak RSS barely +notices. (An aside: `mallinfo2` is glibc-specific and its counters are +process-wide approximations — use it for intuition, `getrusage` for numbers +you report.) + +## Arenas: pay once, free once + +The arena idea removes per-object reclamation entirely: allocate by bumping a +pointer through a slab, never free individuals, release the whole slab in one +move when the batch of work is done. C++23 spells it +`std::pmr::monotonic_buffer_resource` — a memory resource whose `deallocate` +is deliberately a **no-op**; everything comes back when the resource is +destroyed. What pmr actually changes is therefore two things and only two +things: **allocation locality** (the batch's strings sit contiguously in one +slab instead of scattered across malloc chunks) and **free-at-once** (batch +teardown is one release, not a thousand `free` calls). It does not magically +lower your high-water mark — the slab is a fixed cost you always touch, and +the numbers below show it. + +Rust's story starts with a fact worth proving rather than asserting: the +default `GlobalAlloc` on Linux **is malloc**. The release binary imports it +from glibc — + +```console +[host]$ nm -D rust/target/release/app | grep -wE 'malloc|free|realloc|posix_memalign' + U free@GLIBC_2.2.5 + U malloc@GLIBC_2.2.5 + U posix_memalign@GLIBC_2.2.5 + U realloc@GLIBC_2.2.5 +``` + +— and `ltrace -x malloc -c` counts 415 live `malloc` calls for a 100-iteration +default run against 317 for the arena run (at tiny N the two copies into the +long-lived index dominate; the churn savings grow with N). For the arena +variant the example uses `bumpalo 3.20.3`: a `Bump` is the same bump-pointer +design, `bump.reset()` is the O(1) wholesale release, and the borrow checker +statically guarantees nothing bump-allocated survives the reset. Go gets +neither pmr nor bumpalo — the language's `arena` experiment stalled — so the +idiomatic third spelling is `sync.Pool`: churn objects are *recycled* rather +than batch-freed, which attacks the same cost (garbage pressure) from the +other side. + +## The GC's bargain, with the knobs live + +Go's collector trades your CPU for your RSS on a schedule called the pacer, +and its two knobs are environment variables, so you can renegotiate the +bargain without recompiling. `go doc runtime` on this host's go1.26.5: *"The +GOGC variable sets the initial garbage collection target percentage. A +collection is triggered when the ratio of freshly allocated data to live data +remaining after the previous collection reaches this percentage."* And +GOMEMLIMIT *"sets a soft memory limit for the runtime"* that includes the heap +and everything else the runtime manages. Here is the same binary, four +policies, run back to back on this host: + +```console +[host]$ ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=8064KB ms=47 gc_cycles=9 +[host]$ GOGC=25 ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=5516KB ms=63 gc_cycles=32 +[host]$ GOGC=off GOMEMLIMIT=8MiB ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=5676KB ms=60 gc_cycles=47 +[host]$ GOGC=off GOMEMLIMIT=64MiB ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=37164KB ms=50 gc_cycles=0 +``` + +Read it as a supply curve: 9 cycles at 8064KB by default; four times the +cycles (32) buys the peak down to 5516KB at a wall-time cost; with pacing off, +an 8MiB soft limit alone drives 47 cycles to stay under it; and a 64MiB limit +the workload never approaches means the collector *never fires* — zero cycles, +and the garbage piles up to 37164KB of RSS. That last line is the sharpest +lesson: a GC's memory use is a policy outcome, not a property of your code. + +One note on *which* collector: go1.26.5's Green Tea GC. On this host the +toolchain accepts `GOEXPERIMENT=greenteagc` and `nogreenteagc` (and rejects +made-up names), and the version stamps tell you the default: a +`nogreenteagc` build is marked `go1.26.5-X:nogreenteagc` — a deviation — +while a `greenteagc` build stamps plain `go1.26.5`, meaning Green Tea *is* +this toolchain's default state. `go doc runtime` here documents GOGC and +GOMEMLIMIT but nothing Green-Tea-specific, so that observable stamp is as far +as this chapter's claims go. + +## How the code works + +Two structures anchor all six builds. The long-lived survivor is a plain hash +map — `std::unordered_map`, `map[string]string`, +`HashMap` — deliberately ordinary, because the chapter is +about the churn, not the index. The churn carrier is what varies: C++ holds +each batch's intermediates in a `std::pmr::vector` wired to +a `monotonic_buffer_resource` over one reusable 256KiB slab (a +`std::vector` allocated once, outside the loop); Rust holds +`bumpalo::collections::String`s in a `BumpVec` inside a `Bump`; Go pools a +`scratch` struct of three `bytes.Buffer`s plus a `[]byte` for +`strconv.AppendInt`. The scratch containers exist so the *default* variant +also keeps a batch's fragments alive to batch end — both variants hold the +same live set, so their peaks are comparable and only reclamation differs. + +Here is the arena-side builder in each language, verbatim from +`examples/17-allocators-and-gc-runtimes/{cpp/src/main.cpp,go/main.go,rust/src/main.rs}`: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +// arena variant: same shape, but the intermediates come from a monotonic +// buffer over one reusable slab and are released wholesale per batch. +[[nodiscard]] Index build_arena(long allocs) { + std::vector slab(kSlabBytes); // reused by every batch + Index index; + index.reserve(kKeyspace); + for (long start = 0; start < allocs; start += kBatch) { + const long end = std::min(start + kBatch, allocs); + std::pmr::monotonic_buffer_resource arena{slab.data(), slab.size()}; + std::pmr::vector scratch{&arena}; + scratch.reserve(static_cast(end - start)); + for (long i = start; i < end; ++i) { + std::pmr::string key{&arena}; + std::pmr::string frag{&arena}; + std::pmr::string value{&arena}; + make_kv(i, key, frag, value); + index.insert_or_assign(std::string{key}, std::string{value}); + scratch.push_back(std::move(frag)); + } + } // ~arena: the whole batch vanishes at once; no per-object frees + return index; +} +``` + +```go +// buildArena: same logical work, but the intermediates live in pooled +// buffers that are recycled instead of collected. Only the final key/value +// strings are copied out into the long-lived index. +func buildArena(allocs int) map[string]string { + index := make(map[string]string, keyspace) + for i := 0; i < allocs; i++ { + s := scratchPool.Get().(*scratch) + s.reset() + idx := i % keyspace + s.key.WriteString("key-") + s.writeNum(&s.key, idx) + s.frag.WriteString("value-") + s.writeNum(&s.frag, idx) + s.frag.WriteByte('-') + s.writeNum(&s.frag, i%97) + s.frag.WriteByte('/') + for r := 0; r < repeat; r++ { + s.value.Write(s.frag.Bytes()) + } + index[s.key.String()] = s.value.String() + scratchPool.Put(s) + } + return index +} +``` + +```rust +/// arena variant: same shape, but the intermediates are bump-allocated and +/// the whole batch is reclaimed at once by `bump.reset()`. +fn build_arena(allocs: usize) -> HashMap { + let mut bump = Bump::with_capacity(SLAB_BYTES); + let mut index = HashMap::with_capacity(KEYSPACE); + let mut start = 0; + while start < allocs { + let end = (start + BATCH).min(allocs); + { + let mut scratch: BumpVec = BumpVec::with_capacity_in(end - start, &bump); + for i in start..end { + let idx = i % KEYSPACE; + let mut key = BumpString::new_in(&bump); + write!(key, "key-{idx}").expect("bump string write cannot fail"); + let mut frag = BumpString::new_in(&bump); + write!(frag, "value-{idx}-{}/", i % 97).expect("bump string write cannot fail"); + let mut value = BumpString::new_in(&bump); + for _ in 0..REPEAT { + value.push_str(frag.as_str()); + } + index.insert(key.as_str().to_owned(), value.as_str().to_owned()); + scratch.push(frag); // keeps the churn alive per batch + } + } // borrows on the bump end here... + bump.reset(); // ...so the whole batch vanishes in O(1) + start = end; + } + index +} +``` + +Walk the C++ one first. The slab lives *outside* the batch loop and the +`monotonic_buffer_resource` is constructed *inside* it, over +`slab.data()/slab.size()`: each batch gets a fresh bump pointer into the same +already-touched pages, so batch teardown (`~arena` at the closing brace) frees +1000 iterations of strings without a single `free` call, and the next batch +pays no new page faults. The `scratch.reserve` is load-bearing in a way it +usually isn't: a monotonic resource never reclaims, so letting the vector grow +geometrically would leave every outgrown copy as dead weight *inside the +slab*. Same reason `make_kv` builds numbers with `std::to_chars` via +`append_num` — `std::to_string` would hand back an ordinary heap +`std::string`, silently routing churn around the arena. The survivors are +copied out explicitly (`std::string{key}`) because the index must outlive the +resource; forgetting that copy is the classic pmr lifetime bug, and it is the +exact discipline in one line: *bulk-free the churn, copy out what survives.* + +The Rust version encodes the same rules in types instead of comments. +`BumpString::new_in(&bump)` borrows the arena, so the inner block must end — +every borrow dropped — before `bump.reset()` will compile. Where the C++ +version would let you keep a `pmr::string` past `~arena` and crash, here the +borrow checker rejects the program. `reset()` keeps bumpalo's largest chunk, +so after the first batch the steady state is the same reused-slab pattern as +C++. The Go version has no batch boundary at all: `Get` → `reset` → build in +place → copy out via `String()` → `Put`, per iteration. `writeNum` reuses one +`[]byte` through `strconv.AppendInt` so digit formatting allocates nothing; +the only per-iteration allocations left are the two `String()` copies that the +index keeps — visible below in the heap profile, and the reason `gc_cycles` +drops from 9 to 3. + +The wiring is the book's standard shape: a hand-rolled flag loop +(`--allocs`, `--variant`) so all three usage diagnostics are byte-identical; +the build+query phase timed with a monotonic clock; `getrusage(RUSAGE_SELF)` +read afterwards for `ru_maxrss` (kilobytes on Linux — it is bytes on the +BSDs, one of the fragile bits stated plainly); and Go additionally reads +`runtime.MemStats` before and after, reporting the `NumGC` delta so the GC +cost of *this workload* — not the process's whole life — is what prints. The +query phase re-derives every key and fails on a missing key or a zero byte +sum, so neither compiler nor collector can quietly optimize the work away. +Two more fragile bits: the 256KiB slab is sized to a batch, and a +monotonic resource that overflows silently falls through to its upstream +(the default resource — malloc), correct but no longer arena-fast; and +`ru_maxrss` is a high-water mark, so it can never show the arena *releasing* +memory — that is what massif is for below. + +## Errors, three ways + +The contract: bad usage (unknown flag, non-positive `--allocs`, unknown +variant) prints a diagnostic plus the usage line on stderr and exits 2; +runtime failures print `allocbench: ...` and exit 1. Running all three with +`--variant marble` on this host produced byte-identical stderr — confirmed +with `cmp` across the three captures — and exit 2 from each: + +```console +[host]$ ./cpp/build/release/app --variant marble +allocbench: unknown variant: marble +usage: allocbench [--allocs N] [--variant default|arena] +``` + +The mechanisms differ by language, the policy doesn't. C++ parses into +`std::expected` — a usage error is a *value* carried to +`main`, which owns both stderr lines and the exit code; the one true runtime +error path (`getrusage` failing) converts `errno` into `std::error_code` +first. Go returns wrapped errors (`fmt.Errorf` with `%w`) from `parseArgs` +and `run`, and `main` chooses exit 2 or 1 by *which call* failed rather than +by inspecting strings. Rust splits the same way structurally: +`parse_args() -> Result` for usage, `anyhow::Result` with +`.context()` for runtime, printed with `{err:#}` so a failure would show the +whole chain (`getrusage: ...`) on one line. Same taxonomy in all three: usage +errors are boring values, runtime errors carry their cause. + +## Concurrency lens + +Everything in this chapter is single-threaded, and almost none of it is +thread-neutral. glibc malloc scales by giving *threads* their own arenas (up +to 8x the core count) plus the lock-free tcache — which is why a +multi-threaded allocation-heavy program's RSS can multiply: each arena +fragments separately. `monotonic_buffer_resource` is explicitly *not* +thread-safe; the pattern that survives contact with threads is one arena per +thread or per task, never one shared bump pointer. bumpalo says the same +thing in the type system: `Bump` is not `Sync`, so two threads cannot even +share a reference. `sync.Pool` is the one built for concurrency — it shards +per-P (per scheduler processor) so `Get`/`Put` on different cores don't +contend, and the GC empties it between cycles, which is precisely why it +suits churn-recycling and not long-term caching. And the Go collector itself +is concurrent: those 9 (or 47) cycles ran on other cores while `buildDefault` +kept allocating, so `gc_cycles` costs less wall time than it suggests — but +`GOMEMLIMIT` is global, so in a real service every goroutine shares the same +bargain you tuned above. + +## Build, run, observe + +```bash +[host]$ cd examples/17-allocators-and-gc-runtimes && ./demo.sh +``` + +Each language builds and runs both variants. Driven by hand on this host, +one line per run: + +```console +[host]$ ./demo.sh cpp run +allocbench: variant=default allocs=200000 peak_rss=3908KB ms=18 +[host]$ ./demo.sh cpp run --variant arena +allocbench: variant=arena allocs=200000 peak_rss=4240KB ms=20 +[host]$ ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=8064KB ms=47 gc_cycles=9 +[host]$ ./demo.sh go run --variant arena +allocbench: variant=arena allocs=200000 peak_rss=7212KB ms=33 gc_cycles=3 +[host]$ ./demo.sh rust run +allocbench: variant=default allocs=200000 peak_rss=2612KB ms=45 +[host]$ ./demo.sh rust run --variant arena +allocbench: variant=arena allocs=200000 peak_rss=2424KB ms=45 +``` + +Three readings. The C++ arena's peak is *higher* (4240 vs 3908KB): malloc +recycled the default variant's churn through the same tcache chunks, while +the always-touched slab is a fixed cost — the arena's win is deterministic +wholesale release, not a lower high-water mark at this size. Go shows the GC +account directly: recycling via `sync.Pool` cut cycles 9 → 3 and time 47 → +33 ms. Rust's arena win is wall time — on repeated runs the default settles +around 39–45 ms and the arena at 26–27 ms (the tied 45/45 pair above caught +a cold first run). The runner +(`python3 scripts/test-all-examples.py --only 17-allocators-and-gc-runtimes`) +verified the full behavior contract this session: `PASS PASS PASS`, 3 passed, +0 failed, 0 skipped. + +## Cross-check: massif, /usr/bin/time, pprof + +**Heap shape, under massif.** `ru_maxrss` is one number; valgrind's massif +records the whole heap-over-time curve (at ~20x slowdown, so 20,000 allocs). +Peak snapshots from `ms_print`, default first: + +```console +[host]$ valgrind --tool=massif --massif-out-file=massif-default.out \ + ./cpp/build/release/app --allocs 20000 +[host]$ ms_print massif-default.out +Number of snapshots: 74 + Detailed snapshots: [5 (peak), 12, 38, 41, 45, 49, 59, 69] + n time(i) total(B) useful-heap(B) extra-heap(B) stacks(B) + 5 3,960,468 273,800 254,786 19,014 0 +93.06% (254,786B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc. +->29.16% (79,840B) 0x402BD6: allocate (new_allocator.h:162) +| ->29.16% (79,840B) 0x402BD6: insert_or_assign > (unordered_map.h:786) +| ->29.16% (79,840B) 0x402BD6: build_default (main.cpp:141) +``` + +(Intermediate frames of the allocation tree trimmed.) + +The default peak is 273,800 bytes of many small blocks, the largest tree +being the index's hashtable nodes out of `build_default`. The arena run's +peak (snapshot 4 of 59) is 504,000 bytes — *larger*, matching the RSS story — +and 52.01% of it is a single 262,144-byte block: `_M_create_storage` → +`build_arena (main.cpp:151)`, the 256KiB slab, allocated once. The per-string +churn has vanished from malloc's ledger entirely, because inside the slab it +is pointer arithmetic — which is the pmr claim (locality plus free-at-once) +made visible by an independent tool. + +**RSS agreement, with `/usr/bin/time -v`.** The kernel's accounting of the +same runs: the Go binary's self-reported `peak_rss=8320KB` matched `Maximum +resident set size (kbytes): 8320` exactly (both read the same `ru_maxrss`, +one via `getrusage`, one via `wait4`). The C++ pair read 3916 self vs 4388 +from time, and 4312 vs 4356 for arena — the binary samples before printing +and process teardown, time observes the whole life; same ordering, sub-500KB +skew, no surprises. + +**Where the Go bytes came from, with pprof.** Adding a temporary ten-line +`MEMPROFILE` hook (`runtime.GC()` then `pprof.WriteHeapProfile`) to a scratch +copy of `main.go` and reading the profiles with +`go tool pprof -sample_index=alloc_space -top`: + +```console +[host]$ go tool pprof -sample_index=alloc_space -top -nodecount=3 app heap-default.pb.gz +Showing nodes accounting for 41474.84kB, 100% of 41474.84kB total + flat flat% sum% cum cum% +38913.61kB 93.82% 93.82% 40449.63kB 97.53% main.buildDefault + 1536.02kB 3.70% 97.53% 1536.02kB 3.70% internal/strconv.FormatInt + 513kB 1.24% 98.77% 513kB 1.24% runtime.mallocgc +``` + +The default variant allocated 41.5MB total, 93.82% of it flat in +`buildDefault` — the churn. The arena profile totals 13.3MB, topped by +`bytes.(*Buffer).String` at 10752.55kB (80.76%): almost exactly the two +surviving copies per iteration, with the churn recycled through the pool. +Three tools, three vantage points — allocator ledger, kernel RSS, runtime +profile — one consistent story. + +> **On the lab VM** unverified — +> the eBPF view of this chapter is allocator tracing without recompiling: +> `[vm]$ /usr/share/bcc/tools/memleak -p ` samples outstanding +> allocations by stack, kernel-side, on any binary. bcc-tools need root and +> a matching kernel, so they run on the `systems-target` VM, and Part 8 +> exercises them there against these exact binaries. + +## What you learned + +- `free` returns chunks to glibc's arena and tcache, not pages to the kernel + — `mallinfo2` showed 6340KB of arena still held with 5KB in use, and a + half-freed heap is fragmentation you can read out (`in-use 3130KB` + interleaved with `free-on-list 3209KB`). +- An arena changes two things — allocation locality and free-at-once — and + massif proved both: the churn collapsed into one 262,144-byte slab + allocation, while peak RSS stayed *comparable* (4240 vs 3908KB) because + malloc recycles churn well too. +- Rust's default allocator is glibc malloc (`nm -D` shows the imports; + `ltrace` counts the calls), so everything in the malloc story applies until + you opt into `bumpalo` — where `reset()` is O(1) and the borrow checker + enforces the copy-out-what-survives rule that pmr leaves to discipline. +- Go's GC is a negotiable policy: the same binary ran 9 cycles at 8064KB, 47 + cycles at 5676KB under `GOMEMLIMIT=8MiB`, and 0 cycles at 37164KB when the + limit was never approached — and `sync.Pool` is the arena-shaped tool that + cut the default workload's cycles from 9 to 3. + +Next, **pipes, FIFOs, and splice**: the oldest IPC primitive on the system, +and how to move bytes between processes without copying them through +userspace at all. + +--- + +

verified — every number above +was produced on the Fedora 44 reference host (kernel 7.1.3-200.fc44) this +session: the runner printed 17-allocators-and-gc-runtimes PASS PASS +PASS (3 passed, 0 failed, 0 skipped); the six variant lines, the four +GOGC/GOMEMLIMIT runs (gc_cycles 9/32/47/0), the mallinfo2 probe output, the +nm -D imports, the ltrace counts (415 vs 317 mallocs), the +massif peak snapshots (273,800B default, 504,000B arena with the 262,144B +slab block at main.cpp:151), the /usr/bin/time -v +agreements (8320KB == 8320KB for Go), and the pprof totals (41474.84kB vs +13314.77kB) are all real captured output. The Green Tea note claims only +what this host's go1.26.5 toolchain shows: greenteagc builds +carry no experiment deviation stamp while nogreenteagc does. +Bad usage was exercised on all three binaries: byte-identical stderr +(cmp-confirmed) and exit 2. The memleak callout is +unverified as marked and deferred to Part 8.

diff --git a/_docs/18-pipes-fifos-splice.md b/_docs/18-pipes-fifos-splice.md new file mode 100644 index 0000000..782f594 --- /dev/null +++ b/_docs/18-pipes-fifos-splice.md @@ -0,0 +1,526 @@ +--- +title: "Pipes, FIFOs, and splice" +order: 18 +part: "IPC" +description: "pmon v4 turns the supervisor into a log pipeline: the pipe as a ring of kernel pages with its capacity read live via F_GETPIPE_SZ, what PIPE_BUF atomicity does and does not promise, FIFO open semantics and the ENXIO trick, SIGPIPE/EPIPE as the writer's contract, and splice moving log bytes into a FIFO without a userspace copy — proven with strace, /proc, and a 5.3 GB/s dd run." +duration: "45 minutes" +--- + +This part of the book is about processes talking to each other, and it opens +with the primitive every shell has used since 1973. `pmon` ended the process +part supervising a crashing child with two byte-identical engines — pidfd and +sigchld — but it threw the child's output away, and a supervisor that +discards logs is half a supervisor. Version 4 captures the child's stdout and +stderr into a framed log file and then republishes that log through a named +pipe to any reader that wants it, surviving readers that come and go. The one +new idea is that a pipe is not a file with two ends: it is a bounded ring of +kernel pages, and every behavior this chapter meets — the 65536-byte +capacity, `PIPE_BUF` atomicity, EOF as "last write end closed", `SIGPIPE` on +a readerless write, even `splice(2)`'s zero-copy trick — falls straight out +of that ring. + +The code is in `examples/18-pipes-fifos-splice/`. `./demo.sh` there builds +all three implementations and runs a short capture demo; its `README.md` +specifies the CLI, the log record format, and the exit codes all three +languages share. + +{% include excalidraw.html + file="18-pipe-internals" + alt="Three panels: a writer process box holding fd 1 the write end, a kernel band labelled struct pipe_inode_info inode 16462630 containing a row of eight page slots from the sixteen-slot ring with head and tail pointers marked, and a reader process box holding fd 5 the read end with fdinfo flags 02000000; notes record the 65536-byte capacity read via F_GETPIPE_SZ, the F_SETPIPE_SZ rounding of 1 up to 4096, the 1048576 pipe-max-size ceiling, and the warning that PIPE_BUF atomicity is not message framing." + caption="Figure 18.1 — one pipe from this chapter's live supervise run: the child's fd 1 and the supervisor's fd 5 share the ring behind pipe inode 16462630; every number is from /proc or a live fcntl" %} + +> **Tools used** — `strace`, `mkfifo`, `dd`, `stat`, `getconf`, `ls`/`cat` +> over `/proc`, `python3` (host); `bpftrace`/bcc-tools (lab VM, exercised in +> Part 8). Everything here is checked by `scripts/check-host.sh`, ships with +> Fedora, or is preinstalled in the lab VMs. + +## A pipe is a ring of pages + +`pipe2(2)` allocates a `pipe_inode_info` in the kernel — an array of sixteen +`pipe_buffer` slots, each holding one page — and returns two fds onto it: +slot 0 reads from the ring's tail, slot 1 writes at its head. Nothing about +this is a file. There is no offset (`lseek` fails with `ESPIPE`), no name, +and no storage: a full ring blocks the writer, an empty ring blocks the +reader, and the kernel wakes each side as the other makes progress. That +back-pressure is the feature — a slow log consumer stalls the producer +instead of eating unbounded memory. + +The capacity is not folklore; you can ask for it. `fcntl(F_GETPIPE_SZ)` +reads it live, and `F_SETPIPE_SZ` resizes in whole pages: + +```bash +[host]$ python3 -c ' +import fcntl, os +r, w = os.pipe2(os.O_CLOEXEC) +print("capacity:", fcntl.fcntl(w, 1032)) # F_GETPIPE_SZ +print("PIPE_BUF:", os.pathconf(w, "PC_PIPE_BUF")) +fcntl.fcntl(w, 1031, 1) # F_SETPIPE_SZ +print("asked for 1, got:", fcntl.fcntl(w, 1032))' +capacity: 65536 +PIPE_BUF: 4096 +asked for 1, got: 4096 +[host]$ cat /proc/sys/fs/pipe-max-size +1048576 +``` + +Sixteen 4096-byte pages, 65536 bytes; ask for a 1-byte pipe and the kernel +rounds up to one page; unprivileged growth stops at +`/proc/sys/fs/pipe-max-size` (1048576 here). `pmon` uses `pipe2` rather than +`pipe` for the same reason chapter 7 put `O_CLOEXEC` in every open: the flag +lands atomically, with no window for a concurrently spawned child to inherit +the fd. `strace -f -e trace=pipe2` on the C++ supervisor shows exactly two +calls per attempt — `pipe2([5, 6], O_CLOEXEC) = 0` and +`pipe2([7, 8], O_CLOEXEC) = 0` — one pipe for the child's stdout, one for +its stderr. + +## `PIPE_BUF`, and what it does not promise + +POSIX guarantees that a single `write(2)` of at most `PIPE_BUF` bytes — 4096 +on Linux, read out above with `pathconf` — is atomic: its bytes land in the +ring contiguously, never interleaved with another writer's bytes. That +guarantee is narrower than it sounds, and the gap is why v4's capture +architecture exists. Atomicity is *not framing*: the reader can still `read` +half a message, because reads have no atomicity at all. It is *not a promise +about large writes*: one `write` of 8000 bytes may be split anywhere, with +another writer's data landing in the middle. And it is *not a promise about +your language's buffering*: a child that "prints a line" may reach the +kernel as several small writes or one giant flush, at the whim of its stdio +library — a supervised process is exactly the kind of writer you cannot +retrain. + +So `pmon` never relies on write atomicity to keep `[out]` and `[err]` +records apart. The tempting shortcut — `dup2` both child streams onto *one* +pipe and let `PIPE_BUF` sort it out — loses the out/err distinction forever +and still interleaves mid-line whenever a write exceeds 4096 bytes or a line +crosses two writes. Instead each stream gets its own pipe, and a userspace +relay loop reassembles byte chunks into complete lines before stamping the +prefix. Interleaving still happens — but only at line granularity, chosen by +the relay, never mid-record. That relay loop is the real price +of framed capture: the kernel moves bytes; meaning is your job. + +## FIFOs: a pipe with a name + +`supervise` and its child share ancestry, so inheriting pipe ends works. A +log *consumer* started an hour later from another terminal shares nothing — +it needs a rendezvous point with a name. `mkfifo(3)` creates one: an inode +of type `S_IFIFO` whose only content is a pipe ring materialized in the +kernel when both ends open it. `stat` reports our created FIFO as `fifo 644 +live2.fifo` — a filesystem name, zero bytes of storage. + +Opening a FIFO has its own choreography, because a pipe with one end is +useless. Who blocks when: + +| `open(2)` mode | counterpart absent | counterpart present | +|---|---|---| +| `O_RDONLY` | **blocks** until a writer opens | returns at once | +| `O_WRONLY` | **blocks** until a reader opens | returns at once | +| `O_RDONLY\|O_NONBLOCK` | returns at once (reads give `EAGAIN`/EOF later) | returns at once | +| `O_WRONLY\|O_NONBLOCK` | **fails with `ENXIO`** | returns at once | +| `O_RDWR` | returns at once (Linux behavior; POSIX leaves it undefined) | returns at once | + +The asymmetry in row four is deliberate kernel policy — a nonblocking +writer with no reader would otherwise instantly fill the ring and stall — +and `pmon tail` turns it into a feature: it opens the FIFO +`O_WRONLY|O_NONBLOCK|O_CLOEXEC`, treats `ENXIO` as "no reader yet, sleep +50 ms, retry", and on success clears `O_NONBLOCK` with `fcntl(F_SETFL)` so +the relay gets blocking back-pressure. The cleared bit is visible in +`/proc//fdinfo/4`: `flags: 02100001` decodes as `O_WRONLY` + +`O_LARGEFILE` + `O_CLOEXEC` — no `O_NONBLOCK` remaining. + +## `SIGPIPE`/`EPIPE`: the writer's contract + +The reader side of a pipe has a gentle end-of-conversation signal: when the +last write end closes, `read` returns 0, EOF. The writer side gets a +harsher one. Writing to a pipe whose last read end has closed raises +`SIGPIPE`, whose default action *terminates the process* — the design that +makes `head -1 < bigfile-producer` shut the producer down for free in a +shell pipeline. A long-lived relay must not die because one `cat` was +killed, so `cmd_tail` ignores `SIGPIPE` in all three languages, converting +the event into an `EPIPE` error return from `write`/`splice` at the exact +call that noticed. That is the writer's contract: handle `EPIPE` at every +write site, or inherit a default that kills you. `pmon` treats it as a +state transition — print `pmon: tail reader detached`, close the writer fd, +and go back to waiting for the next reader. + +{% include excalidraw.html + file="18-log-topology" + alt="Three horizontal bands. Top, pmon supervise pid 2633754: the sh child pid 2633756 writes fd 1 and fd 2 into two kernel pipe nodes with inodes 16462630 and 16462631, which feed a poll loop node listing signalfd 4, pidfd 6, and pipe read ends 5 and 7, appending framed out and err lines into a live.log node opened append-only as fd 3. Middle, pmon tail pid 2634759: a log fd 3 node with fdinfo pos 10 connects by an amber splice arrow labelled with the real splice call returning 22 to a FIFO writer fd 4 node whose flags 02100001 show O_NONBLOCK cleared. Bottom, kernel FIFO plus reader: the live.fifo inode of type S_IFIFO flows into a cat reader holding fd 3, with a note that a detached reader causes EPIPE and the relay re-splices undelivered bytes to the next reader." + caption="Figure 18.2 — pmon v4's log pipeline end to end; every pid, fd number, pipe inode, flag value, and splice return comes from the live runs traced in this chapter" %} + +## How the code works + +Two structures carry the capture. `Capture` owns one child's two pipe read +ends plus per-stream open/closed state; `LineRelay` owns the framing — a +byte buffer per stream that accumulates chunks, emits every completed line +to the log as `[out] line\n` or `[err] line\n` immediately (line-buffered, +so the log is useful while the child lives), bounds the partial-line buffer +at 64 KiB, and on EOF flushes any trailing partial line with a supplied +newline. Two relays, not one, because the *pipes* are two: bytes from +stdout can never contaminate an `[err]` record even if the child sprays +both streams at once. + +The wiring is where the invariants live. The C++ supervisor, per attempt: + +```cpp + Capture cap{*log}; + std::array outp{}, errp{}; + if (::pipe2(outp.data(), O_CLOEXEC) != 0 || ::pipe2(errp.data(), O_CLOEXEC) != 0) { + std::println(stderr, "pmon: pipe2: {}", last_error().message()); + return 1; + } + cap.out_r = Fd{outp[0]}; + Fd out_w{outp[1]}; + cap.err_r = Fd{errp[0]}; + Fd err_w{errp[1]}; + + const auto pid = spawn_child(cmd, out_w, err_w); +``` + +`spawn_child` wires the write ends to the child with +`posix_spawn_file_actions_adddup2(fa, out.get(), STDOUT_FILENO)` — and +`dup2` semantics do the O_CLOEXEC bookkeeping for us: the *duplicate* +created in the child has the close-on-exec bit clear, so the child's fds 1 +and 2 survive `exec` while every original supervisor-side fd vanishes from +the child. Two lines later comes the sentence this chapter exists to teach: + +```cpp + out_w.reset(); // parent's copies of the write ends must close, + err_w.reset(); // or EOF never arrives +``` + +EOF on a pipe means "*every* write-end fd is closed". The supervisor holds +copies of the write ends between `pipe2` and `posix_spawnp`; if it kept +them, the child could die and the read ends would stay open forever — the +supervisor would be holding its own pipe open, waiting on itself. Closing +the parent's copies immediately after spawn makes child-exit imply +pipe-EOF, which is what lets `drain_to_eof` terminate. + +From there the pipes simply join the multiplexing each engine already had. +C++ polls four fds — signalfd, both pipe read ends, and (pidfd engine) the +pidfd — marking a drained pipe's slot as `fd = -1`, which `poll(2)` +ignores. Rust builds the same four-slot `rustix::event::poll` set beside +its nix `SignalFd`, parking closed pipes with an empty event mask. Go +inverts idiomatically: `capture` wraps each pipe in a `bufio.Scanner` +goroutine feeding one channel of prefixed `logLine`s into the same `select` +that already multiplexes the pidfd poller, `signal.Notify`, and the +deadline; a `sync.WaitGroup` closes the channel when both scanners hit EOF, +so channel-closed is the Go spelling of drained-to-EOF. In every language +the reap path calls the drain *before* printing the exit observation — the +log is complete before `pmon: child=… exited status=…` appears, an +ordering `verify.lua` asserts. + +`tail` is the second half: create the FIFO (`EEXIST` is tolerated only if +the existing inode really is a FIFO — anything else is error, exit 1), open +the log read-only at offset 0 so a late reader gets the backlog, then relay +forever. The C++ and Rust relays move bytes with `splice(2)`; Go uses +`io.Copy` per reader session. The heart of each: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +// splice(2) fast path: move log bytes kernel-side into the pipe. The file +// offset only advances on success, so a detached reader (EPIPE) loses +// nothing — the same bytes are respliced for the next reader. +[[nodiscard]] std::expected +relay_splice(const Fd& log, const Fd& w) { + const ssize_t n = ::splice(log.get(), nullptr, w.get(), nullptr, kChunk, 0); + if (n > 0) { + return Relay::moved; + } + if (n == 0) { + return Relay::idle; + } + switch (errno) { + case EPIPE: + return Relay::detached; + case EINTR: + case EAGAIN: + return Relay::idle; + case EINVAL: + case ENOSYS: + return Relay::no_splice; // fs without splice support: fall back + default: + return std::unexpected(last_error()); + } +} +``` + +```go + // One copy session: on EPIPE (reader detached) io.Copy reports + // how many bytes actually reached the pipe, so the log offset is + // rewound to the first unwritten byte — the next reader loses + // nothing. + start, serr := logf.Seek(0, io.SeekCurrent) + if serr != nil { + w.Close() + fmt.Fprintf(os.Stderr, "pmon: seek %s: %v\n", logPath, serr) + return 1 + } + n, cerr := io.Copy(w, logf) + if cerr == nil { // caught up: poll for appended lines + time.Sleep(pollTick) + continue + } + if errors.Is(cerr, syscall.EPIPE) { + fmt.Println("pmon: tail reader detached") + if _, serr := logf.Seek(start+n, io.SeekStart); serr != nil { + w.Close() + fmt.Fprintf(os.Stderr, "pmon: seek %s: %v\n", logPath, serr) + return 1 + } + w.Close() + continue reader + } + w.Close() + fmt.Fprintf(os.Stderr, "pmon: relay: %v\n", cerr) + return 1 +``` + +```rust +/// splice(2) fast path: move log bytes kernel-side into the pipe. The file +/// offset only advances on success, so a detached reader (EPIPE) loses +/// nothing — the same bytes are respliced for the next reader. +fn relay_splice(log: &File, w: &OwnedFd) -> Result { + match splice(log, None, w, None, CHUNK, SpliceFlags::empty()) { + Ok(0) => Ok(Relay::Idle), + Ok(_) => Ok(Relay::Moved), + Err(rustix::io::Errno::PIPE) => Ok(Relay::Detached), + Err(rustix::io::Errno::INTR) | Err(rustix::io::Errno::AGAIN) => Ok(Relay::Idle), + Err(rustix::io::Errno::INVAL) | Err(rustix::io::Errno::NOSYS) => Ok(Relay::NoSplice), + Err(e) => Err(e).context("splice"), + } +} +``` + +`splice` exists because a pipe *is* a ring of page references: when one fd +of the pair is a pipe, the kernel can move data by wiring the file's +page-cache pages into `pipe_buffer` slots — no copy into userspace, no +copy back. Passing `NULL`/`None` for the file's offset pointer means "use +the fd's own offset and advance it", which is precisely the no-loss +mechanism: the offset moves only for bytes the pipe accepted, so `EPIPE` +mid-relay leaves it parked at the first undelivered byte, and the next +reader's first `splice` resends exactly from there. Go has no `splice` in +its portable API, so it makes the same promise arithmetically: remember +`start`, let `io.Copy` report `n` bytes delivered before the `EPIPE`, seek +back to `start+n`. (`io.Copy` between an `*os.File` pair actually tries +`copy_file_range`/`sendfile` internally — but to a FIFO it settles on +plain read/write, as the strace below confirms only for C++ and Rust.) The +C++/Rust fallback `relay_rw` keeps the identical rewind contract for +filesystems where `splice` returns `EINVAL`; a `tee(2)` aside completes +the family — it duplicates pipe contents to a second pipe without +consuming them, which is how you would split this log stream to two +FIFOs. + +The fragile bits, stated plainly: the 50 ms `sleep`-poll for both "log has +no new bytes" and "no reader yet" is deliberate simplicity — chapter 9's +inotify machinery could make both event-driven, and a real system would use +it; the FIFO relay serves one reader at a time (a second simultaneous +opener would interleave records with the first — FIFOs have one ring, not +one per reader); and log rotation is unhandled: `tail` follows an fd, not +a name, so a renamed-away log keeps feeding the old inode. Chapter 19 +replaces "one anonymous byte stream" with a real control plane. + +## Errors, three ways + +The chapter's errno vocabulary is small and every entry is *expected*, so +the interesting part is which errors are policy at the call site versus +fatal. `ENXIO` from a nonblocking FIFO open means "no reader yet": C++ +checks `fd.error().value() != ENXIO`, Go `errors.Is(err, unix.ENXIO)`, +Rust matches `Err(rustix::io::Errno::NXIO)` — all three sleep and retry. +`EPIPE` means "reader left": mapped to the `detached` state, never +propagated. `EEXIST` from `mkfifo` is acceptance-with-verification: stat +the path, require `S_ISFIFO` (C++), `fs.ModeNamedPipe` (Go), +`file_type().is_fifo()` (Rust), and only a non-FIFO squatter is fatal. +`EINTR` everywhere returns to the loop — `tail`'s stop handlers install +with no `SA_RESTART` precisely so a signal can interrupt a blocking call +and let the loop observe the stop flag. Everything unexpected carries its +context up to one printer and exit 1, same as every pmon before it: three +error mechanisms (`std::expected`, wrapped `error` values, `anyhow` +results), one policy. + +## Concurrency lens + +The capture is a small concurrency study. C++ and Rust stay +single-threaded by *adding fds to an existing poll set* — the same event +loop that watches for child exit watches for child output, so there is no +locking anywhere; the log write happens on the only thread. Go runs three +goroutines per attempt (two scanners, one WaitGroup-closer), and the +channel is what serializes: only the `select` loop's goroutine touches the +log file, so `[out]`/`[err]` records cannot tear even though the scanners +run concurrently. The deadlock the design must dodge is the classic +pipe-capture one: a child that fills both pipes while the supervisor waits +only for exit would stall forever — which is why output fds live in the +same poll set/select as the exit source, never behind it. Signal-wise, +`SIGPIPE` disposition is process-wide state: C++/Rust set `SIG_IGN` +explicitly; Go's `signal.Ignore(syscall.SIGPIPE)` does the same job with a +runtime subtlety — Go would otherwise re-raise a fatal SIGPIPE only for +writes to fds 1 and 2, and the FIFO writer is fd 3, but the explicit +ignore documents intent and covers the stdout prints too. And the +supervisor still resets the child's signal mask at spawn (chapter 12's +lesson): a captured child is still a child. + +## Build, run, observe + +```bash +[host]$ cd examples/18-pipes-fifos-splice && ./demo.sh +``` + +Each language builds and runs a capture self-demo. Driving it by hand the +way this chapter's evidence was produced — first the capture: + +```bash +[host]$ APP=examples/18-pipes-fifos-splice/cpp/build/release/app +[host]$ $APP supervise --max-restarts 2 --log pmon.log -- sh -c 'echo hello; echo oops >&2; exit 1' +pmon: engine=pidfd child=2632228 pidfd=6 +pmon: child=2632228 exited status=1 +pmon: restart 1/2 +pmon: child=2632229 exited status=1 +pmon: restart 2/2 +pmon: child=2632230 exited status=1 +pmon: giving up after 2 restarts +[host]$ cat pmon.log +[out] hello +[err] oops +[out] hello +[err] oops +[out] hello +[err] oops +``` + +(Spawn lines for attempts two and three trimmed.) Three attempts, six +framed records, stdout and stderr never crossed. Then the FIFO leg, +exercising detach and reattach: + +```bash +[host]$ : > live2.log && $APP tail --log live2.log --fifo live2.fifo & +pmon: tail ready (fifo live2.fifo) +[host]$ cat live2.fifo & echo '[out] one' >> live2.log +[out] one +[host]$ kill %2 # kill the cat +pmon: tail reader detached +[host]$ echo '[out] appended during gap' >> live2.log +[host]$ cat live2.fifo # reattach: nothing was lost +[out] appended during gap +[host]$ kill -TERM %1 +pmon: exiting (signal) +``` + +Every line above is from the captured run: the first reader got `[out] +one`, the line appended while no reader existed arrived intact at the +second reader, and SIGTERM ended the relay with exit 0. The runner +(`python3 scripts/test-all-examples.py --only 18-pipes-fifos-splice`) +asserts this whole choreography — plus the v2 engine contract unchanged +under capture — and printed `PASS PASS PASS` for cpp, go, and rust on this +host. + +## Cross-check, four ways + +**The fd table of the running trio.** With `supervise` driving a ticking +child, `tail` relaying, and a `cat` attached, `/proc//fd` maps every +descriptor in Figure 18.2 (paths shortened): + +```bash +[host]$ ls -l /proc/2633754/fd # supervisor +0 -> /dev/null 1 -> sup.out 2 -> sup.err 3 -> live.log +4 -> anon_inode:[signalfd] 5 -> pipe:[16462630] +6 -> anon_inode:[pidfd] 7 -> pipe:[16462631] +[host]$ ls -l /proc/2633756/fd # child sh +0 -> /dev/null 1 -> pipe:[16462630] 2 -> pipe:[16462631] +[host]$ ls -l /proc/2634759/fd # tail relay +3 -> live2.log 4 -> live2.fifo +[host]$ ls -l /proc/2634762/fd # cat reader +1 -> c1.out 3 -> live2.fifo +``` + +The child's fd 1 and the supervisor's fd 5 name the *same* pipe inode +16462630 — one ring, two processes — and fd 2/fd 7 pair up on 16462631; +the relay and the reader meet at the FIFO. `fdinfo` reads out the flags: +the supervisor's pipe read end shows `flags: 02000000` (`O_CLOEXEC`, from +`pipe2`), and the relay's FIFO writer shows `02100001` with `pos: 10` on +its log fd — ten bytes, exactly `[out] one\n`, delivered. + +**The capacity, from the fd itself.** The `F_GETPIPE_SZ` probe above +printed `capacity: 65536` and `PIPE_BUF: 4096` from a live pipe, agreeing +with `getconf PIPE_BUF /` (4096) and with sixteen pages of ring. + +**The zero-copy path, under strace.** Filtering the C++ `tail` to +`splice` while a reader attaches, receives one tick line, and is killed: + +```bash +[host]$ strace -f -e trace=splice $APP tail --log live.log --fifo live.fifo +splice(3, NULL, 4, NULL, 65536, 0) = 22 +splice(3, NULL, 4, NULL, 65536, 0) = 0 +splice(3, NULL, 4, NULL, 65536, 0) = -1 EPIPE (Broken pipe) +``` + +fd 3 (the log) to fd 4 (the FIFO), 65536-byte requests: 22 bytes moved — +one `[out] tick 1784434137\n` record — then 0 for "caught up", then +`EPIPE` at the instant the `cat` died, answered on stdout by `pmon: tail +reader detached`. The Rust binary's trace is the same shape: +`splice(3, NULL, 4, NULL, 65536, 0) = 16` for its 16-byte record, then +`= 0`, then `= -1 EPIPE (Broken pipe)` — 22 splice calls total in that +session and not one `read` of log data in between. Go, by design, shows +plain `read`/`write` instead. + +**Throughput, order-of-magnitude.** A FIFO is a kernel ring, so moving +bytes through one should run at memory speed, not disk speed: + +```bash +[host]$ mkfifo dd.fifo && cat dd.fifo > /dev/null & dd if=/dev/zero of=dd.fifo bs=64k count=16384 +1073741824 bytes (1.1 GB, 1.0 GiB) copied, 0.202097 s, 5.3 GB/s +``` + +One GiB through the FIFO in a fifth of a second on this host — 5.3 GB/s +through two processes and one ring, with `bs=64k` matching both the ring +capacity and pmon's `kChunk`. + +> **On the lab VM** unverified — +> the fleet-wide view of this chapter is tracing pipe traffic and splice +> calls system-wide with `bpftrace` kprobes and bcc-tools instead of +> per-process `strace`; that tooling is not runnable on this host without +> privileges, and Part 8 (Debugging) exercises exactly that on the +> `systems-target` VM. + +## What you learned + +- A pipe is a bounded ring of sixteen kernel pages — capacity readable and + resizable through `fcntl(F_GETPIPE_SZ/F_SETPIPE_SZ)` in whole-page units + — and its blocking behavior in both directions is back-pressure, not a + defect. +- `PIPE_BUF` (4096) makes small single writes atomic but promises nothing + about framing, large writes, or reads — which is why framed `[out]`/`[err]` + capture needs two pipes and a userspace line relay, and why the parent + must close its write-end copies or EOF never comes. +- A FIFO is a pipe with an inode: `open` blocks until both ends exist, + `O_WRONLY|O_NONBLOCK` turns "no reader" into a pollable `ENXIO`, and + `SIGPIPE`-ignored plus `EPIPE`-handled is the contract every long-lived + writer must sign. +- `splice(2)` moves file bytes into a pipe kernel-side — `strace` showed + the relay running on nothing but `splice` — and advancing the file + offset only for delivered bytes is what makes reader churn lossless in + all three languages. + +Next, **UNIX sockets and fd passing**: pmon grows a real control plane — a +socket a stranger can find, credentials the kernel vouches for, and open +file descriptors sent across process boundaries. + +--- + +

verified — every number and +output excerpt above was reproduced on the Fedora 44 reference host (kernel +7.1.3-200.fc44) this session: the runner printed +18-pipes-fifos-splice  PASS  PASS  PASS +(3 passed, 0 failed, 0 skipped); the F_GETPIPE_SZ probe printed capacity +65536, PIPE_BUF 4096, and the 1→4096 rounding live; the supervise run +produced the exact restart transcript and six-record log shown; the trio's +/proc/<pid>/fd listings (pipe inodes 16462630/16462631 +shared child↔supervisor, fdinfo flags 02000000 and 02100001, log pos 10), +the detach/reattach transcript ([out] one, gap append delivered +to the second reader, tail reader detached, +exiting (signal), exit 0), the C++ and Rust +strace splice excerpts (22- and 16-byte moves, 0 when idle, +-1 EPIPE on reader death), the +pipe2([5, 6], O_CLOEXEC) trace, and the dd-through-FIFO figure +(1073741824 bytes, 0.202097 s, 5.3 GB/s) are all real captured output. The +bcc-tools/bpftrace callout is unverified as marked and deferred to Part 8; +pipe capacity and throughput figures are host-specific.

diff --git a/_docs/19-unix-sockets-fd-passing.md b/_docs/19-unix-sockets-fd-passing.md new file mode 100644 index 0000000..7f7f58b --- /dev/null +++ b/_docs/19-unix-sockets-fd-passing.md @@ -0,0 +1,497 @@ +--- +title: "UNIX sockets and fd passing" +order: 19 +part: "IPC" +description: "pmon grows a control plane on a UNIX stream socket: filesystem vs abstract addresses under ss -x, SO_PEERCRED as kernel-vouched local auth, and SCM_RIGHTS sending an open log fd across process boundaries — proven shared at the open-file-description layer with /proc fdinfo on both sides." +duration: "50 minutes" +--- + +Chapter 18 gave `pmon` a log pipeline built from pipes and FIFOs — fast, +kernel-buffered, and strictly byte-shaped: a pipe moves data, and only +between processes that inherited its ends. Version 5 needs something pipes +cannot do: a stranger — a `pmctl` process started minutes later, from +another terminal — must find the running supervisor, prove who it is, ask +questions, and get answers. That is a *control plane*, and the Linux +primitive for it is the UNIX domain socket: a socket with the full +`connect`/`accept` shape of TCP but living entirely inside the kernel, with +two abilities no network socket has. It can tell the server exactly which +uid and pid is calling (`SO_PEERCRED`), and it can carry *file descriptors* +in its messages (`SCM_RIGHTS`). The second one is the headline: `pmctl +logfd` never opens the log file — the supervisor hands it an already-open +descriptor, and the chapter closes the loop on chapter 7's three-layer model +by proving, with `/proc` on both processes at once, exactly which layer +crossed the socket. + +The code is in `examples/19-unix-sockets-fd-passing/`. `./demo.sh` there +builds all three implementations; its `README.md` specifies the CLI, the +one-line wire protocol, and the exit codes all three languages share. + +{% include excalidraw.html + file="19-uds-control-plane" + alt="Two process bands over a kernel band: the pmon supervise process holds listener fd 4 bound to /tmp/…/ctl.sock, an accepted fd 5 annotated with the SO_PEERCRED result uid=1000 pid=2624432, and log fd 3 opened O_RDWR|O_APPEND feeding a dashed child box for sleep 300 pid 2622905; the pmctl process holds client fd 3 which connects down to socket inode 16422777 in the kernel band, paired by a bidirectional amber arrow with socket inode 16411996 owned by the supervisor, exactly as ss -xp reported." + caption="Figure 19.1 — pmon v5's control plane during one status call: every pid, fd number, and socket inode in this figure comes from the live run traced later in the chapter" %} + +> **Tools used** — `ss`, `strace`, `stat`, `tail`, `ls`, `python3` (host); +> `bpftrace`/bcc-tools (lab VM, exercised in Part 8). Everything host-side is +> checked by `scripts/check-host.sh` or ships with Fedora. + +## One family, three conversations + +`AF_UNIX` sockets come in the same three types as `AF_INET`, and the choice +is about message shape, not speed. `SOCK_STREAM` is a connected, reliable +byte stream — no boundaries, which is why pmon's protocol is line-framed +("one `\n`-terminated command in, one reply out"). `SOCK_DGRAM` is +connectionless datagrams — unlike UDP they are reliable and ordered +kernel-local, but there is no connection, and without a connection there is +no accepted socket to ask `SO_PEERCRED` about, which rules it out here. +`SOCK_SEQPACKET` is the hybrid TCP never had: connection-oriented *and* +boundary-preserving, a good fit for record protocols. pmon uses a stream +because the framing is one trivial line and because the connection itself is +load-bearing: it scopes authentication (credentials are checked per accepted +connection) and delivery (the reply belongs to whoever wrote the command). + +`socketpair(2)` is the degenerate case worth knowing: it returns two +already-connected `AF_UNIX` ends with no address at all, the right tool when +the two parties are parent and child and nobody needs to *find* anybody — +it is how container runtimes hand fds to the processes they spawn. pmon +needs the opposite: a rendezvous point a stranger can name. That means an +address, and UNIX sockets have two address namespaces. + +## Two namespaces: a path, or nothing but a name + +A **filesystem** socket is what `bind` creates when `sun_path` starts with a +normal character: a socket-type inode at that path. It obeys directory +permissions — parking `ctl.sock` in a `0700` directory *is* the access +policy — and it outlives its creator, which is why `listen_ctl` starts with +`unlink()`: a crashed supervisor leaves a stale inode that would make the +next `bind` fail with `EADDRINUSE`. An **abstract** socket, a Linux +extension, is what you get when `sun_path[0]` is a NUL byte: no inode, no +permissions, automatically gone when the last fd closes, and scoped to the +network namespace. Both kinds show up in `ss -x`, and the rendering +distinguishes them — on this host, with pmon's socket listening and a +one-line `python3` peer bound to the abstract name `\0pmon-demo`: + +```console +[host]$ ss -xl | grep -E 'pmon|ctl.sock' +u_str LISTEN 0 8 /tmp/p19.t9j0/ctl.sock 16411867 * 0 +u_str LISTEN 0 1 @pmon-demo 16406037 * 0 +``` + +The `@` prefix is `ss`'s spelling of that leading NUL. pmon deliberately +uses the filesystem kind: the path *is* the discovery mechanism and the +permission check, and `stop` unlinking it is a clean "no longer accepting" +signal. The classic trap is capacity, not permissions: `sun_path` is 108 +bytes, so a control socket under a deep CI workspace path fails to bind at +all — every implementation checks this (the C++ one returns +`socket path too long`), and `verify.lua` runs from `mktemp -d` for exactly +this reason. + +## Who is calling? Ask the kernel, not the packet + +The first thing the supervisor does with an accepted connection — before +reading a single command byte — is: + +```cpp +ucred cred{}; +socklen_t cred_len = sizeof(cred); +if (::getsockopt(conn.get(), SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) == 0) { + std::println(stderr, "pmon: ctl connect uid={} pid={}", cred.uid, cred.pid); +} +``` + +`SO_PEERCRED` returns the pid, uid, and gid of the peer *as recorded by the +kernel at `connect` time*. The client cannot forge it, because the client +never supplies it — this is the difference between authentication and a +claim. On the live run every control connection produced a line like: + +```console +pmon: ctl connect uid=1000 pid=2623337 +``` + +and `verify.lua` asserts that the uid printed equals `id -u` in all three +languages. pmon only logs the identity; the one-line change to *enforce* +(compare `cred.uid` against `getuid()` and hang up) is the pattern every +socket-activated daemon on your machine uses. Two sharp edges: the pid is +the connector's pid at connect time — the peer may have exited or the pid +been reused by the time you act on it (chapter 13's pidfd argument, again), +and stronger per-message guarantees exist via `SCM_CREDENTIALS` if you need +them. + +## What actually crosses the socket + +`SCM_RIGHTS` is routinely described as "sending a file descriptor", and that +phrase hides the mechanism. The number does not travel — fd 3 in the sender +arrived as fd 4 in the receiver on the traced run below. What the kernel +does is precise in chapter 7's terms: it takes the sender's fd-table slot, +follows it to the **open file description**, allocates a *new slot in the +receiver's fd table* (lowest free, as always), and points it at that same +description. The result is exactly what `dup(2)` produces — shared offset, +shared status flags, independent close — except the two references live in +different processes. The inode layer never moves either; it was always +system-wide. + +{% include excalidraw.html + file="19-scm-rights-transfer" + alt="Two fd-table bands over a shared kernel band: sender slot 3 with close-on-exec set at open, and receiver slot 4 labeled new lowest free with close-on-exec set by MSG_CMSG_CLOEXEC, both pointing at one dark open-file-description box holding pos 47 and flags 02102002, which points down to inode 95934 for pmon.log; sendmsg with cmsg_data 3 names the sender slot, an amber dashed arrow shows the kernel installing the new receiver slot, and recvmsg reports cmsg_data 4." + caption="Figure 19.2 — SCM_RIGHTS in chapter 7's three layers: two per-process slots, one open file description, one inode; pos, flags, inode, and both fd numbers are from the held-open run below" %} + +That layer choice has practical consequences pmon is built around. The log +is opened `O_RDWR | O_APPEND`, not `O_WRONLY` — flags live on the +description, so a write-only description would arrive write-only and +`pmctl`'s reads would fail `EBADF`. And `pmctl` reads with `pread` at +explicit offsets instead of `read`, because the *offset* is shared too: a +`lseek`+`read` would move the supervisor's append position out from under +it. (`O_APPEND` writes ignore the offset, so the writer is safe regardless — +but the discipline costs nothing and the point generalizes.) + +## How the code works + +The supervision half is chapter 11–13 machinery matured: `spawn_child` +forks/execs the command with `setpgid(0,0)` so `stop` can `SIGTERM` the +whole group, `dup2`s the log fd over stdout and stderr, and a reaper — +`std::jthread`, goroutine, or `std::thread` — `wait`s, backs off 300 ms, and +respawns, guarding a small `State{child, started, restarts, stopping}` with +a mutex (C++/Rust condvar, Go channels). What is new is the socket plumbing. +`listen_ctl` is the canonical four-step: `unlink` the stale path, +`socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)`, `bind` a `sockaddr_un` +(after the 108-byte check), `listen`. The accept loop takes each connection +with `accept4(…, SOCK_CLOEXEC)` — chapter 7's rule that the cloexec bit must +be set atomically applies to *every* fd source, and `accept` is one — +logs `SO_PEERCRED`, reads one line, and dispatches: `status` formats the +shared state, `stop` tears down, `logfd` calls the interesting function. +Sending, in the C++ spelling, verbatim from +`examples/19-unix-sockets-fd-passing/cpp/src/main.cpp`: + +```cpp +void send_fd(int sock, int fd) { + char payload[] = "ok\n"; + iovec iov{payload, 3}; + alignas(cmsghdr) char ctrl[CMSG_SPACE(sizeof(int))]{}; + msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + cmsghdr* cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SCM_RIGHTS; + cm->cmsg_len = CMSG_LEN(sizeof(int)); + std::memcpy(CMSG_DATA(cm), &fd, sizeof(int)); + if (::sendmsg(sock, &msg, 0) < 0) { + std::println(stderr, "pmon: sendmsg: {}", errno_text()); + } +} +``` + +Every line is load-bearing. Control messages ride in an *ancillary buffer* +alongside the normal payload, and the `CMSG_*` macros exist because that +buffer has alignment rules you should never hand-compute: `CMSG_SPACE(n)` +sizes the buffer (header + padded data — 24 bytes here), `CMSG_LEN(n)` is +the length recorded *inside* the header (20: header + 4 unpadded), and +`CMSG_DATA` locates the aligned payload slot where the fd is `memcpy`ed. +Getting `msg_controllen` and `cmsg_len` confused is the classic way to send +a message the receiver silently drops. The 3-byte `"ok\n"` payload is not +decoration either: ancillary data is delivered *with* a byte of real data, +and sending at least one byte is the portable way to guarantee the cmsg is +picked up by the receiver's next `recvmsg`. + +The receive side is where the three languages diverge most, so it gets the +tabs — each function verbatim: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +Result recv_fd(int sock) { + char payload[16]; + iovec iov{payload, sizeof(payload)}; + alignas(cmsghdr) char ctrl[CMSG_SPACE(sizeof(int))]{}; + msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + ssize_t n = ::recvmsg(sock, &msg, MSG_CMSG_CLOEXEC); + if (n < 0) { + return std::unexpected(std::format("recvmsg: {}", errno_text())); + } + for (cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; cm = CMSG_NXTHDR(&msg, cm)) { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { + int fd = -1; + std::memcpy(&fd, CMSG_DATA(cm), sizeof(int)); + return Fd{fd}; + } + } + return std::unexpected("no SCM_RIGHTS control message in reply"); +} +``` + +```go +func recvLogFd(conn *net.UnixConn) (int, error) { + buf := make([]byte, 16) + oob := make([]byte, unix.CmsgSpace(4)) + _, oobn, _, _, err := conn.ReadMsgUnix(buf, oob) + if err != nil { + return -1, fmt.Errorf("recvmsg: %w", err) + } + msgs, err := unix.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return -1, fmt.Errorf("parse cmsg: %w", err) + } + for _, m := range msgs { + fds, ferr := unix.ParseUnixRights(&m) + if ferr == nil && len(fds) > 0 { + return fds[0], nil + } + } + return -1, errors.New("no SCM_RIGHTS control message in reply") +} +``` + +```rust +fn recv_log_fd(conn: &UnixStream) -> Result { + let mut payload = [0u8; 16]; + let mut iov = [IoSliceMut::new(&mut payload)]; + let mut cmsg_buf = nix::cmsg_space!(RawFd); + let msg = recvmsg::<()>( + conn.as_raw_fd(), + &mut iov, + Some(&mut cmsg_buf), + MsgFlags::MSG_CMSG_CLOEXEC, + ) + .context("recvmsg")?; + for cmsg in msg.cmsgs().context("parse cmsg")? { + if let ControlMessageOwned::ScmRights(fds) = cmsg + && let Some(&fd) = fds.first() + { + // SAFETY: the kernel installed this fd for us via SCM_RIGHTS; + // we are its only owner. + return Ok(unsafe { OwnedFd::from_raw_fd(fd) }); + } + } + bail!("no SCM_RIGHTS control message in reply") +} +``` + +Three details deserve emphasis. First, `MSG_CMSG_CLOEXEC`: the received fd +is installed by the kernel, not by any `open` you control, so this flag is +the only atomic way to give it the cloexec bit — the recvmsg-then-`fcntl` +window is chapter 7's fork/exec race all over again. Note the layering in +Figure 19.2: both slots end up cloexec, but each got the bit *its own way*, +because close-on-exec is per-slot, never shared. Second, ownership: the +C++ code moves the raw int into the RAII `Fd` immediately, and Rust wraps +it in `OwnedFd::from_raw_fd` behind the crate's only `unsafe` block — the +comment is the ownership proof that makes it sound; Go documents intent +with `defer unix.Close(fd)` at the call site. Third, all three iterate the +control messages rather than assuming the first one is the rights message — +and all three return the byte-identical diagnostic +`no SCM_RIGHTS control message in reply` when a plain reply arrives, which +is what a `status` response would look like to `recvLogFd`. From there +`log_tail_via_fd` is deliberately mundane: `pread` from offset 0 in 4 KiB +chunks (1 MiB cap — a fragile bound, stated in the source), split lines, +print the last three prefixed `via-fd: `. Mundane is the point: the fd +works like any other, because it *is* any other fd. + +## Errors, three ways + +The contract is the same as every pmon version: exit 0, runtime failures +exit 1 with one diagnostic line, usage errors exit 2 — and `verify.lua` +asserts all three per language, 27 behavioral checks each. The +interesting split is *which* errors each side owns. `pmctl` treats a failed +`connect` as fatal (`pmctl: error: connect /tmp/…/absent.sock: No such file +or directory` — `std::error_code` text in C++, wrapped `%w` chain in Go, +`anyhow` context in Rust; same errno underneath). The supervisor, by +contrast, must survive *its* clients: an unknown command gets an +`err unknown command` reply line, `pmctl` unwraps the `err ` prefix and +exits 1, and the supervisor keeps serving. A peer that hangs up mid-reply +just ends that connection — `write_all` swallows the error deliberately, +because there is nobody left to report it to, and the loop `accept`s the +next caller. `EINTR` is retried at every blocking call in the C++ +(`read_command`, `write_all`, `waitpid`), which the runtimes of Go and Rust +handle beneath their standard libraries. And the address-length failure is +caught before the syscall in all three, because a 108-byte truncation by +`strncpy` — the traditional C bug — would *bind to the wrong path* rather +than fail. + +## Concurrency lens + +Two threads of control share `State` in every implementation: the accept +loop and the reaper. The subtle race is `stop` versus respawn: `stop` sets +`stopping` and SIGTERMs the current group, but the reaper may already be +past its backoff and inside `spawn_child`. All three implementations +re-check `stopping` *after* the spawn returns and, if the flag flipped, +undo — SIGTERM and reap the child they just created — before signaling +`child_done`. Without that re-check, `stop` leaks exactly one freshly +respawned process, a bug that only bites when the child dies within 300 ms +of a stop. The languages phrase the wait differently — C++ and Rust block +on a condvar predicate, Go closes a `stopCh` the reaper `select`s against +the backoff timer — but the state machine is identical, which is what makes +the single `verify.lua` possible. Two quieter points: connections are +handled serially by design (one accepted fd at a time, so state locking +stays trivial and `SO_PEERCRED` lines interleave with nothing), and the log +file is shared by three writers — supervisor and each child generation via +`dup2` — safely, because every line is a single `write` on an `O_APPEND` +description, which the kernel makes an atomic append. One Go footnote: +`listener.SetUnlinkOnClose(false)` keeps Go's helpfulness from unlinking +the socket before the stop path does it explicitly, and +`int(logFile.Fd())` in the `logfd` case is again a loan against the +`*os.File` staying live, exactly as dissected in chapter 7. + +## Build, run, observe + +```bash +[host]$ cd examples/19-unix-sockets-fd-passing && ./demo.sh build +``` + +The runner (`python3 scripts/test-all-examples.py --only +19-unix-sockets-fd-passing`) reports `PASS PASS PASS` for cpp, go, rust — +27 behavioral checks per language. Driving the C++ build by hand the way +this chapter's evidence was produced (`$WD` is a short `mktemp -d` +directory — remember `sun_path`): + +```bash +[host]$ ./cpp/build/release/app supervise --ctl $WD/ctl.sock --log $WD/pmon.log -- \ + /bin/sh -c 'echo alpha; echo beta; echo gamma; exec sleep 300' & +pmon: listening on /tmp/p19.t9j0/ctl.sock +pmon: started child pid=2622905 +[host]$ ./cpp/build/release/app pmctl --ctl $WD/ctl.sock status +child=2622905 uptime=20s restarts=0 +[host]$ ./cpp/build/release/app pmctl --ctl $WD/ctl.sock logfd +via-fd: alpha +via-fd: beta +via-fd: gamma +[host]$ kill 2622905 && sleep 1 && ./cpp/build/release/app pmctl --ctl $WD/ctl.sock status +child=2624872 uptime=0s restarts=1 +[host]$ ./cpp/build/release/app pmctl --ctl $WD/ctl.sock stop +stopping +``` + +Each `pmctl` call also produced a `pmon: ctl connect uid=1000 pid=…` line on +the supervisor's stderr; the kill produced `pmon: child pid=2622905 exited +status=143` and `pmon: restart 1 child pid=2624872`; and after `stop` the +socket file was gone (`test -S` fails) and the child group with it. The +`via-fd:` lines matched `tail -3 $WD/pmon.log` byte for byte — but output +equality could still be a program quietly opening the path, so the +cross-checks watch the mechanism itself. + +## Cross-check, three ways + +**The socket pair and its owners, with `ss -xp`.** During a control call +held open (below), both endpoints of the stream show up, each naming its +owning process and cross-referencing the other's inode: + +```console +[host]$ ss -xp | grep -E 'ctl.sock|"app"' +u_str ESTAB 0 0 /tmp/p19.t9j0/ctl.sock 16411996 * 16422777 users:(("app",pid=2622904,fd=5)) +u_str ESTAB 0 0 * 16422777 * 16411996 users:(("app",pid=2624432,fd=3)) +``` + +Supervisor pid 2622904 holds fd 5 (the accepted socket — Figure 19.1), +client pid 2624432 holds fd 3, and the peer columns pair the two inodes. +This is the same accounting the supervisor's `SO_PEERCRED` line reported +from the other direction: `pmon: ctl connect uid=1000 pid=2624432`. + +**The cmsg itself, under `strace`.** Tracing `sendmsg` on the supervisor +and `recvmsg` on `pmctl` during a `logfd` call, `strace` decodes the +control message on both sides: + +```console +2622904 sendmsg(5, {…, msg_iov=[{iov_base="ok\n", iov_len=3}], + msg_control=[{cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, + cmsg_data=[3]}], msg_controllen=24, …}, 0) = 3 +recvmsg(3, {…, msg_control=[{cmsg_len=20, cmsg_level=SOL_SOCKET, + cmsg_type=SCM_RIGHTS, cmsg_data=[4]}], msg_controllen=24, + msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 3 +pread64(4, "pmon: start child pid=2622905\nal"..., 4096, 0) = 47 +pread64(4, "", 4096, 47) = 0 +``` + +(Fields trimmed; the full lines are in the trace files.) The sender named +fd **3**; the receiver got fd **4** — the number is per-process, so it did +not travel. `cmsg_len=20` and `msg_controllen=24` are `CMSG_LEN(4)` and +`CMSG_SPACE(4)` from `send_fd`, live. And the very next syscalls show +`pmctl` reading all 47 bytes of log *through fd 4* at explicit offsets — +no `openat` anywhere in the trace. + +**The shared description, in `/proc` — both processes at once.** `pmctl` +normally exits in milliseconds, so to catch both sides holding the fd, the +run above injected a 15-second delay after `recvmsg` +(`strace -e inject=recvmsg:delay_exit=15000000`). During the window, with +supervisor pid 2622904 and `pmctl` pid 2623753: + +```console +[host]$ ls -l /proc/2622904/fd | grep pmon.log; ls -l /proc/2623753/fd | grep pmon.log +lrwx------ 1 rsedor rsedor 64 Jul 18 23:57 3 -> /tmp/p19.t9j0/pmon.log +lrwx------ 1 rsedor rsedor 64 Jul 18 23:58 4 -> /tmp/p19.t9j0/pmon.log +[host]$ cat /proc/2622904/fdinfo/3 /proc/2623753/fdinfo/4 +pos: 47 +flags: 02102002 +mnt_id: 59 +ino: 95934 +pos: 47 +flags: 02102002 +mnt_id: 59 +ino: 95934 +[host]$ stat --format='%i %s %n' /tmp/p19.t9j0/pmon.log +95934 47 /tmp/p19.t9j0/pmon.log +``` + +Field for field equal, and each field pins a layer of the chapter 7 model. +`ino: 95934` on both fds matches `stat` on the path — one inode. `pos: 47` +on both — one *offset*, which only makes sense if there is one open file +description; 47 is the file's size because every `O_APPEND` write advanced +the shared position, and `pread` never moved it. `flags: 02102002` on both — +one set of status flags, decoding as `O_RDWR` (02) + `O_APPEND` (02000) + +`O_LARGEFILE` (0100000) + `O_CLOEXEC` (02000000): the description's flags +crossed intact, while the cloexec bit — per-slot, remember — reads set on +both sides only because each side set its own, one at `open` time, one via +`MSG_CMSG_CLOEXEC`. Two fd tables, one description, one inode: each claim +in Figure 19.2 observed on a live pair of processes. + +> **On the lab VM** unverified — +> the fleet-wide view of this chapter is tracing fd passing across *all* +> processes with bcc-tools (`sofdsnoop`) and bpftrace probes on +> `unix_stream_sendmsg`, instead of per-process `strace`; bcc-tools are not +> runnable on this host without privileges, and the Debugging part (Part 8) +> exercises exactly that on the `systems-target` VM. + +## What you learned + +- `AF_UNIX` gives you three conversation shapes (stream, datagram, + seqpacket) and two address namespaces — a filesystem inode governed by + directory permissions and unlink, or an abstract NUL-prefixed name that + `ss -x` renders with `@` and the kernel garbage-collects; `socketpair` + skips addressing entirely for related processes. +- `SO_PEERCRED` is local auth with the kernel as witness: the supervisor + logged `pmon: ctl connect uid=1000 pid=…` for every connection without + the client claiming anything. +- `SCM_RIGHTS` transfers a *reference to the open file description*, not a + number: sender fd 3 arrived as receiver fd 4, and `/proc` fdinfo on both + processes simultaneously showed one `pos`, one `flags`, one `ino` — + which is why the log must be `O_RDWR`, why `pmctl` uses `pread`, and why + `MSG_CMSG_CLOEXEC` exists. +- The cmsg API is exact bookkeeping: `CMSG_SPACE` sizes the buffer, + `CMSG_LEN` fills the header, at least one real payload byte rides along — + and `strace` will show you all of it, decoded, when you get it wrong. + +Next, **shared memory coordination**: `shmkv` stops copying bytes through +the kernel entirely — one mapped file, a seqlock for consistent reads, and +a futex so readers learn about writes without polling. + +--- + +

verified — every number and +output excerpt above was produced on the Fedora 44 reference host this +session: the runner printed 19-unix-sockets-fd-passing PASS PASS +PASS (3 passed, 0 failed, 0 skipped; verify.lua 27/27 per language); +the hand-driven session produced the exact status, +via-fd:, restart, and stopping lines shown, with +the socket file confirmed absent after stop; ss -xl showed the +filesystem and abstract (@pmon-demo) sockets as printed; +ss -xp showed the established pair with owning pids +2622904/2624432 and cross-referenced inodes; the strace excerpts are real +trimmed output with cmsg_data=[3] sent and +cmsg_data=[4] received; and the delay-injection window yielded +the simultaneous /proc/<pid>/fd and fdinfo +captures (pos 47, flags 02102002, ino 95934 on both sides, matching +stat). The "On the lab VM" bcc-tools callout is unverified as +marked and is exercised in Part 8.

diff --git a/_docs/20-shared-memory-coordination.md b/_docs/20-shared-memory-coordination.md new file mode 100644 index 0000000..f32435d --- /dev/null +++ b/_docs/20-shared-memory-coordination.md @@ -0,0 +1,532 @@ +--- +title: "Shared-memory coordination: futexes and seqlocks" +order: 20 +part: "IPC" +description: "The coordination ladder from a 1 ms poll loop through POSIX message queues to the futex — the kernel's one real primitive, sleep/wake on a shared word — plus a seqlock for read-mostly snapshots, process-shared atomics in all three languages, and a measured IPC latency matrix, as shmkv v1 gains watchers that never poll." +duration: 60 minutes +--- + +Chapter 16 (mmap and shared mappings) left `shmkv` in an awkward state: two +processes share a page of key-value slots, but a reader who wants to *notice* +a change has no option except reading the page again, and again, and again. +This chapter fixes that, and the one new idea is smaller than you expect: for +coordination between processes, the kernel provides essentially **one +primitive** — `futex(2)`, "sleep until this 32-bit word changes; wake the +sleepers queued on it." Everything you have ever called a mutex, condition +variable, semaphore, or `WaitGroup` is userspace protocol wrapped around that +word. `shmkv` v1 builds the protocol in the open: a **seqlock** makes reads of +the shared page consistent without ever blocking the writer, and a shared +futex turns "something changed" from a question you keep asking into an event +you sleep through. Along the way we climb the whole coordination ladder — +polling, POSIX message queues, futex — and measure every rung. + +The code is in `examples/20-shared-memory-coordination/`. `./demo.sh` builds +all three implementations and runs the serve/watch/bench demo; the `README.md` +there pins the SHKV2 page layout, the CLI, and the exit codes all three +languages share. + +{% include excalidraw.html + file="20-futex-wait-wake" + alt="Three lanes: watcher process, kernel, writer process. The watcher's seqlock read finds nothing new and calls futex FUTEX_WAIT on word value 3; the kernel re-checks the word equals 3 and sleeps the task on a wait queue keyed by inode and offset 12. The writer publishes k4 under the seqlock bumping seq 6 to 7 to 8, stores futex word 4 with release ordering, and calls FUTEX_WAKE INT_MAX which returns 1; an amber arrow marks the watcher runnable again, and it back-fills ids from the 8-slot ring. A dashed note explains the EAGAIN lost-wakeup guard." + caption="Figure 20.1 — one shmkv wake end to end: the watcher parks on the mapped word, the writer publishes and wakes, the kernel's wait queue keyed by (inode, offset) connects them; every syscall in this figure appears verbatim in the strace cross-check below" %} + +> **Tools used** — `strace`, `xxd`, `perf`, `/usr/bin/time`, `awk` (over +> `/proc//status`), `gcc` (for the comparison harness), `python3` (the +> example runner) — all host; `offcputime`/`futexctn` (bcc-tools, lab VM, +> exercised in Part 8). Everything host-side is checked by +> `scripts/check-host.sh` or ships with Fedora. + +## The coordination ladder + +Suppose process A has changed shared memory and process B needs to know. The +options form a ladder, and this chapter's `bench` subcommand times every rung. + +**Rung 0: poll.** B re-reads the page in a loop, sleeping between looks so it +does not burn a CPU. Simple, portable — and the sleep *is* the latency. Our +poll channel deliberately sleeps 1 ms *before* checking, so by construction +every observation costs at least one full nap; the measured p50 of 1,056 µs is +the sleep plus scheduler overhead, and no tuning removes the trade-off: a +shorter nap trades latency for wasted wakeups, and every nap is a voluntary +context switch whether or not anything happened. + +**Rung 1: a kernel object with a queue.** POSIX message queues +(`mq_open(2)`, `mq_timedsend(2)`, `mq_timedreceive(2)`) give you a named +kernel channel with priorities and blocking receive. They carry *data*, not +just "something happened" — our bench sends the 8-byte update id through the +queue. The cost is a syscall on every send and every receive, and a second +copy of information the shared page already holds. + +**Rung 2: the futex.** `futex(addr, FUTEX_WAIT, expected, timeout)` says: +*if* the 32-bit word at `addr` still holds `expected`, put me to sleep on a +kernel wait queue attached to that address; `futex(addr, FUTEX_WAKE, n)` +wakes up to `n` sleepers. The check-then-sleep is atomic against wakers — +that is the entire reason the primitive exists. If the word already moved, +`FUTEX_WAIT` returns `EAGAIN` immediately and the caller re-reads shared +state: the lost-wakeup race that plagues homemade "check a flag, then sleep" +schemes is closed *inside* the kernel. This is the C++ spelling, from +`examples/20-shared-memory-coordination/cpp/src/main.cpp`: + +```cpp +void futex_wait(std::uint32_t* addr, std::uint32_t expected, + std::chrono::milliseconds timeout) { + timespec ts{.tv_sec = timeout.count() / 1000, + .tv_nsec = (timeout.count() % 1000) * 1'000'000}; + // EAGAIN (word already moved on), ETIMEDOUT and EINTR are all normal here: + // the caller re-checks shared state and decides whether to wait again. + ::syscall(SYS_futex, addr, FUTEX_WAIT, expected, &ts, nullptr, 0); +} +``` + +Two details are load-bearing. There is no glibc wrapper — the raw +`syscall(SYS_futex, …)` *is* the interface, in all three languages. And the +call deliberately omits `FUTEX_PRIVATE_FLAG`: a private futex is keyed by +process-local virtual address, which is faster but invisible to other +processes; a **shared** futex is keyed by `(inode, offset)` of the mapped +file, so two unrelated processes that `mmap` the same `/tmp/kv.shm` sleep and +wake on the same kernel queue even though the page sits at different virtual +addresses in each. The fast path costs *zero* syscalls: an uncontended check +is just an atomic load, and only actual sleeping or waking enters the kernel. +That is why glibc builds every pthread mutex and condvar on this exact call. + +## A seqlock for read-mostly data + +The futex says *when* to look; the seqlock makes what you see *consistent*. +`shmkv`'s page has one writer and many readers, and the readers vastly +outnumber the writes — the classic seqlock shape (the kernel uses one for +`gettimeofday`'s clock data). The writer's side is four lines: + +```cpp +template +void seqlock_write(const Shm& shm, Mutate&& mutate) { + auto seq = shm.seq(); + seq.fetch_add(1, std::memory_order_relaxed); // odd: writer active + std::atomic_thread_fence(std::memory_order_release); + mutate(); + seq.fetch_add(1, std::memory_order_release); // even: consistent again +} +``` + +The writer bumps the sequence word **twice** per update, and both bumps are +essential. The first makes the word *odd* — a flag meaning "mutation in +progress; anything you copy now may be torn," so readers that see an odd +value do not even bother copying. The second makes it even again — but at a +*different* even value than before. That difference is the whole detection +scheme: a reader records the word before copying and compares it after; if a +writer ran at any point in between, the word has moved (odd mid-flight, or a +larger even value), the copies cannot be trusted, and the reader throws them +away and retries. Bump once instead of twice and a reader that started just +before the writer could see the same value on both sides of a torn copy. The +fences pair up across processes: the writer's release fence ensures the slot +bytes are visible before the closing bump, and the reader's acquire fence +ensures its copies happened before it re-reads the word. Writers never wait, +never take a lock, never care how many readers exist — readers pay with +occasional retries, which is exactly the right trade for read-mostly data. +The retry loop is bounded at 10⁶ attempts so a corrupt file (a stuck-odd word +from a crashed writer) produces `seqlock read livelocked` instead of a hang. + +## Process-shared atomics, three ways + +All of this only works if atomic operations on *mapped* memory are really +atomic, and each language has its own rules for saying so. In C++, a +`std::atomic` must be constructed, but our words live in a file another +process formatted — so `Shm` binds `std::atomic_ref` onto the raw bytes: + +```cpp + std::atomic_ref seq() const { + return std::atomic_ref{*reinterpret_cast(base() + kOffSeq)}; + } +``` + +`atomic_ref` (C++20) is precisely "atomic operations on an object I do not +own": the contract is that the referenced `uint32_t` is properly aligned (our +header offsets 8, 12, 16 guarantee it on a page-aligned mapping) and that no +plain, non-atomic access touches it while any `atomic_ref` does. Rust's +equivalent is `AtomicU32::from_ptr`, and its safety argument is an aliasing +argument: `&AtomicU32` is a shared reference to an `UnsafeCell`-backed type, +which is the *only* kind of shared reference Rust permits to memory that +mutates underneath you. Casting `*mut u8` into `&u32` and racing it would be +undefined behavior; casting to `&AtomicU32` is the sanctioned spelling, +provided the pointer is aligned, live for the lifetime, and every access to +those four bytes goes through atomics. Go has no blessed story at all: +`sync/atomic` on a `*uint32` pointing into an `unix.Mmap` slice works — +since Go 1.19 the memory model documents atomics as sequentially consistent, +they compile to the same `lock`-prefixed instructions, the mapping is outside +the GC-managed heap so nothing moves it — but *you* carry the alignment +burden (64-bit atomics fault on unaligned addresses on some platforms; our +counter at offset 16 of a page is safe), and nothing stops ordinary code +from racing the same bytes non-atomically. The comment block at the top of +`examples/20-shared-memory-coordination/go/main.go` spells the caveats out. + +> **Sidebar: where is eventfd?** `eventfd(2)` is a lighter kernel counter +> that chapter 9's event loops already waited on, and it measures well (2 µs +> p50 on this host, same harness). But an eventfd is named by *file +> descriptor*, not by anything in the shared page — to coordinate two +> unrelated processes with one you must inherit it across `fork` or pass it +> through a UNIX socket with `SCM_RIGHTS`, exactly the machinery chapter 19 +> (UNIX sockets and fd passing) built for pmon's `logfd`. The futex needs +> none of that: its rendezvous *is* the mapped file. That is the deep +> difference — fd-based channels need an fd-passing bootstrap; a shared +> mapping is its own bootstrap. + +## How the code works + +One page carries everything. The SHKV2 layout — magic at offset 0, the +seqlock `u32` at 8, the futex `u32` at 12, a `u64` update counter at 16, and +eight 64-byte slots (24-byte key, 40-byte value, NUL-padded) from offset 64 — +is deliberately two counters, not one. The `u64` is the truth: total updates +ever published, compared and back-filled from. The `u32` at offset 12 exists +because `FUTEX_WAIT` can only watch 32 bits; it holds the low half of the +counter and is *only* used as the thing to sleep on. The slots form a ring +keyed by `(id-1) % 8`, which buys slack: a watcher that sleeps through a +burst can reconstruct up to eight missed updates from the ring instead of +only ever seeing the latest. `Shm` owns the fd and the mapping (RAII `Fd` + +`Mapping` in C++, `Drop` impls around `OwnedFd` and a munmap guard in Rust, +`defer`-driven `close` in Go) and hands out the three header words through +the per-language atomic views described above. + +The publish path in `cmd_serve` is the writer's half of Figure 20.1: under +`seqlock_write`, store the slot bytes and the `u64` counter; after the +closing bump, store the `u32` futex word with release ordering; then +`FUTEX_WAKE` with `INT_MAX` — wake *every* watcher, since any number of +processes may be parked. The order matters: data first (under the seqlock), +then the word, then the wake, so a woken watcher is guaranteed to find the +word already changed and the data already consistent. + +The watch loop is the reader's half, and its shape is the condition-variable +pattern with the pieces visible. Take a seqlock snapshot; if the counter +advanced, print every missed id from the ring and loop. Only when there is +nothing new does it arm the sleep: + +```cpp + std::uint32_t w = shm->futex_word().load(std::memory_order_acquire); + if (w != static_cast(last)) continue; // moved on: re-read now + futex_wait(shm->futex_ptr(), w, 2000ms); +``` + +Read the guard carefully: it re-checks the word *after* deciding the snapshot +was stale, and passes the exact observed value as `expected`. If a publish +lands between those two lines, the kernel's own compare fails, `FUTEX_WAIT` +returns `EAGAIN`, and the loop re-reads — a wake can be early, late, or +spurious, and the loop is correct anyway, because sleeping is only ever an +optimization over re-checking. The 2-second timeout is the same humility +applied to crashes: a dead writer costs a watcher at most 2 s per lap until +its own 30 s deadline fails the run. + +The reading half of the seqlock is the protocol walked in every language: + +{% include codetabs.html langs="C++|Go|Rust" %} + +```cpp +std::expected seqlock_read(const Shm& shm) { + auto seq = shm.seq(); + for (int attempt = 0; attempt < 1'000'000; ++attempt) { + std::uint32_t s1 = seq.load(std::memory_order_acquire); + if (s1 & 1) { // writer mid-update: retry + std::this_thread::yield(); + continue; + } + Snapshot snap; + snap.counter = shm.counter().load(std::memory_order_relaxed); + std::memcpy(snap.slots.data(), shm.base() + kOffSlots, kSlotsBytes); + std::atomic_thread_fence(std::memory_order_acquire); + if (seq.load(std::memory_order_relaxed) == s1) return snap; + } + return std::unexpected(std::string("seqlock read livelocked")); +} +``` + +```go +func seqlockRead(s *shm) (snapshot, error) { + for attempt := 0; attempt < 1_000_000; attempt++ { + s1 := atomic.LoadUint32(s.seq()) + if s1&1 != 0 { // writer mid-update: retry + runtime.Gosched() + continue + } + var snap snapshot + snap.counter = atomic.LoadUint64(s.counter()) + copy(snap.slots[:], s.m[offSlots:offSlots+slotsBytes]) + if atomic.LoadUint32(s.seq()) == s1 { + return snap, nil + } + } + return snapshot{}, fmt.Errorf("seqlock read livelocked") +} +``` + +```rust +fn seqlock_read(shm: &Shm) -> Result { + for _ in 0..1_000_000 { + let s1 = shm.seq().load(Ordering::Acquire); + if s1 & 1 != 0 { + std::thread::yield_now(); // writer mid-update: retry + continue; + } + let mut snap = Snapshot { counter: shm.counter().load(Ordering::Relaxed), slots: [0; SLOTS_BYTES] }; + unsafe { + ptr::copy_nonoverlapping(shm.slots(), snap.slots.as_mut_ptr(), SLOTS_BYTES); + } + fence(Ordering::Acquire); + if shm.seq().load(Ordering::Relaxed) == s1 { + return Ok(snap); + } + } + bail!("seqlock read livelocked") +} +``` + +Same protocol, three idioms: acquire-load the word, refuse odd, copy the +counter and all 512 slot bytes with a *plain* copy that is allowed to race, +fence, re-check. Go leans on its sequentially-consistent atomics instead of +explicit fences and yields to the goroutine scheduler rather than the OS. +The copy being torn sometimes is not a bug — it is the design; the re-check +is what makes it safe to have raced. + +`cmd_bench` is the measurement harness for the ladder. A consumer thread +(`std::jthread`, a goroutine, a `thread::scope` spawn) blocks on one of the +three channels; the producer publishes and — this is the part that keeps the +numbers meaningful — **spins until the consumer acknowledges round `i-1` +before sending round `i`**, through an `ack` atomic. Without that +serialization, a fast producer queues ahead and you measure queue depth, not +wakeup latency. The mq channel carries the id through a real POSIX queue +(created `O_EXCL`, unlinked by RAII/defer on every path — `verify.lua` +checks `/dev/mqueue` is clean afterwards); Go builds `mq_open`, +`mq_timedsend`, `mq_timedreceive`, and `mq_unlink` from raw `Syscall6` +because glibc's wrappers live in librt — and the kernel, it turns out, wants +the queue name *without* the leading slash glibc requires, which the strace +below confirms. + +Fragile bits, stated plainly: one writer per file is an unchecked assumption +(two servers would interleave seqlock bumps and corrupt each other — a +`flock` would fix it and chapter 25's futex-built locks are the deeper +answer); the futex word is 32 bits, so a watcher blind for exactly 2³² +updates would miss a wake until the 2 s timeout catches it; the ring +back-fills at most 8 ids, and a watcher asleep for more sees only the latest +8; and the bench's `p50_us=0` for futex is truncation, not magic — the +serialized handshake means the consumer often observes the word before it +ever reaches the syscall, so the fast path is a sub-microsecond atomic load. + +## Errors, three ways + +The shared contract is exit 2 for usage, exit 1 with a `shmkv:`-prefixed +line on stderr for runtime failures — and unlike fwatch's byte-identical +diagnostics, this contract deliberately pins only the prefix and the codes, +so each language's errno formatting shows through: C++ formats +`strerror(errno)` into `std::expected`'s error string (`shmkv: open +/tmp/nope.shm: No such file or directory`), Go's wrapped `%w` chain +lower-cases it (`…: no such file or directory`), and Rust's +`anyhow::Context` appends its own tail (`…: No such file or directory (os +error 2)`). All three were exercised this session and all exited 1. Domain +errors the kernel cannot detect get one shared spelling: a file that exists +but lacks the magic prints `bad magic (want SHKV2)` identically everywhere, +and the futex-specific errnos — `EAGAIN`, `ETIMEDOUT`, `EINTR` — are not +errors at all here; every implementation swallows them and re-reads shared +state, because the loop, not the syscall, owns correctness. + +## Concurrency lens + +Everything in this chapter is the concurrency lens, but the Go caveat +deserves its own light. `FUTEX_WAIT` is a blocking syscall: it parks the +calling **OS thread** in the kernel, where the goroutine scheduler cannot +reach it. The runtime's sysmon notices the stall and hands the thread's P to +another thread, so the program keeps running — but every concurrent futex +waiter pins one kernel thread for its whole sleep. One watcher per process is +fine; a thousand goroutines each parked in `FUTEX_WAIT` would be a thousand +OS threads, which is precisely the degenerate case channels and the +netpoller exist to avoid. The comment at the top of `go/main.go` documents +this trade so the pattern is not copied blindly. The second thing worth +naming: the memory-ordering vocabulary of chapter 6 — acquire, release, +fences — crossed a process boundary in this chapter without changing at all. +Cache coherence does not know what a process is; orderings pair between any +two threads that touch the same physical page, whichever address spaces they +live in. That is why `seqlock_write`'s release fence in a Go server orders +writes that a Rust watcher's acquire fence observes. + +## Build, run, observe + +```bash +[host]$ cd examples/20-shared-memory-coordination && ./demo.sh +``` + +Cross-language is the point, so drive it by hand with a Go writer and a Rust +watcher on one file: + +```bash +[host]$ ./go/bin/app serve /tmp/kv.shm --updates 6 --interval-ms 100 & +[host]$ ./rust/target/release/app watch /tmp/kv.shm --events 6 +watch: file=/tmp/kv.shm events=6 +observed update 1: k1=value-1 +observed update 2: k2=value-2 +observed update 3: k3=value-3 +observed update 4: k4=value-4 +observed update 5: k5=value-5 +observed update 6: k6=value-6 +watch: complete events=6 +``` + +The contract is the page plus the futex, not the runtime — the Go process's +`FUTEX_WAKE` lands in the Rust process's `FUTEX_WAIT`. Afterwards the file +itself reads out the whole protocol: + +```bash +[host]$ xxd -l 96 /tmp/kv.shm +00000000: 5348 4b56 3200 0000 0c00 0000 0600 0000 SHKV2........... +00000010: 0600 0000 0000 0000 0000 0000 0000 0000 ................ +00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ +00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................ +00000040: 6b31 0000 0000 0000 0000 0000 0000 0000 k1.............. +00000050: 0000 0000 0000 0000 7661 6c75 652d 3100 ........value-1. +``` + +Little-endian, offset 8: the seqlock word is `0x0c` = 12 — exactly two bumps +for each of six publishes, resting even. Offset 12: futex word 6. Offset 16: +`u64` counter 6. Offset 64: slot 0 still holds `k1`/`value-1`, id 1 having +landed at `(1-1) % 8`. Then the ladder, measured — 2,000 serialized rounds +per channel, C++ bench: + +```bash +[host]$ for ch in futex mq poll; do ./cpp/build/release/app bench /tmp/b.shm --rounds 2000 --channel $ch; done +bench: channel=futex rounds=2000 p50_us=0 p99_us=4 +bench: channel=mq rounds=2000 p50_us=2 p99_us=14 +bench: channel=poll rounds=2000 p50_us=1056 p99_us=1083 +``` + +The Go and Rust benches bracket the same values (Go: 0/4, 3/6, 1003/1021; +Rust: 0/0, 2/5, 1061/1153 µs). Folding in the fd-based channels from +chapters 18 and 19 — pipe, UNIX stream socketpair, and the eventfd from the +sidebar, re-measured this session with the identical serialized-round +harness rebuilt around `read(2)` — gives the matrix: + +| Channel | Kernel object | p50 (µs) | p99 (µs) | Waiter blocks in | Chapter | +|---|---|---|---|---|---| +| futex wake | wait queue on (inode, offset) | <1 | 4 | `futex(FUTEX_WAIT)` | this one | +| pipe | pipe buffer | 2 | 5 | `read(2)` | 18 | +| eventfd | 64-bit counter | 2 | 6 | `read(2)` | 9, 19 | +| POSIX mq | message queue | 2 | 14 | `mq_timedreceive(2)` | this one | +| UNIX socket | socket buffer | 3 | 12 | `read(2)` | 19 | +| 1 ms poll loop | none | 1,056 | 1,083 | `nanosleep(2)` | rung 0 | + +{% include excalidraw.html + file="20-ipc-matrix" + alt="A ladder of six boxes ordered left to right by latency: futex wake p50 under 1 microsecond p99 4, eventfd 2 and 6, pipe 2 and 5, POSIX mq 2 and 14, UNIX socket 3 and 12, then a dark box for the 1 millisecond poll loop at p50 1056 and p99 1083 microseconds with an amber arrow labeled roughly 350x. Below, two summary boxes: the scheduler evidence of 8 versus 3774 voluntary context switches in a 4 second window, and rusage counts of 120 versus 2002 voluntary switches over 2000 rounds." + caption="Figure 20.2 — the coordination ladder as measured on this host: every blocking channel lands within one order of magnitude, and the poll loop pays its nap" %} + +Two takeaways, plainly bounded by this host (an idle 8-core/16-thread Fedora +44 laptop; treat the rankings as portable and the absolutes as local). The +fast one: *every* blocking channel — futex, pipe, eventfd, mq, UDS — lands +in single-digit microseconds at p50; within that band, choose by shape +(named vs fd-based, data-carrying vs pure signal), not speed, though the +futex is alone in costing zero syscalls when nothing is contended. The slow +one: the poll loop is ~350× worse at p50 than the slowest blocking channel +and over 1,000× worse than the futex, and it cannot be fixed, because its +latency is its sleep — the only knob trades latency against wasted wakeups. + +## Cross-check, three ways + +**The syscalls, under `strace`.** The watcher, filtered to `futex` (glibc's +own private-futex noise trimmed), during a three-update serve: + +```bash +[host]$ strace -f -e trace=futex -o watch.trace ./cpp/build/release/app watch /tmp/kv.shm --events 3 +[host]$ grep -v PRIVATE watch.trace +2622774 futex(0x7f7e9662600c, FUTEX_WAIT, 0, {tv_sec=2, tv_nsec=0}) = 0 +2622774 futex(0x7f7e9662600c, FUTEX_WAIT, 1, {tv_sec=2, tv_nsec=0}) = 0 +2622774 futex(0x7f7e9662600c, FUTEX_WAIT, 2, {tv_sec=2, tv_nsec=0}) = 0 +2622774 +++ exited with 0 +++ +``` + +The address ends in `0x00c` — offset 12 in the mapped page, exactly where +the layout puts the futex word — the op is plain `FUTEX_WAIT` (shared, no +`_PRIVATE`), and `expected` climbs 0, 1, 2 as each observed update re-arms +the next sleep. The serve side, traced in a separate run, is the mirror +image: `futex(0x7f79491d300c, FUTEX_WAKE, 2147483647) = 1` three times — wake +`INT_MAX`, one waiter actually woken. And the mq bench under +`-e trace=mq_open,mq_timedsend,mq_timedreceive,mq_unlink`: + +```bash +2623348 mq_open("shmkv-bench-2623348", O_RDWR|O_CREAT|O_EXCL, 0600, {mq_flags=0, mq_maxmsg=8, mq_msgsize=8, mq_curmsgs=0}) = 4 +2623348 mq_timedsend(4, "\1\0\0\0\0\0\0\0", 8, 0, {tv_sec=1784433492, tv_nsec=616289183}) = 0 +2623349 mq_timedreceive(4, "\1\0\0\0\0\0\0\0", 8, NULL, {tv_sec=1784433492, tv_nsec=616316355}) = 8 +... +2623348 mq_unlink("shmkv-bench-2623348") = 0 +``` + +The kernel-level name really has no leading slash — the Go implementation's +raw-syscall comment, confirmed against the C++ binary going through glibc. +Producer tid 2623348 sends, consumer tid 2623349 receives the same 8 bytes, +and the unlink runs on exit. (That traced 5-round run reported +`p50_us=19` — tracing overhead is real; never quote numbers from under +`strace`.) + +**The scheduler's ledger, in `/proc`.** `voluntary_ctxt_switches` counts how +often a task *chose* to sleep. A watcher parked on the futex through a slow +serve (10 updates, 500 ms apart), sampled 4 s apart: + +```bash +[host]$ grep voluntary_ctxt_switches /proc//status # then again after 4 s +voluntary_ctxt_switches: 2 +voluntary_ctxt_switches: 10 +``` + +Eight sleeps in four seconds — one per update observed, nothing else. The +poll-channel bench over the same window (summing all threads via +`/proc//task/*/status`): 285 → 4,059, i.e. **3,774** voluntary switches +in 4 s, ~943/s — the 1 ms nap plus overhead, matching the measured 1,056 µs +p50 almost exactly. Whole-process rusage agrees: `/usr/bin/time -v` on 2,000 +rounds reports 120 voluntary switches for the futex channel (the serialized +handshake usually publishes before the consumer reaches the syscall — the +p50 = 0 fast path, counted) against 2,002 for poll: one nap per round, plus +startup, by construction. `perf stat -e context-switches` was exercised on +both runs but on this host counted zero: stock Fedora ships +`kernel.perf_event_paranoid = 2`, which restricts unprivileged perf to +user-space counting, and context switches happen in the kernel — the +elapsed-time line still tells the story (0.004 s futex vs 2.125 s poll for +the same 2,000 rounds), and the chapter 28 debugging toolbelt revisits perf +with `CAP_PERFMON`. + +> **On the lab VM** unverified — +> the eBPF view of this chapter is `offcputime` proving the watcher's stack +> is parked in `futex_wait_queue` (not spinning) and `futexctn` summarizing +> contention fleet-wide; bcc-tools need privileges this host's lane does not +> use, and chapter 30 (eBPF observation tools) exercises exactly that on the +> `systems-target` VM. + +## What you learned + +- The kernel's one coordination primitive is the futex — atomically + check-then-sleep on a 32-bit word, wake its queue — and a *shared* futex + keyed by `(inode, offset)` lets unrelated processes rendezvous through a + mapped file with zero syscalls on the uncontended path. +- A seqlock suits read-mostly shared data: the writer bumps the sequence word + twice (odd = in progress, and the closing bump makes any overlap + detectable), readers copy optimistically and retry on mismatch — torn + copies are designed-for, discarded, and bounded. +- Process-shared atomics have per-language rules: `std::atomic_ref` and + `AtomicU32::from_ptr` bind atomic operations (and, in Rust, the aliasing + contract) onto mapped words you do not own; Go's `sync/atomic` works on + mmapped memory but leaves alignment to you, and `FUTEX_WAIT` parks a whole + OS thread per waiting goroutine. +- Measured on this host, every blocking IPC channel notifies in single-digit + microseconds at p50 while a 1 ms poll loop pays three orders of magnitude + more — and `/proc`'s `voluntary_ctxt_switches` (8 vs 3,774 in the same + 4 s) is the scheduler's own testimony as to why. + +Next, **TCP servers**: the socket leaves the machine, and listen/accept, +backlogs, and `SO_REUSEPORT` decide who gets the connection. + +--- + +

verified — every number and +excerpt above was produced on the Fedora 44 reference host this session: the +runner printed 20-shared-memory-coordination PASS PASS PASS +(3 passed, 0 failed, 0 skipped); the Go-serve/Rust-watch transcript, the +xxd dump (seq 12, futex word 6, counter 6), the bench rows for +all three languages, and the pipe/UDS/eventfd comparison rows all come from +runs performed now; the strace excerpts are real trimmed output (shared +FUTEX_WAIT at offset 0xc with expected 0/1/2, +FUTEX_WAKE INT_MAX returning 1, slashless kernel mq names, +mq_unlink on exit); the context-switch evidence (watch 2→10, +poll 285→4,059 over 4 s; rusage 120 vs 2,002) was sampled live; and the +perf_event_paranoid = 2 limitation was observed, not assumed. +The bcc-tools callout is unverified as marked, and all latency absolutes are +plainly bounded by this host.

diff --git a/assets/diagrams/11-fork-exec-wait-seq.excalidraw b/assets/diagrams/11-fork-exec-wait-seq.excalidraw new file mode 100644 index 0000000..75894bb --- /dev/null +++ b/assets/diagrams/11-fork-exec-wait-seq.excalidraw @@ -0,0 +1,1519 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r450229643", + "type": "rectangle", + "x": 20, + "y": 60, + "width": 430, + "height": 470, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 511503243, + "versionNonce": 124937714, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t205664474", + "type": "text", + "x": 32, + "y": 66, + "width": 85.80000000000001, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1598038233, + "versionNonce": 1504475946, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "parent \u2014 pmon", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "parent \u2014 pmon", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r480250694", + "type": "rectangle", + "x": 490, + "y": 60, + "width": 430, + "height": 470, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef7ee", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1181858177, + "versionNonce": 1768305437, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t68484746", + "type": "text", + "x": 502, + "y": 66, + "width": 125.4, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1006656582, + "versionNonce": 606005580, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child \u2192 the command", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child \u2192 the command", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1466222267", + "type": "rectangle", + "x": 60, + "y": 95, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 278968663, + "versionNonce": 234655914, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t465466671", + "type": "text", + "x": 70, + "y": 103, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1907619890, + "versionNonce": 1802084347, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe2(O_CLOEXEC)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe2(O_CLOEXEC)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1039604720", + "type": "text", + "x": 70, + "y": 123, + "width": 217.8, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1761436640, + "versionNonce": 88331470, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "self-pipe: exec verdict travels here", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "self-pipe: exec verdict travels here", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1209253571", + "type": "rectangle", + "x": 60, + "y": 175, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1623895219, + "versionNonce": 424434144, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1188782694", + "type": "text", + "x": 70, + "y": 183, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 127128216, + "versionNonce": 613572900, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fork() \u2192 clone(SIGCHLD)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fork() \u2192 clone(SIGCHLD)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1433015983", + "type": "text", + "x": 70, + "y": 203, + "width": 254.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 703875586, + "versionNonce": 1955508980, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "COW: pages shared until either side writes", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "COW: pages shared until either side writes", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1940807535", + "type": "rectangle", + "x": 530, + "y": 175, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1807746560, + "versionNonce": 485418574, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1287261031", + "type": "text", + "x": 540, + "y": 183, + "width": 215.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 283948386, + "versionNonce": 67005482, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "same program, pid of its own", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "same program, pid of its own", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t493941636", + "type": "text", + "x": 540, + "y": 203, + "width": 254.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 929687353, + "versionNonce": 409291320, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd table copied \u2014 pipe write end inherited", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd table copied \u2014 pipe write end inherited", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r867319244", + "type": "rectangle", + "x": 530, + "y": 265, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1745814890, + "versionNonce": 397342590, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t948022841", + "type": "text", + "x": 540, + "y": 273, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1244127339, + "versionNonce": 752533182, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "execvp(CMD)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "execvp(CMD)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t164009135", + "type": "text", + "x": 540, + "y": 293, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 794108159, + "versionNonce": 1417943345, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "success: O_CLOEXEC closes the pipe end", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "success: O_CLOEXEC closes the pipe end", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r441424012", + "type": "rectangle", + "x": 60, + "y": 265, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 713085200, + "versionNonce": 402871830, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t68871678", + "type": "text", + "x": 70, + "y": 273, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 707436073, + "versionNonce": 1787603464, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read(pipe)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read(pipe)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t840172856", + "type": "text", + "x": 70, + "y": 293, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 429415175, + "versionNonce": 1846569835, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "EOF = exec happened \u00b7 4 bytes = errno", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "EOF = exec happened \u00b7 4 bytes = errno", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1751320462", + "type": "rectangle", + "x": 530, + "y": 355, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1324499767, + "versionNonce": 629187317, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1657499803", + "type": "text", + "x": 540, + "y": 363, + "width": 261.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 271841915, + "versionNonce": 874347213, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "CMD runs \u2026 then exits or is killed", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CMD runs \u2026 then exits or is killed", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1948731190", + "type": "text", + "x": 540, + "y": 383, + "width": 205.70000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1310732910, + "versionNonce": 230584254, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "status 0/1/42 \u00b7 or SIGTERM/SIGKILL", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "status 0/1/42 \u00b7 or SIGTERM/SIGKILL", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1405974167", + "type": "rectangle", + "x": 60, + "y": 355, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 40379604, + "versionNonce": 1002745103, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t609143602", + "type": "text", + "x": 70, + "y": 363, + "width": 254.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1859411719, + "versionNonce": 1182902286, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "waitpid / wait4(&status, &rusage)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "waitpid / wait4(&status, &rusage)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t185405556", + "type": "text", + "x": 70, + "y": 383, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1272270386, + "versionNonce": 146545748, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "blocks; reaps the zombie", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "blocks; reaps the zombie", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r416826012", + "type": "rectangle", + "x": 60, + "y": 445, + "width": 350, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 998496625, + "versionNonce": 1282880110, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t38840937", + "type": "text", + "x": 70, + "y": 453, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 674277744, + "versionNonce": 1425540321, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "report + mirror the fate", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "report + mirror the fate", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1444333130", + "type": "text", + "x": 70, + "y": 473, + "width": 248.05, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 904524590, + "versionNonce": 1162847890, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "status s \u2192 exit s \u00b7 signal n \u2192 exit 128+n", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "status s \u2192 exit s \u00b7 signal n \u2192 exit 128+n", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1658318477", + "type": "arrow", + "x": 235, + "y": 151, + "width": 0, + "height": 24, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 188753645, + "versionNonce": 1977231835, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 24 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a117015626", + "type": "arrow", + "x": 410, + "y": 203, + "width": 120, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 817254435, + "versionNonce": 971376433, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 120, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1586828205", + "type": "text", + "x": 470.0, + "y": 189.0, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1715258510, + "versionNonce": 1001936733, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "returns 0 in the child", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "returns 0 in the child", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a839315366", + "type": "arrow", + "x": 235, + "y": 231, + "width": 0, + "height": 34, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1261353350, + "versionNonce": 618748914, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1977148785", + "type": "text", + "x": 235.0, + "y": 234.0, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1929875719, + "versionNonce": 1700553055, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "returns child pid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "returns child pid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a816103161", + "type": "arrow", + "x": 705, + "y": 231, + "width": 0, + "height": 34, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 323937088, + "versionNonce": 697310767, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a55482874", + "type": "arrow", + "x": 530, + "y": 293, + "width": 120, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1298655134, + "versionNonce": 1391922991, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -120, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1037073561", + "type": "text", + "x": 470.0, + "y": 279.0, + "width": 235.95000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 760026308, + "versionNonce": 1931675058, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "EOF (success) / errno then _exit(127)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "EOF (success) / errno then _exit(127)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a912080084", + "type": "arrow", + "x": 705, + "y": 321, + "width": 0, + "height": 34, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 221412536, + "versionNonce": 1643876237, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a429043980", + "type": "arrow", + "x": 235, + "y": 321, + "width": 0, + "height": 34, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 170080838, + "versionNonce": 1851917677, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1347324053", + "type": "arrow", + "x": 530, + "y": 383, + "width": 120, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1740832105, + "versionNonce": 906404539, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -120, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t354780417", + "type": "text", + "x": 470.0, + "y": 369.0, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 365862782, + "versionNonce": 65180018, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "zombie until reaped \u00b7 SIGCHLD", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "zombie until reaped \u00b7 SIGCHLD", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1411214881", + "type": "arrow", + "x": 235, + "y": 411, + "width": 0, + "height": 34, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 458530070, + "versionNonce": 58612359, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 34 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1805743549", + "type": "text", + "x": 20, + "y": 20, + "width": 629.2, + "height": 16.25, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1976822687, + "versionNonce": 1803920343, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon\u2019s lifecycle: fork \u2192 exec \u2192 wait, with a CLOEXEC self-pipe carrying the exec verdict", + "fontSize": 13, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon\u2019s lifecycle: fork \u2192 exec \u2192 wait, with a CLOEXEC self-pipe carrying the exec verdict", + "lineHeight": 1.25, + "baseline": 13 + }, + { + "id": "t948498138", + "type": "text", + "x": 20, + "y": 545, + "width": 1107.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 926005024, + "versionNonce": 1029380755, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Go\u2019s os/exec and Rust\u2019s Command run this same sequence internally \u2014 strace shows clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD) and clone3({CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND})", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Go\u2019s os/exec and Rust\u2019s Command run this same sequence internally \u2014 strace shows clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD) and clone3({CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND})", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/11-fork-exec-wait-seq.svg b/assets/diagrams/11-fork-exec-wait-seq.svg new file mode 100644 index 0000000..3e201d8 --- /dev/null +++ b/assets/diagrams/11-fork-exec-wait-seq.svg @@ -0,0 +1,50 @@ + + + +parent — pmon + +child → the command + +pipe2(O_CLOEXEC) +self-pipe: exec verdict travels here + +fork() → clone(SIGCHLD) +COW: pages shared until either side writes + +same program, pid of its own +fd table copied — pipe write end inherited + +execvp(CMD) +success: O_CLOEXEC closes the pipe end + +read(pipe) +EOF = exec happened · 4 bytes = errno + +CMD runs … then exits or is killed +status 0/1/42 · or SIGTERM/SIGKILL + +waitpid / wait4(&status, &rusage) +blocks; reaps the zombie + +report + mirror the fate +status s → exit s · signal n → exit 128+n + + +returns 0 in the child +returns 0 in the child + +returns child pid +returns child pid + + +EOF (success) / errno then _exit(127) +EOF (success) / errno then _exit(127) + + + +zombie until reaped · SIGCHLD +zombie until reaped · SIGCHLD + +pmon’s lifecycle: fork → exec → wait, with a CLOEXEC self-pipe carrying the exec verdict +Go’s os/exec and Rust’s Command run this same sequence internally — strace shows clone(CLONE_VM|CLONE_PIDFD|CLONE_VFORK|SIGCHLD) and clone3({CLONE_VM|CLONE_VFORK|CLONE_CLEAR_SIGHAND}) + \ No newline at end of file diff --git a/assets/diagrams/11-process-lifecycle-states.excalidraw b/assets/diagrams/11-process-lifecycle-states.excalidraw new file mode 100644 index 0000000..9387c19 --- /dev/null +++ b/assets/diagrams/11-process-lifecycle-states.excalidraw @@ -0,0 +1,1412 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r496319537", + "type": "rectangle", + "x": 20, + "y": 60, + "width": 570, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 528825229, + "versionNonce": 1029713419, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t692979133", + "type": "text", + "x": 32, + "y": 66, + "width": 231.00000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1986201439, + "versionNonce": 202692106, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "alive \u2014 address space, threads, fds", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "alive \u2014 address space, threads, fds", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r765105247", + "type": "rectangle", + "x": 610, + "y": 60, + "width": 310, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1328618229, + "versionNonce": 1159341931, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t784831387", + "type": "text", + "x": 622, + "y": 66, + "width": 217.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 445678751, + "versionNonce": 1554922252, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "after exit \u2014 only the task record", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "after exit \u2014 only the task record", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r788751560", + "type": "rectangle", + "x": 40, + "y": 150, + "width": 130, + "height": 64, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1886929471, + "versionNonce": 1634619398, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t527970461", + "type": "text", + "x": 50, + "y": 158, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 50579976, + "versionNonce": 1243209820, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fork / clone3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fork / clone3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t544170101", + "type": "text", + "x": 50, + "y": 178, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1523046247, + "versionNonce": 1458531969, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "returns twice", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "returns twice", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1345191914", + "type": "rectangle", + "x": 230, + "y": 100, + "width": 150, + "height": 64, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 557788388, + "versionNonce": 149840263, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1636833587", + "type": "text", + "x": 240, + "y": 108, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1007064551, + "versionNonce": 879177271, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Running R", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Running R", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1396005545", + "type": "text", + "x": 240, + "y": 128, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 639300529, + "versionNonce": 654372288, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "on a CPU", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "on a CPU", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r646539280", + "type": "rectangle", + "x": 230, + "y": 250, + "width": 150, + "height": 64, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 234800589, + "versionNonce": 184382571, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1730070482", + "type": "text", + "x": 240, + "y": 258, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1789566097, + "versionNonce": 129549077, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Sleeping S", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Sleeping S", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t139603676", + "type": "text", + "x": 240, + "y": 278, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1873595931, + "versionNonce": 136417777, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "blocked in a syscall", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "blocked in a syscall", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r863691805", + "type": "rectangle", + "x": 440, + "y": 250, + "width": 130, + "height": 64, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 644588852, + "versionNonce": 1949606285, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1723432692", + "type": "text", + "x": 450, + "y": 258, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 895187458, + "versionNonce": 1813538768, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Stopped T", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Stopped T", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1525693570", + "type": "text", + "x": 450, + "y": 278, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 786472165, + "versionNonce": 273615925, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "job control", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "job control", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r980108652", + "type": "rectangle", + "x": 640, + "y": 100, + "width": 250, + "height": 84, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 619504765, + "versionNonce": 1512190851, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t302377235", + "type": "text", + "x": 650, + "y": 108, + "width": 69.30000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 676949389, + "versionNonce": 797588991, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Zombie Z", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Zombie Z", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1430922169", + "type": "text", + "x": 650, + "y": 128, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1829262179, + "versionNonce": 1003594150, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": " in ps f", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": " in ps f", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t743726897", + "type": "text", + "x": 650, + "y": 143, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1023310535, + "versionNonce": 858294042, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exit status + rusage only", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exit status + rusage only", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t439198454", + "type": "text", + "x": 650, + "y": 158, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1047593573, + "versionNonce": 1721110147, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "VmRSS gone, rss field 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "VmRSS gone, rss field 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1009881962", + "type": "rectangle", + "x": 660, + "y": 260, + "width": 210, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 677806914, + "versionNonce": 324970486, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t824739155", + "type": "text", + "x": 670, + "y": 268, + "width": 215.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1880290152, + "versionNonce": 1183829520, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reaped \u2014 pid table slot free", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reaped \u2014 pid table slot free", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t428988664", + "type": "text", + "x": 670, + "y": 288, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1896568156, + "versionNonce": 1975346827, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "status copied to the parent", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "status copied to the parent", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a325666420", + "type": "arrow", + "x": 170, + "y": 165, + "width": 60, + "height": 25, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1385651143, + "versionNonce": 240709239, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 60, + -25 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1376574843", + "type": "text", + "x": 200.0, + "y": 138.5, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 491540076, + "versionNonce": 1975472333, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child starts", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child starts", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a328716426", + "type": "arrow", + "x": 290, + "y": 164, + "width": 0, + "height": 86, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 703948108, + "versionNonce": 1846602108, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 86 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1435032555", + "type": "text", + "x": 290.0, + "y": 193.0, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 400587385, + "versionNonce": 454316465, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read / sleep", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read / sleep", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a294987673", + "type": "arrow", + "x": 320, + "y": 250, + "width": 0, + "height": 86, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 610722850, + "versionNonce": 990860315, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -86 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t345933182", + "type": "text", + "x": 320.0, + "y": 193.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 691553799, + "versionNonce": 1766849525, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "wakeup", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "wakeup", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a793887457", + "type": "arrow", + "x": 380, + "y": 270, + "width": 60, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1791175523, + "versionNonce": 1397907526, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 60, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t493799229", + "type": "text", + "x": 410.0, + "y": 256.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1538889811, + "versionNonce": 1390563893, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIGSTOP", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIGSTOP", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1726303899", + "type": "arrow", + "x": 440, + "y": 300, + "width": 60, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 608879663, + "versionNonce": 680438226, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -60, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1851091451", + "type": "text", + "x": 410.0, + "y": 286.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1495574748, + "versionNonce": 1328492545, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIGCONT", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIGCONT", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a497330329", + "type": "arrow", + "x": 380, + "y": 132, + "width": 260, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1095278643, + "versionNonce": 800521567, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 260, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1202691219", + "type": "text", + "x": 510.0, + "y": 118.0, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 637837496, + "versionNonce": 504547377, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exit(2) / fatal signal", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exit(2) / fatal signal", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a976291379", + "type": "arrow", + "x": 765, + "y": 184, + "width": 0, + "height": 76, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 187714938, + "versionNonce": 268730934, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 76 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1938367401", + "type": "text", + "x": 765.0, + "y": 208.0, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1960328369, + "versionNonce": 1063287613, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "parent wait4 / waitid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "parent wait4 / waitid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t348704568", + "type": "text", + "x": 20, + "y": 20, + "width": 600.6, + "height": 16.25, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1997668825, + "versionNonce": 1595438948, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Process states as ps STAT letters \u2014 the kernel keeps a zombie until someone reaps it", + "fontSize": 13, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Process states as ps STAT letters \u2014 the kernel keeps a zombie until someone reaps it", + "lineHeight": 1.25, + "baseline": 13 + }, + { + "id": "t1761206140", + "type": "text", + "x": 20, + "y": 380, + "width": 756.2500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 831364051, + "versionNonce": 1792180355, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "observed: pmon stopped mid-wait (STAT TN) left its exited child as ZN \u2018[sleep] \u2019 for a 4 s window; SIGCONT reaped it", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "observed: pmon stopped mid-wait (STAT TN) left its exited child as ZN \u2018[sleep] \u2019 for a 4 s window; SIGCONT reaped it", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/11-process-lifecycle-states.svg b/assets/diagrams/11-process-lifecycle-states.svg new file mode 100644 index 0000000..ade20f4 --- /dev/null +++ b/assets/diagrams/11-process-lifecycle-states.svg @@ -0,0 +1,50 @@ + + + +alive — address space, threads, fds + +after exit — only the task record + +fork / clone3 +returns twice + +Running R +on a CPU + +Sleeping S +blocked in a syscall + +Stopped T +job control + +Zombie Z +<defunct> in ps f +exit status + rusage only +VmRSS gone, rss field 0 + +reaped — pid table slot free +status copied to the parent + +child starts +child starts + +read / sleep +read / sleep + +wakeup +wakeup + +SIGSTOP +SIGSTOP + +SIGCONT +SIGCONT + +exit(2) / fatal signal +exit(2) / fatal signal + +parent wait4 / waitid +parent wait4 / waitid +Process states as ps STAT letters — the kernel keeps a zombie until someone reaps it +observed: pmon stopped mid-wait (STAT TN) left its exited child as ZN ‘[sleep] <defunct>’ for a 4 s window; SIGCONT reaped it + \ No newline at end of file diff --git a/assets/diagrams/12-signal-delivery-paths.excalidraw b/assets/diagrams/12-signal-delivery-paths.excalidraw new file mode 100644 index 0000000..cc0eff0 --- /dev/null +++ b/assets/diagrams/12-signal-delivery-paths.excalidraw @@ -0,0 +1,2032 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r585006092", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 900, + "height": 190, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 622063416, + "versionNonce": 1337574242, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t498282517", + "type": "text", + "x": 32, + "y": 26, + "width": 323.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1101525496, + "versionNonce": 1602537561, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel \u2014 every signal passes the same three words", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel \u2014 every signal passes the same three words", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1032425404", + "type": "rectangle", + "x": 20, + "y": 230, + "width": 900, + "height": 240, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 645560421, + "versionNonce": 1494935167, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t786622167", + "type": "text", + "x": 32, + "y": 236, + "width": 501.6, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1365952268, + "versionNonce": 1331288654, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon supervise \u2014 three ways to hear the same signals, none in signal context", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon supervise \u2014 three ways to hear the same signals, none in signal context", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r598491652", + "type": "rectangle", + "x": 40, + "y": 55, + "width": 150, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 465126405, + "versionNonce": 1106313751, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1235820508", + "type": "text", + "x": 50, + "y": 63, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 455064631, + "versionNonce": 218528111, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kill -TERM ", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kill -TERM ", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1281326419", + "type": "text", + "x": 50, + "y": 83, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1941035529, + "versionNonce": 1688181600, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tgkill, terminal ^C", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tgkill, terminal ^C", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r873352062", + "type": "rectangle", + "x": 40, + "y": 125, + "width": 150, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1163884097, + "versionNonce": 1273507206, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1839275286", + "type": "text", + "x": 50, + "y": 133, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 905415382, + "versionNonce": 1364012759, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child exits", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child exits", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1296921273", + "type": "text", + "x": 50, + "y": 153, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1614103830, + "versionNonce": 4657451, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel raises SIGCHLD", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel raises SIGCHLD", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r301817459", + "type": "rectangle", + "x": 240, + "y": 55, + "width": 200, + "height": 58, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1383108780, + "versionNonce": 1298308348, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1236264407", + "type": "text", + "x": 250, + "y": 63, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1732977168, + "versionNonce": 1552034686, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "disposition (per-process)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "disposition (per-process)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t118076754", + "type": "text", + "x": 250, + "y": 83, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1370729174, + "versionNonce": 825452387, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIG_DFL / SIG_IGN / handler", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIG_DFL / SIG_IGN / handler", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r701954369", + "type": "rectangle", + "x": 240, + "y": 128, + "width": 200, + "height": 58, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 110939747, + "versionNonce": 1611328088, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1629931015", + "type": "text", + "x": 250, + "y": 136, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 843888316, + "versionNonce": 1503804339, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mask (per-thread)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mask (per-thread)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1860125726", + "type": "text", + "x": 250, + "y": 156, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 829031168, + "versionNonce": 58319530, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "blocked signals wait", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "blocked signals wait", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1706583714", + "type": "rectangle", + "x": 490, + "y": 90, + "width": 200, + "height": 58, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1423358464, + "versionNonce": 1903342459, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t176484996", + "type": "text", + "x": 500, + "y": 98, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 757184596, + "versionNonce": 1666077880, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pending set", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pending set", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t404849259", + "type": "text", + "x": 500, + "y": 118, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1598819740, + "versionNonce": 773778095, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one bit per signal \u2014 coalesces", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one bit per signal \u2014 coalesces", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1929108026", + "type": "rectangle", + "x": 730, + "y": 90, + "width": 170, + "height": 58, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1168388303, + "versionNonce": 1638086355, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1077234921", + "type": "text", + "x": 740, + "y": 98, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1310111428, + "versionNonce": 1896365227, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "delivery", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "delivery", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t119673380", + "type": "text", + "x": 740, + "y": 118, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1728278136, + "versionNonce": 207967048, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "only when unblocked", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "only when unblocked", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1820806622", + "type": "rectangle", + "x": 40, + "y": 270, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1141368843, + "versionNonce": 629821178, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1502845550", + "type": "text", + "x": 50, + "y": 278, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 262922666, + "versionNonce": 997408523, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "C++ - signalfd(2)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "C++ - signalfd(2)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1502902010", + "type": "text", + "x": 50, + "y": 298, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1969783633, + "versionNonce": 1481548508, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mask blocks all four signals", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mask blocks all four signals", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1189609796", + "type": "rectangle", + "x": 40, + "y": 390, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 891504344, + "versionNonce": 1480647614, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t639073874", + "type": "text", + "x": 50, + "y": 398, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 749655562, + "versionNonce": 1268963416, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll + read(sfd)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll + read(sfd)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1624180022", + "type": "text", + "x": 50, + "y": 418, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1531652457, + "versionNonce": 1606451351, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "signalfd_siginfo record", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "signalfd_siginfo record", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r159862551", + "type": "rectangle", + "x": 340, + "y": 270, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 994592140, + "versionNonce": 1518802246, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1933835294", + "type": "text", + "x": 350, + "y": 278, + "width": 154.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1179561634, + "versionNonce": 309589107, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Go - runtime handler", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Go - runtime handler", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1949686345", + "type": "text", + "x": 350, + "y": 298, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 231624731, + "versionNonce": 566874997, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "installed by 114 rt_sigaction", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "installed by 114 rt_sigaction", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r37367043", + "type": "rectangle", + "x": 340, + "y": 390, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1496523350, + "versionNonce": 1169718170, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t546467986", + "type": "text", + "x": 350, + "y": 398, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1547546668, + "versionNonce": 1915935856, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "signal.Notify channel", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "signal.Notify channel", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1420833855", + "type": "text", + "x": 350, + "y": 418, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1374803910, + "versionNonce": 1212034777, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "select over sig / wait / timer", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "select over sig / wait / timer", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1061127413", + "type": "rectangle", + "x": 640, + "y": 270, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 452163040, + "versionNonce": 933739321, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t585154216", + "type": "text", + "x": 650, + "y": 278, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 194355593, + "versionNonce": 572604650, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Rust - self-pipe handler", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Rust - self-pipe handler", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1405578758", + "type": "text", + "x": 650, + "y": 298, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 811984327, + "versionNonce": 242685856, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "AtomicBool + write(doorbell)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "AtomicBool + write(doorbell)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r880898685", + "type": "rectangle", + "x": 640, + "y": 390, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1139153558, + "versionNonce": 1204526343, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1639916193", + "type": "text", + "x": 650, + "y": 398, + "width": 223.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1733653668, + "versionNonce": 1845864534, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll pipe, drain, check flags", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll pipe, drain, check flags", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t657297147", + "type": "text", + "x": 650, + "y": 418, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 588552775, + "versionNonce": 1753078657, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "waitpid(WNOHANG) for truth", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "waitpid(WNOHANG) for truth", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a788251330", + "type": "arrow", + "x": 190, + "y": 81, + "width": 50, + "height": 3, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1566303304, + "versionNonce": 1348136400, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 50, + 3 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1253619272", + "type": "arrow", + "x": 190, + "y": 151, + "width": 50, + "height": 6, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 474126109, + "versionNonce": 1346003739, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 50, + 6 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1254577134", + "type": "arrow", + "x": 440, + "y": 84, + "width": 50, + "height": 24, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1906651320, + "versionNonce": 57252121, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 50, + 24 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1767754716", + "type": "text", + "x": 465.0, + "y": 82.0, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1158870109, + "versionNonce": 1189357567, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "checked first", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "checked first", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1832579801", + "type": "arrow", + "x": 440, + "y": 157, + "width": 50, + "height": 27, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1210900194, + "versionNonce": 903797098, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 50, + -27 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1560671677", + "type": "text", + "x": 465.0, + "y": 129.5, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1184592776, + "versionNonce": 185683785, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "blocked? stay pending", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "blocked? stay pending", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a292858014", + "type": "arrow", + "x": 690, + "y": 119, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 373542047, + "versionNonce": 504106091, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1136445913", + "type": "arrow", + "x": 170, + "y": 328, + "width": 0, + "height": 62, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1758271886, + "versionNonce": 1078864911, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 62 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1350408311", + "type": "text", + "x": 170.0, + "y": 345.0, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 848116343, + "versionNonce": 1430527113, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read as data", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read as data", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1583538215", + "type": "arrow", + "x": 470, + "y": 328, + "width": 0, + "height": 62, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 77101951, + "versionNonce": 1071778908, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 62 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1366405702", + "type": "text", + "x": 470.0, + "y": 345.0, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1276514074, + "versionNonce": 130762705, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "channel send", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "channel send", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a944482591", + "type": "arrow", + "x": 770, + "y": 328, + "width": 0, + "height": 62, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 513475540, + "versionNonce": 343048526, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 62 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1485747309", + "type": "text", + "x": 770.0, + "y": 345.0, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1333418770, + "versionNonce": 875149749, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "doorbell byte", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "doorbell byte", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1938341013", + "type": "arrow", + "x": 560, + "y": 148, + "width": 390, + "height": 122, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 731218578, + "versionNonce": 284122877, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -390, + 122 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a764542507", + "type": "arrow", + "x": 590, + "y": 148, + "width": 120, + "height": 122, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 641627639, + "versionNonce": 1335691071, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -120, + 122 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1403974184", + "type": "arrow", + "x": 620, + "y": 148, + "width": 150, + "height": 122, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1980489728, + "versionNonce": 13307114, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 150, + 122 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1121699780", + "type": "text", + "x": 730, + "y": 165, + "width": 179.025, + "height": 13.125, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1273767193, + "versionNonce": 598791934, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "handlers may run on any thread,", + "fontSize": 10.5, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "handlers may run on any thread,", + "lineHeight": 1.25, + "baseline": 10.5 + }, + { + "id": "t1390001947", + "type": "text", + "x": 730, + "y": 179, + "width": 161.70000000000002, + "height": 13.125, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1870129548, + "versionNonce": 211660115, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "between any two instructions", + "fontSize": 10.5, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "between any two instructions", + "lineHeight": 1.25, + "baseline": 10.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/12-signal-delivery-paths.svg b/assets/diagrams/12-signal-delivery-paths.svg new file mode 100644 index 0000000..aef01ac --- /dev/null +++ b/assets/diagrams/12-signal-delivery-paths.svg @@ -0,0 +1,66 @@ + + + +kernel — every signal passes the same three words + +pmon supervise — three ways to hear the same signals, none in signal context + +kill -TERM <pid> +tgkill, terminal ^C + +child exits +kernel raises SIGCHLD + +disposition (per-process) +SIG_DFL / SIG_IGN / handler + +mask (per-thread) +blocked signals wait + +pending set +one bit per signal — coalesces + +delivery +only when unblocked + +C++ - signalfd(2) +mask blocks all four signals + +poll + read(sfd) +signalfd_siginfo record + +Go - runtime handler +installed by 114 rt_sigaction + +signal.Notify channel +select over sig / wait / timer + +Rust - self-pipe handler +AtomicBool + write(doorbell) + +poll pipe, drain, check flags +waitpid(WNOHANG) for truth + + + +checked first +checked first + +blocked? stay pending +blocked? stay pending + + +read as data +read as data + +channel send +channel send + +doorbell byte +doorbell byte + + + +handlers may run on any thread, +between any two instructions + \ No newline at end of file diff --git a/assets/diagrams/12-supervisor-state-machine.excalidraw b/assets/diagrams/12-supervisor-state-machine.excalidraw new file mode 100644 index 0000000..02eb1e6 --- /dev/null +++ b/assets/diagrams/12-supervisor-state-machine.excalidraw @@ -0,0 +1,1138 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1960116780", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 900, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 299512269, + "versionNonce": 343755009, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t362970878", + "type": "text", + "x": 32, + "y": 26, + "width": 277.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 234307926, + "versionNonce": 284495850, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon supervise \u2014 one loop, two live states", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon supervise \u2014 one loop, two live states", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1194994896", + "type": "rectangle", + "x": 70, + "y": 100, + "width": 240, + "height": 70, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1876315733, + "versionNonce": 344528300, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1919038255", + "type": "text", + "x": 80, + "y": 108, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 928946933, + "versionNonce": 650332410, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child running", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child running", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1823617302", + "type": "text", + "x": 80, + "y": 128, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1016828728, + "versionNonce": 1359784085, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll timeout = infinite", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll timeout = infinite", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t397498557", + "type": "text", + "x": 80, + "y": 143, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 809538669, + "versionNonce": 1945341030, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "waiting for signal or exit", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "waiting for signal or exit", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1427628439", + "type": "rectangle", + "x": 610, + "y": 100, + "width": 240, + "height": 70, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 313127841, + "versionNonce": 1443086281, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t836961197", + "type": "text", + "x": 620, + "y": 108, + "width": 92.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1106027230, + "versionNonce": 703938543, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "backoff wait", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "backoff wait", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t726267341", + "type": "text", + "x": 620, + "y": 128, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1613804618, + "versionNonce": 1802814438, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no child alive", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no child alive", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t723031785", + "type": "text", + "x": 620, + "y": 143, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1536368399, + "versionNonce": 599637579, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll timeout = deadline", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll timeout = deadline", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1100040280", + "type": "rectangle", + "x": 70, + "y": 350, + "width": 240, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1998822015, + "versionNonce": 1231277617, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t28189590", + "type": "text", + "x": 80, + "y": 358, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 686197953, + "versionNonce": 679299284, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exit 0", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exit 0", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1687936373", + "type": "text", + "x": 80, + "y": 378, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 123613158, + "versionNonce": 1464546149, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "clean exit or shutdown", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "clean exit or shutdown", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1346957382", + "type": "rectangle", + "x": 610, + "y": 350, + "width": 240, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 972603819, + "versionNonce": 1744887863, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1890284463", + "type": "text", + "x": 620, + "y": 358, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 580406103, + "versionNonce": 1196194611, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exit 1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exit 1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t747809272", + "type": "text", + "x": 620, + "y": 378, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1898109101, + "versionNonce": 952698810, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "giving up after N restarts", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "giving up after N restarts", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1076777603", + "type": "rectangle", + "x": 350, + "y": 230, + "width": 220, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1417991176, + "versionNonce": 1957126222, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1131074753", + "type": "text", + "x": 360, + "y": 238, + "width": 107.80000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1032797258, + "versionNonce": 1203947908, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIGHUP: reload", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIGHUP: reload", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t162500721", + "type": "text", + "x": 360, + "y": 258, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1142495048, + "versionNonce": 386264254, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stop child, reset budget,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stop child, reset budget,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t583154171", + "type": "text", + "x": 360, + "y": 273, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1155405964, + "versionNonce": 879955261, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "spawn replacement", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "spawn replacement", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a99902190", + "type": "arrow", + "x": 310, + "y": 120, + "width": 300, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1390383214, + "versionNonce": 832046499, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 300, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t67279851", + "type": "text", + "x": 460.0, + "y": 106.0, + "width": 254.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 849884133, + "versionNonce": 1896471928, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child fails: restart #k, backoff B*2^(k-1)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child fails: restart #k, backoff B*2^(k-1)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a149437363", + "type": "arrow", + "x": 610, + "y": 150, + "width": 300, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1822754270, + "versionNonce": 1265669523, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -300, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t17541476", + "type": "text", + "x": 460.0, + "y": 136.0, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 795574693, + "versionNonce": 898988641, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "backoff elapsed: spawn again", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "backoff elapsed: spawn again", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a410345596", + "type": "arrow", + "x": 190, + "y": 170, + "width": 0, + "height": 180, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 407042966, + "versionNonce": 1581325958, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 180 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t336909287", + "type": "text", + "x": 190.0, + "y": 246.0, + "width": 260.15000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1980811748, + "versionNonce": 1894363242, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "exit 0, or SIGTERM/SIGINT -> forward + reap", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "exit 0, or SIGTERM/SIGINT -> forward + reap", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a847901935", + "type": "arrow", + "x": 730, + "y": 170, + "width": 0, + "height": 180, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1529526746, + "versionNonce": 1533907479, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 180 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t40697553", + "type": "text", + "x": 730.0, + "y": 246.0, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 93846015, + "versionNonce": 1735606059, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "restarts == max", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "restarts == max", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a865914971", + "type": "arrow", + "x": 350, + "y": 250, + "width": 110, + "height": 80, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1234524362, + "versionNonce": 738522016, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -110, + -80 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1870447610", + "type": "arrow", + "x": 570, + "y": 250, + "width": 110, + "height": 80, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 585370789, + "versionNonce": 98067959, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 110, + -80 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1813174078", + "type": "text", + "x": 625.0, + "y": 196.0, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 10984497, + "versionNonce": 190520077, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "also cancels a pending backoff", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "also cancels a pending backoff", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1040960912", + "type": "text", + "x": 70, + "y": 435, + "width": 816.7500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1868744691, + "versionNonce": 1703804347, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Every transition is triggered by an ordinary value read in the main loop - a signalfd record, a channel receive, or a drained doorbell.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Every transition is triggered by an ordinary value read in the main loop - a signalfd record, a channel receive, or a drained doorbell.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/12-supervisor-state-machine.svg b/assets/diagrams/12-supervisor-state-machine.svg new file mode 100644 index 0000000..8da57e5 --- /dev/null +++ b/assets/diagrams/12-supervisor-state-machine.svg @@ -0,0 +1,40 @@ + + + +pmon supervise — one loop, two live states + +child running +poll timeout = infinite +waiting for signal or exit + +backoff wait +no child alive +poll timeout = deadline + +exit 0 +clean exit or shutdown + +exit 1 +giving up after N restarts + +SIGHUP: reload +stop child, reset budget, +spawn replacement + +child fails: restart #k, backoff B*2^(k-1) +child fails: restart #k, backoff B*2^(k-1) + +backoff elapsed: spawn again +backoff elapsed: spawn again + +exit 0, or SIGTERM/SIGINT -> forward + reap +exit 0, or SIGTERM/SIGINT -> forward + reap + +restarts == max +restarts == max + + +also cancels a pending backoff +also cancels a pending backoff +Every transition is triggered by an ordinary value read in the main loop - a signalfd record, a channel receive, or a drained doorbell. + \ No newline at end of file diff --git a/assets/diagrams/13-pid-reuse-race-pidfd.excalidraw b/assets/diagrams/13-pid-reuse-race-pidfd.excalidraw new file mode 100644 index 0000000..b33befa --- /dev/null +++ b/assets/diagrams/13-pid-reuse-race-pidfd.excalidraw @@ -0,0 +1,1924 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r435230663", + "type": "rectangle", + "x": 20, + "y": 40, + "width": 960, + "height": 220, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fdf6f6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 663190722, + "versionNonce": 1819142740, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t795970643", + "type": "text", + "x": 32, + "y": 46, + "width": 415.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 513411066, + "versionNonce": 317548941, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "supervising by pid number \u2014 five steps to signalling a stranger", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "supervising by pid number \u2014 five steps to signalling a stranger", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1468136737", + "type": "rectangle", + "x": 20, + "y": 300, + "width": 960, + "height": 200, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f9f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 641060711, + "versionNonce": 1478354133, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1388723843", + "type": "text", + "x": 32, + "y": 306, + "width": 310.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1493217178, + "versionNonce": 541373088, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "supervising by pidfd \u2014 the fd pins the identity", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "supervising by pidfd \u2014 the fd pins the identity", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r569349741", + "type": "rectangle", + "x": 40, + "y": 80, + "width": 170, + "height": 72, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 639154055, + "versionNonce": 458738510, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1073008390", + "type": "text", + "x": 50, + "y": 88, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 596613164, + "versionNonce": 977918981, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1 spawn", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1 spawn", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t434710155", + "type": "text", + "x": 50, + "y": 108, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 491294749, + "versionNonce": 1210635759, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "remember pid 4242", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "remember pid 4242", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1667085103", + "type": "text", + "x": 50, + "y": 123, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 760406077, + "versionNonce": 1023046065, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "(an integer, nothing more)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "(an integer, nothing more)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1150414431", + "type": "rectangle", + "x": 232, + "y": 80, + "width": 170, + "height": 72, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 502696686, + "versionNonce": 1817916743, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1872263903", + "type": "text", + "x": 242, + "y": 88, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1127403093, + "versionNonce": 817951703, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "2 child crashes", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "2 child crashes", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t546444610", + "type": "text", + "x": 242, + "y": 108, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 874130198, + "versionNonce": 1591864633, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "zombie: pid still", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "zombie: pid still", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1123559458", + "type": "text", + "x": 242, + "y": 123, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 833363644, + "versionNonce": 1892191679, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reserved, status pending", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reserved, status pending", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1986815994", + "type": "rectangle", + "x": 424, + "y": 80, + "width": 180, + "height": 72, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 786234865, + "versionNonce": 601099754, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t371762691", + "type": "text", + "x": 434, + "y": 88, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1873012921, + "versionNonce": 784862109, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "3 reaped elsewhere", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "3 reaped elsewhere", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t580995551", + "type": "text", + "x": 434, + "y": 108, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1484786651, + "versionNonce": 156069453, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "a wait() collects it \u2014", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "a wait() collects it \u2014", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t147730771", + "type": "text", + "x": 434, + "y": 123, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 332104607, + "versionNonce": 71097299, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pid 4242 is now free", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pid 4242 is now free", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1416924447", + "type": "rectangle", + "x": 626, + "y": 80, + "width": 170, + "height": 72, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 556809577, + "versionNonce": 1502379263, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t663328196", + "type": "text", + "x": 636, + "y": 88, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1918583064, + "versionNonce": 217824568, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "4 kernel recycles", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "4 kernel recycles", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t610855087", + "type": "text", + "x": 636, + "y": 108, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 325625566, + "versionNonce": 453574564, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pid 4242 = someone", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pid 4242 = someone", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t711172095", + "type": "text", + "x": 636, + "y": 123, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 461581684, + "versionNonce": 1168627938, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "else's new process", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "else's new process", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1685960918", + "type": "rectangle", + "x": 818, + "y": 80, + "width": 150, + "height": 72, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1501556190, + "versionNonce": 23134661, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1421514794", + "type": "text", + "x": 828, + "y": 88, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1926954514, + "versionNonce": 132686750, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "5 kill(4242, TERM)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "5 kill(4242, TERM)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1824182513", + "type": "text", + "x": 828, + "y": 108, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 625836062, + "versionNonce": 790705891, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "delivered to", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "delivered to", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t534405187", + "type": "text", + "x": 828, + "y": 123, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 736111141, + "versionNonce": 784012891, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the stranger", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the stranger", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1664865799", + "type": "rectangle", + "x": 40, + "y": 344, + "width": 200, + "height": 72, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1074104585, + "versionNonce": 1378023140, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1254475632", + "type": "text", + "x": 50, + "y": 352, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 828506984, + "versionNonce": 221617452, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1 spawn + pidfd_open", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1 spawn + pidfd_open", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t385269481", + "type": "text", + "x": 50, + "y": 372, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1305910845, + "versionNonce": 1509586315, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child 4242 -> pidfd 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child 4242 -> pidfd 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1470309836", + "type": "text", + "x": 50, + "y": 387, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 970417337, + "versionNonce": 797664380, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd refers to the process", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd refers to the process", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r741823953", + "type": "rectangle", + "x": 286, + "y": 344, + "width": 200, + "height": 72, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1789178552, + "versionNonce": 148826757, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1332251134", + "type": "text", + "x": 296, + "y": 352, + "width": 107.80000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1309433941, + "versionNonce": 1710238774, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "2 child exits", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "2 child exits", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t241068567", + "type": "text", + "x": 296, + "y": 372, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1593373098, + "versionNonce": 869813541, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pidfd 4 turns readable \u2014", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pidfd 4 turns readable \u2014", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t24103140", + "type": "text", + "x": 296, + "y": 387, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1730802889, + "versionNonce": 1707927025, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll/epoll sees POLLIN", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll/epoll sees POLLIN", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1429777043", + "type": "rectangle", + "x": 532, + "y": 344, + "width": 200, + "height": 72, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 203414794, + "versionNonce": 1824019574, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1883568214", + "type": "text", + "x": 542, + "y": 352, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 983138357, + "versionNonce": 1279319327, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "3 waitid(P_PIDFD, 4)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "3 waitid(P_PIDFD, 4)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1603677610", + "type": "text", + "x": 542, + "y": 372, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1730291168, + "versionNonce": 1042166651, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reaps exactly the process", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reaps exactly the process", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t778315349", + "type": "text", + "x": 542, + "y": 387, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1251065667, + "versionNonce": 815663200, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "behind the fd", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "behind the fd", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1770375680", + "type": "rectangle", + "x": 778, + "y": 344, + "width": 190, + "height": 72, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 229398141, + "versionNonce": 194019521, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1358652500", + "type": "text", + "x": 788, + "y": 352, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1514907271, + "versionNonce": 635460675, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "4 pidfd_send_signal(4)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "4 pidfd_send_signal(4)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t250323267", + "type": "text", + "x": 788, + "y": 372, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1544049349, + "versionNonce": 824167825, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "after exit: ESRCH \u2014", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "after exit: ESRCH \u2014", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t390191242", + "type": "text", + "x": 788, + "y": 387, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 851805063, + "versionNonce": 42212710, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "never a recycled pid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "never a recycled pid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1660209235", + "type": "arrow", + "x": 210, + "y": 116, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 860557901, + "versionNonce": 1335546568, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1668575037", + "type": "arrow", + "x": 402, + "y": 116, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 776726812, + "versionNonce": 473315315, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a831735275", + "type": "arrow", + "x": 604, + "y": 116, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 516343103, + "versionNonce": 93260412, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1678087175", + "type": "arrow", + "x": 796, + "y": 116, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1456453786, + "versionNonce": 624757448, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1403926754", + "type": "arrow", + "x": 893, + "y": 152, + "width": 182, + "height": 58, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 391133596, + "versionNonce": 298622092, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -182, + 58 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t234788357", + "type": "text", + "x": 802.0, + "y": 167.0, + "width": 187.55, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 554838990, + "versionNonce": 830278721, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "same integer, different process", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "same integer, different process", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1376762164", + "type": "arrow", + "x": 240, + "y": 380, + "width": 44, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 473877619, + "versionNonce": 830754977, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 44, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1981835062", + "type": "text", + "x": 262.0, + "y": 366.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 665254380, + "versionNonce": 908969276, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a605004044", + "type": "arrow", + "x": 486, + "y": 380, + "width": 44, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 194175350, + "versionNonce": 734276848, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 44, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t329191634", + "type": "text", + "x": 508.0, + "y": 366.0, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 914236171, + "versionNonce": 1920132580, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "readable", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "readable", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a78368167", + "type": "arrow", + "x": 732, + "y": 380, + "width": 44, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1758853321, + "versionNonce": 1011623690, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 44, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t272781043", + "type": "text", + "x": 40, + "y": 225, + "width": 677.6, + "height": 13.75, + "angle": 0, + "strokeColor": "#8a3b3b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1773962745, + "versionNonce": 176793325, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "The window 3->5 can be microseconds or hours; nothing the supervisor holds distinguishes old 4242 from new 4242.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "The window 3->5 can be microseconds or hours; nothing the supervisor holds distinguishes old 4242 from new 4242.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t794717789", + "type": "text", + "x": 40, + "y": 438, + "width": 592.9000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#3b6a3b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1699548758, + "versionNonce": 497224407, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Real run on this host: /proc//fdinfo/4 reads Pid: 2624390 \u2014 the fd itself names the child;", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Real run on this host: /proc//fdinfo/4 reads Pid: 2624390 \u2014 the fd itself names the child;", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1709657099", + "type": "text", + "x": 40, + "y": 454, + "width": 762.3000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#3b6a3b", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1040850036, + "versionNonce": 1542139072, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "strace: pidfd_open(2624926)=4, poll([pidfd,signalfd]) -> POLLIN, waitid(P_PIDFD,4)=CLD_KILLED, pidfd_send_signal(4,SIGTERM)=0.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "strace: pidfd_open(2624926)=4, poll([pidfd,signalfd]) -> POLLIN, waitid(P_PIDFD,4)=CLD_KILLED, pidfd_send_signal(4,SIGTERM)=0.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t10824905", + "type": "text", + "x": 20, + "y": 520, + "width": 858.0000000000001, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 308646964, + "versionNonce": 1363707558, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Steps 1-5 above are the classic pid-reuse race every kill(2)-based supervisor carries; the lower lane is pmon v2's default engine.", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Steps 1-5 above are the classic pid-reuse race every kill(2)-based supervisor carries; the lower lane is pmon v2's default engine.", + "lineHeight": 1.25, + "baseline": 12 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/13-pid-reuse-race-pidfd.svg b/assets/diagrams/13-pid-reuse-race-pidfd.svg new file mode 100644 index 0000000..645a6cc --- /dev/null +++ b/assets/diagrams/13-pid-reuse-race-pidfd.svg @@ -0,0 +1,61 @@ + + + +supervising by pid number — five steps to signalling a stranger + +supervising by pidfd — the fd pins the identity + +1 spawn +remember pid 4242 +(an integer, nothing more) + +2 child crashes +zombie: pid still +reserved, status pending + +3 reaped elsewhere +a wait() collects it — +pid 4242 is now free + +4 kernel recycles +pid 4242 = someone +else's new process + +5 kill(4242, TERM) +delivered to +the stranger + +1 spawn + pidfd_open +child 4242 -> pidfd 4 +fd refers to the process + +2 child exits +pidfd 4 turns readable — +poll/epoll sees POLLIN + +3 waitid(P_PIDFD, 4) +reaps exactly the process +behind the fd + +4 pidfd_send_signal(4) +after exit: ESRCH — +never a recycled pid + + + + + +same integer, different process +same integer, different process + +poll +poll + +readable +readable + +The window 3->5 can be microseconds or hours; nothing the supervisor holds distinguishes old 4242 from new 4242. +Real run on this host: /proc/<pmon>/fdinfo/4 reads Pid: 2624390 — the fd itself names the child; +strace: pidfd_open(2624926)=4, poll([pidfd,signalfd]) -> POLLIN, waitid(P_PIDFD,4)=CLD_KILLED, pidfd_send_signal(4,SIGTERM)=0. +Steps 1-5 above are the classic pid-reuse race every kill(2)-based supervisor carries; the lower lane is pmon v2's default engine. + \ No newline at end of file diff --git a/assets/diagrams/14-credential-sets-caps.excalidraw b/assets/diagrams/14-credential-sets-caps.excalidraw new file mode 100644 index 0000000..29fd6ce --- /dev/null +++ b/assets/diagrams/14-credential-sets-caps.excalidraw @@ -0,0 +1,2533 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1330114860", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 960, + "height": 150, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1351523619, + "versionNonce": 661404576, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1389534164", + "type": "text", + "x": 32, + "y": 26, + "width": 567.6, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1728333278, + "versionNonce": 1090489424, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "three uids, one state machine \u2014 setresuid(65534, 65534, 65534) moves all three at once", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "three uids, one state machine \u2014 setresuid(65534, 65534, 65534) moves all three at once", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1661154498", + "type": "rectangle", + "x": 20, + "y": 190, + "width": 960, + "height": 450, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 547248767, + "versionNonce": 1648596993, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1531166351", + "type": "text", + "x": 32, + "y": 196, + "width": 825.0000000000001, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 892897953, + "versionNonce": 542834585, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "capability sets across pmon drop --user nobody --keep-cap net_bind_service (values from /proc//status on systems-target)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "capability sets across pmon drop --user nobody --keep-cap net_bind_service (values from /proc//status on systems-target)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r386470249", + "type": "rectangle", + "x": 60, + "y": 60, + "width": 240, + "height": 78, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 711178881, + "versionNonce": 488280027, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1528545335", + "type": "text", + "x": 70, + "y": 68, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1369201434, + "versionNonce": 52424658, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "real uid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "real uid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1850151731", + "type": "text", + "x": 70, + "y": 88, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 970568764, + "versionNonce": 347638846, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "0 \u2192 65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "0 \u2192 65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1472868999", + "type": "text", + "x": 70, + "y": 103, + "width": 199.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 883887941, + "versionNonce": 1732823597, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "who you are (signals, accounting)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "who you are (signals, accounting)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1064625229", + "type": "rectangle", + "x": 380, + "y": 60, + "width": 240, + "height": 78, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1715373233, + "versionNonce": 1697500241, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t3660554", + "type": "text", + "x": 390, + "y": 68, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 531108681, + "versionNonce": 1967130484, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "effective uid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "effective uid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t236643751", + "type": "text", + "x": 390, + "y": 88, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 313376859, + "versionNonce": 257197020, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "0 \u2192 65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "0 \u2192 65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1727573190", + "type": "text", + "x": 390, + "y": 103, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 816887267, + "versionNonce": 1928441724, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the one permission checks read", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the one permission checks read", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r889319134", + "type": "rectangle", + "x": 700, + "y": 60, + "width": 240, + "height": 78, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 726182572, + "versionNonce": 435891799, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1044286183", + "type": "text", + "x": 710, + "y": 68, + "width": 69.30000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 681263174, + "versionNonce": 151193675, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "saved uid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "saved uid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t400246828", + "type": "text", + "x": 710, + "y": 88, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 494893335, + "versionNonce": 1210215816, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "0 \u2192 65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "0 \u2192 65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1298413591", + "type": "text", + "x": 710, + "y": 103, + "width": 205.70000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1115364629, + "versionNonce": 1450862894, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "the way back \u2014 gone after the drop", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "the way back \u2014 gone after the drop", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r292580514", + "type": "rectangle", + "x": 40, + "y": 240, + "width": 140, + "height": 96, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1840288901, + "versionNonce": 1192170595, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t453049202", + "type": "text", + "x": 50, + "y": 248, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1372524819, + "versionNonce": 1283386482, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon (root)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon (root)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1211958522", + "type": "text", + "x": 50, + "y": 268, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1760422605, + "versionNonce": 298449027, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uid r/e/s = 0/0/0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uid r/e/s = 0/0/0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1224620401", + "type": "text", + "x": 50, + "y": 283, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 357774306, + "versionNonce": 1491663761, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Prm=Eff=1ffffffffff", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Prm=Eff=1ffffffffff", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1100146459", + "type": "text", + "x": 50, + "y": 298, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1956009135, + "versionNonce": 509263278, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Amb=0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Amb=0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1206194806", + "type": "rectangle", + "x": 200, + "y": 240, + "width": 140, + "height": 96, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 713440759, + "versionNonce": 1046743690, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t600344745", + "type": "text", + "x": 210, + "y": 248, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 747743387, + "versionNonce": 1656249216, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "prctl", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "prctl", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1741355194", + "type": "text", + "x": 210, + "y": 268, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1221490067, + "versionNonce": 1205741292, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "PR_SET_KEEPCAPS", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "PR_SET_KEEPCAPS", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t380137602", + "type": "text", + "x": 210, + "y": 283, + "width": 60.50000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1213764979, + "versionNonce": 1217208810, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "before the", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "before the", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1130037340", + "type": "text", + "x": 210, + "y": 298, + "width": 60.50000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 522053634, + "versionNonce": 970536255, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uid change", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uid change", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r130325550", + "type": "rectangle", + "x": 360, + "y": 240, + "width": 140, + "height": 96, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 68385295, + "versionNonce": 1805955032, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t703647754", + "type": "text", + "x": 370, + "y": 248, + "width": 69.30000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 735613985, + "versionNonce": 119297947, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "setresuid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "setresuid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t198797696", + "type": "text", + "x": 370, + "y": 268, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1389099527, + "versionNonce": 1977548063, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "groups, gid, uid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "groups, gid, uid", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1453500106", + "type": "text", + "x": 370, + "y": 283, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1254588806, + "versionNonce": 1634158208, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\u2192 65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\u2192 65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t36833200", + "type": "text", + "x": 370, + "y": 298, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1173310135, + "versionNonce": 1746309969, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "permitted kept", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "permitted kept", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r105004403", + "type": "rectangle", + "x": 520, + "y": 240, + "width": 140, + "height": 96, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 799874472, + "versionNonce": 1353409069, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t289231652", + "type": "text", + "x": 530, + "y": 248, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 563813793, + "versionNonce": 294414292, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "capset", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "capset", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t21903442", + "type": "text", + "x": 530, + "y": 268, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1564597991, + "versionNonce": 1201991311, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Prm=Inh=Eff", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Prm=Inh=Eff", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1480698221", + "type": "text", + "x": 530, + "y": 283, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 741100536, + "versionNonce": 726434350, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "= 0x400", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "= 0x400", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t839298047", + "type": "text", + "x": 530, + "y": 298, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1020288202, + "versionNonce": 1655409967, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "all else dropped", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "all else dropped", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1636012090", + "type": "rectangle", + "x": 680, + "y": 240, + "width": 140, + "height": 96, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 176048233, + "versionNonce": 1860538229, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1075131857", + "type": "text", + "x": 690, + "y": 248, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1837656393, + "versionNonce": 784335287, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "prctl", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "prctl", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t635583590", + "type": "text", + "x": 690, + "y": 268, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 33946793, + "versionNonce": 748292731, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "AMBIENT_RAISE", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "AMBIENT_RAISE", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t792594576", + "type": "text", + "x": 690, + "y": 283, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1735372819, + "versionNonce": 1803560601, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Amb = 0x400", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Amb = 0x400", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t292561842", + "type": "text", + "x": 690, + "y": 298, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 26243345, + "versionNonce": 1439759964, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "needs Prm \u2229 Inh", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "needs Prm \u2229 Inh", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1024070276", + "type": "rectangle", + "x": 840, + "y": 240, + "width": 130, + "height": 96, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1333007020, + "versionNonce": 1341105802, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t447381360", + "type": "text", + "x": 850, + "y": 248, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1408265598, + "versionNonce": 1549716515, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "execve", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "execve", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1498302014", + "type": "text", + "x": 850, + "y": 268, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 540764204, + "versionNonce": 183962086, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "bindprobe", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bindprobe", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t847036901", + "type": "text", + "x": 850, + "y": 283, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1732986746, + "versionNonce": 20959450, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uid=65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uid=65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1796534924", + "type": "text", + "x": 850, + "y": 298, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1239544737, + "versionNonce": 274872288, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "bound :80 \u00b7 exit 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bound :80 \u00b7 exit 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1069966492", + "type": "rectangle", + "x": 200, + "y": 420, + "width": 140, + "height": 88, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 370233853, + "versionNonce": 140293918, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t830741165", + "type": "text", + "x": 210, + "y": 428, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1180962169, + "versionNonce": 562035743, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no --keep-cap", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no --keep-cap", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1585526300", + "type": "text", + "x": 210, + "y": 448, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 619139298, + "versionNonce": 502532698, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "steps 1, 3, 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "steps 1, 3, 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t534256565", + "type": "text", + "x": 210, + "y": 463, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1903510656, + "versionNonce": 1887126384, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "never happen", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "never happen", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1414723801", + "type": "rectangle", + "x": 360, + "y": 420, + "width": 140, + "height": 88, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1638657028, + "versionNonce": 385367764, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1478259345", + "type": "text", + "x": 370, + "y": 428, + "width": 69.30000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 452388598, + "versionNonce": 856959666, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "setresuid", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "setresuid", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t374024081", + "type": "text", + "x": 370, + "y": 448, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 111095231, + "versionNonce": 1135192849, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uid 0 \u2192 65534", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uid 0 \u2192 65534", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t796101706", + "type": "text", + "x": 370, + "y": 463, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 442329112, + "versionNonce": 1425404639, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "permitted", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "permitted", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t128273274", + "type": "text", + "x": 370, + "y": 478, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1390686728, + "versionNonce": 868549389, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "scrubbed to 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "scrubbed to 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r14435269", + "type": "rectangle", + "x": 680, + "y": 420, + "width": 290, + "height": 88, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 704435360, + "versionNonce": 66639038, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t494950242", + "type": "text", + "x": 690, + "y": 428, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1813097528, + "versionNonce": 891331452, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "execve \u2192 bindprobe", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "execve \u2192 bindprobe", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1433705904", + "type": "text", + "x": 690, + "y": 448, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 867957240, + "versionNonce": 333408618, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "every set 0000000000000000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "every set 0000000000000000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1512543987", + "type": "text", + "x": 690, + "y": 463, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1611750380, + "versionNonce": 1839417429, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "bind :80 \u2192 Permission denied \u00b7 exit 3", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bind :80 \u2192 Permission denied \u00b7 exit 3", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a883605702", + "type": "arrow", + "x": 180, + "y": 288, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 329045174, + "versionNonce": 1607398282, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a862528271", + "type": "arrow", + "x": 340, + "y": 288, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 539356783, + "versionNonce": 1324934719, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a959972818", + "type": "arrow", + "x": 500, + "y": 288, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 859894436, + "versionNonce": 1226554290, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a746420349", + "type": "arrow", + "x": 660, + "y": 288, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1723258217, + "versionNonce": 1934772941, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1094954030", + "type": "arrow", + "x": 820, + "y": 288, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 500905791, + "versionNonce": 578853010, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a612241853", + "type": "arrow", + "x": 110, + "y": 336, + "width": 90, + "height": 116, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 586477789, + "versionNonce": 599810747, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 90, + 116 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t657096969", + "type": "text", + "x": 155.0, + "y": 380.0, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 144899423, + "versionNonce": 1287064391, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "negative control", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "negative control", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1526240853", + "type": "arrow", + "x": 340, + "y": 464, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1580885743, + "versionNonce": 96643199, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1172289043", + "type": "arrow", + "x": 500, + "y": 464, + "width": 180, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 210115513, + "versionNonce": 1995292284, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 180, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t780824970", + "type": "text", + "x": 590.0, + "y": 450.0, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1001905411, + "versionNonce": 1093848027, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "nothing to raise", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "nothing to raise", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t347479618", + "type": "text", + "x": 60, + "y": 148, + "width": 828.85, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 801976097, + "versionNonce": 1877030134, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seteuid() moves only the middle one; the saved uid is what lets a set-uid binary flip back \u2014 pmon sets all three so there is no way back.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seteuid() moves only the middle one; the saved uid is what lets a set-uid binary flip back \u2014 pmon sets all three so there is no way back.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t429835207", + "type": "text", + "x": 40, + "y": 550, + "width": 683.6500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 849613052, + "versionNonce": 1343599474, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child status, kept-cap run: CapInh = CapPrm = CapEff = CapAmb = 0000000000000400 \u00b7 CapBnd = 000001ffffffffff", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child status, kept-cap run: CapInh = CapPrm = CapEff = CapAmb = 0000000000000400 \u00b7 CapBnd = 000001ffffffffff", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t376378759", + "type": "text", + "x": 40, + "y": 572, + "width": 816.7500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 639017567, + "versionNonce": 93962612, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "capsh --decode=0000000000000400 \u2192 cap_net_bind_service; the bounding set masks what execve may grant \u2014 it never grants anything itself.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "capsh --decode=0000000000000400 \u2192 cap_net_bind_service; the bounding set masks what execve may grant \u2014 it never grants anything itself.", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1419696448", + "type": "text", + "x": 40, + "y": 594, + "width": 605.0, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 596558414, + "versionNonce": 1604580527, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "negative control: getpcaps prints an empty set and every Cap line but CapBnd reads 0000000000000000.", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "negative control: getpcaps prints an empty set and every Cap line but CapBnd reads 0000000000000000.", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/14-credential-sets-caps.svg b/assets/diagrams/14-credential-sets-caps.svg new file mode 100644 index 0000000..f3950ed --- /dev/null +++ b/assets/diagrams/14-credential-sets-caps.svg @@ -0,0 +1,78 @@ + + + +three uids, one state machine — setresuid(65534, 65534, 65534) moves all three at once + +capability sets across pmon drop --user nobody --keep-cap net_bind_service (values from /proc/<pid>/status on systems-target) + +real uid +0 → 65534 +who you are (signals, accounting) + +effective uid +0 → 65534 +the one permission checks read + +saved uid +0 → 65534 +the way back — gone after the drop + +pmon (root) +uid r/e/s = 0/0/0 +Prm=Eff=1ffffffffff +Amb=0 + +prctl +PR_SET_KEEPCAPS +before the +uid change + +setresuid +groups, gid, uid +→ 65534 +permitted kept + +capset +Prm=Inh=Eff += 0x400 +all else dropped + +prctl +AMBIENT_RAISE +Amb = 0x400 +needs Prm ∩ Inh + +execve +bindprobe +uid=65534 +bound :80 · exit 0 + +no --keep-cap +steps 1, 3, 4 +never happen + +setresuid +uid 0 → 65534 +permitted +scrubbed to 0 + +execve → bindprobe +every set 0000000000000000 +bind :80 → Permission denied · exit 3 + + + + + + +negative control +negative control + + +nothing to raise +nothing to raise +seteuid() moves only the middle one; the saved uid is what lets a set-uid binary flip back — pmon sets all three so there is no way back. +child status, kept-cap run: CapInh = CapPrm = CapEff = CapAmb = 0000000000000400 · CapBnd = 000001ffffffffff +capsh --decode=0000000000000400 → cap_net_bind_service; the bounding set masks what execve may grant — it never grants anything itself. +negative control: getpcaps prints an empty set and every Cap line but CapBnd reads 0000000000000000. + \ No newline at end of file diff --git a/assets/diagrams/15-address-space-map.excalidraw b/assets/diagrams/15-address-space-map.excalidraw new file mode 100644 index 0000000..8a60d91 --- /dev/null +++ b/assets/diagrams/15-address-space-map.excalidraw @@ -0,0 +1,1499 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1124898250", + "type": "rectangle", + "x": 186, + "y": 184, + "width": 358, + "height": 178, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 858803506, + "versionNonce": 705469756, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1384785236", + "type": "text", + "x": 198, + "y": 190, + "width": 323.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 825968794, + "versionNonce": 1533977746, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mmap region \u2014 kernel hands out addresses top-down", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mmap region \u2014 kernel hands out addresses top-down", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1946302597", + "type": "rectangle", + "x": 200, + "y": 46, + "width": 330, + "height": 34, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1824580437, + "versionNonce": 1984332253, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t78035788", + "type": "text", + "x": 210, + "y": 54, + "width": 300.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 565353828, + "versionNonce": 1290602115, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "[vsyscall] --xp \u2014 legacy, kernel-owned", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[vsyscall] --xp \u2014 legacy, kernel-owned", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r996960779", + "type": "rectangle", + "x": 200, + "y": 90, + "width": 330, + "height": 46, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1714246398, + "versionNonce": 373206085, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t329569218", + "type": "text", + "x": 210, + "y": 98, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1910225678, + "versionNonce": 1174897975, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "[stack] rw-p", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[stack] rw-p", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t247441636", + "type": "text", + "x": 210, + "y": 118, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1769974075, + "versionNonce": 166910946, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "grows down; 8 MiB rlimit", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "grows down; 8 MiB rlimit", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1795953106", + "type": "rectangle", + "x": 200, + "y": 146, + "width": 330, + "height": 28, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 224625085, + "versionNonce": 1566664653, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t94026848", + "type": "text", + "x": 210, + "y": 154, + "width": 207.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1340771800, + "versionNonce": 120439515, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "unmapped \u2014 gigabytes of gap", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "unmapped \u2014 gigabytes of gap", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r659421144", + "type": "rectangle", + "x": 210, + "y": 210, + "width": 310, + "height": 34, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 655996394, + "versionNonce": 594882282, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t441645233", + "type": "text", + "x": 220, + "y": 218, + "width": 284.90000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1563878424, + "versionNonce": 1157469108, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "[vdso] [vvar] \u2014 kernel-provided pages", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[vdso] [vvar] \u2014 kernel-provided pages", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r398946233", + "type": "rectangle", + "x": 210, + "y": 252, + "width": 310, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 22852013, + "versionNonce": 925678318, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1324737123", + "type": "text", + "x": 220, + "y": 260, + "width": 284.90000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 455206949, + "versionNonce": 1665366807, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ld-linux, libc.so.6 \u2014 4 segments each", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ld-linux, libc.so.6 \u2014 4 segments each", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t248467718", + "type": "text", + "x": 220, + "y": 280, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 658041412, + "versionNonce": 1763512399, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "r-xp / r--p / r--p / rw-p", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "r-xp / r--p / r--p / rw-p", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r531292591", + "type": "rectangle", + "x": 210, + "y": 304, + "width": 310, + "height": 48, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1656934013, + "versionNonce": 809831577, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t38576650", + "type": "text", + "x": 220, + "y": 312, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1961614992, + "versionNonce": 1667296943, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "64 MiB anonymous rw-p", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "64 MiB anonymous rw-p", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1273556366", + "type": "text", + "x": 220, + "y": 332, + "width": 278.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 710248673, + "versionNonce": 33345596, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mmap(NULL, 67112960, ...) \u2014 the big allocation", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mmap(NULL, 67112960, ...) \u2014 the big allocation", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1382215055", + "type": "rectangle", + "x": 200, + "y": 396, + "width": 330, + "height": 28, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 845666598, + "versionNonce": 240666459, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1385113808", + "type": "text", + "x": 210, + "y": 404, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 510381175, + "versionNonce": 44637627, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "unmapped", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "unmapped", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r911436844", + "type": "rectangle", + "x": 200, + "y": 434, + "width": 330, + "height": 46, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1445012401, + "versionNonce": 1714611782, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t392134754", + "type": "text", + "x": 210, + "y": 442, + "width": 215.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1493778480, + "versionNonce": 22263134, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "[heap] rw-p \u2014 the brk arena", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[heap] rw-p \u2014 the brk arena", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1208303709", + "type": "text", + "x": 210, + "y": 462, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 179014225, + "versionNonce": 1726252090, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "204 KiB at startup; small allocations", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "204 KiB at startup; small allocations", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1727390614", + "type": "rectangle", + "x": 200, + "y": 490, + "width": 330, + "height": 40, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1086128913, + "versionNonce": 556082959, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t363648318", + "type": "text", + "x": 210, + "y": 498, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1373871856, + "versionNonce": 307808142, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "data + relro rw-p / r--p", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "data + relro rw-p / r--p", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t887957445", + "type": "text", + "x": 210, + "y": 518, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1013342728, + "versionNonce": 137157818, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "file-backed at offsets into the binary", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "file-backed at offsets into the binary", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r195414054", + "type": "rectangle", + "x": 200, + "y": 540, + "width": 330, + "height": 40, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1779130790, + "versionNonce": 41934599, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t705989351", + "type": "text", + "x": 210, + "y": 548, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1259550103, + "versionNonce": 17050918, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "text r-xp /usr/bin/app", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "text r-xp /usr/bin/app", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t769259983", + "type": "text", + "x": 210, + "y": 568, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 654817292, + "versionNonce": 1106739776, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "shared, read-only, executable", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "shared, read-only, executable", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1068792649", + "type": "arrow", + "x": 560, + "y": 100, + "width": 0, + "height": 32, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1348065149, + "versionNonce": 220467258, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1739485677", + "type": "text", + "x": 560.0, + "y": 102.0, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 776601698, + "versionNonce": 743217088, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "push/call", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "push/call", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1973694172", + "type": "arrow", + "x": 560, + "y": 250, + "width": 0, + "height": 90, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 543630469, + "versionNonce": 1556520603, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 90 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t14609383", + "type": "text", + "x": 560.0, + "y": 281.0, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1434515871, + "versionNonce": 690330725, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "new mmaps", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "new mmaps", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1070005475", + "type": "arrow", + "x": 560, + "y": 470, + "width": 0, + "height": 28, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1378025233, + "versionNonce": 815557945, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -28 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t185164869", + "type": "text", + "x": 560.0, + "y": 442.0, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 288832113, + "versionNonce": 1356351807, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "brk grows up", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "brk grows up", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1677523133", + "type": "text", + "x": 566, + "y": 30, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1373531656, + "versionNonce": 1038598321, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "high addresses", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "high addresses", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1926742308", + "type": "text", + "x": 566, + "y": 580, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 222476747, + "versionNonce": 1065569990, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "low addresses", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "low addresses", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t392961448", + "type": "text", + "x": 176, + "y": 58, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 328428848, + "versionNonce": 564751320, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ffffffffff600000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ffffffffff600000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t983439932", + "type": "text", + "x": 176, + "y": 108, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 994511046, + "versionNonce": 1937921888, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "7ffcda504000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "7ffcda504000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t885225419", + "type": "text", + "x": 176, + "y": 222, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1551651004, + "versionNonce": 37542251, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "7f7c68d73000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "7f7c68d73000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1592619190", + "type": "text", + "x": 176, + "y": 268, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 976067682, + "versionNonce": 1528950600, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "7f7c68b56000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "7f7c68b56000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t424050399", + "type": "text", + "x": 176, + "y": 320, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1238887477, + "versionNonce": 1593920513, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "7f7e52806000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "7f7e52806000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1117340642", + "type": "text", + "x": 176, + "y": 450, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1872033128, + "versionNonce": 413917204, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "55a5cdb86000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "55a5cdb86000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1095351584", + "type": "text", + "x": 176, + "y": 552, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 485599552, + "versionNonce": 766992787, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "55a5a49ff000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "55a5a49ff000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1583066964", + "type": "text", + "x": 20, + "y": 16, + "width": 389.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 367735941, + "versionNonce": 1740867495, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one x86-64 process, addresses from this chapter's real runs", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one x86-64 process, addresses from this chapter's real runs", + "lineHeight": 1.25, + "baseline": 12 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/15-address-space-map.svg b/assets/diagrams/15-address-space-map.svg new file mode 100644 index 0000000..094f0f4 --- /dev/null +++ b/assets/diagrams/15-address-space-map.svg @@ -0,0 +1,50 @@ + + + +mmap region — kernel hands out addresses top-down + +[vsyscall] --xp — legacy, kernel-owned + +[stack] rw-p +grows down; 8 MiB rlimit + +unmapped — gigabytes of gap + +[vdso] [vvar] — kernel-provided pages + +ld-linux, libc.so.6 — 4 segments each +r-xp / r--p / r--p / rw-p + +64 MiB anonymous rw-p +mmap(NULL, 67112960, ...) — the big allocation + +unmapped + +[heap] rw-p — the brk arena +204 KiB at startup; small allocations + +data + relro rw-p / r--p +file-backed at offsets into the binary + +text r-xp /usr/bin/app +shared, read-only, executable + +push/call +push/call + +new mmaps +new mmaps + +brk grows up +brk grows up +high addresses +low addresses +ffffffffff600000 +7ffcda504000 +7f7c68d73000 +7f7c68b56000 +7f7e52806000 +55a5cdb86000 +55a5a49ff000 +one x86-64 process, addresses from this chapter's real runs + \ No newline at end of file diff --git a/assets/diagrams/15-fault-flow.excalidraw b/assets/diagrams/15-fault-flow.excalidraw new file mode 100644 index 0000000..3f1294c --- /dev/null +++ b/assets/diagrams/15-fault-flow.excalidraw @@ -0,0 +1,1131 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r91450800", + "type": "rectangle", + "x": 30, + "y": 150, + "width": 180, + "height": 64, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1350259542, + "versionNonce": 444563676, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t994164998", + "type": "text", + "x": 40, + "y": 158, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 901862736, + "versionNonce": 1547726394, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "buf[i] = 1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "buf[i] = 1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1929950609", + "type": "text", + "x": 40, + "y": 178, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 437350248, + "versionNonce": 222437882, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "first touch of a page", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "first touch of a page", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1460168090", + "type": "rectangle", + "x": 262, + "y": 150, + "width": 180, + "height": 64, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 16781084, + "versionNonce": 784153489, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t76011706", + "type": "text", + "x": 272, + "y": 158, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1293101634, + "versionNonce": 1768859252, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "MMU: no PTE", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "MMU: no PTE", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t375499403", + "type": "text", + "x": 272, + "y": 178, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1997456194, + "versionNonce": 1624673479, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "CPU traps into the kernel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "CPU traps into the kernel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r122135445", + "type": "rectangle", + "x": 494, + "y": 150, + "width": 170, + "height": 64, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 769304262, + "versionNonce": 1388383907, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1043338024", + "type": "text", + "x": 504, + "y": 158, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 667456768, + "versionNonce": 1569108351, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "page-fault handler", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "page-fault handler", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t133858299", + "type": "text", + "x": 504, + "y": 178, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 972420976, + "versionNonce": 1382719935, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "look up the VMA", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "look up the VMA", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r101944641", + "type": "rectangle", + "x": 716, + "y": 24, + "width": 226, + "height": 54, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1033172000, + "versionNonce": 1124805880, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t504613553", + "type": "text", + "x": 726, + "y": 32, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1282844833, + "versionNonce": 1090579390, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no VMA / bad perms", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no VMA / bad perms", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1224377938", + "type": "text", + "x": 726, + "y": 52, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 406719110, + "versionNonce": 658183478, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIGSEGV", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIGSEGV", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1135793385", + "type": "rectangle", + "x": 716, + "y": 114, + "width": 226, + "height": 54, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1679919726, + "versionNonce": 1651320190, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1454248751", + "type": "text", + "x": 726, + "y": 122, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 54969957, + "versionNonce": 1777965997, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "anonymous, first write", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "anonymous, first write", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1447744649", + "type": "text", + "x": 726, + "y": 142, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1734345001, + "versionNonce": 1808285506, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "new zero-filled frame \u2014 minor", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "new zero-filled frame \u2014 minor", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1226304676", + "type": "rectangle", + "x": 716, + "y": 204, + "width": 226, + "height": 54, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 707671527, + "versionNonce": 1798431536, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t978695792", + "type": "text", + "x": 726, + "y": 212, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 865538920, + "versionNonce": 415544661, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "file page in page cache", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "file page in page cache", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1389394760", + "type": "text", + "x": 726, + "y": 232, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 459833617, + "versionNonce": 219149539, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "map it + fault-around \u2014 minor", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "map it + fault-around \u2014 minor", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r802743723", + "type": "rectangle", + "x": 716, + "y": 294, + "width": 226, + "height": 54, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1410178975, + "versionNonce": 1712821209, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1394843903", + "type": "text", + "x": 726, + "y": 302, + "width": 154.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 31870353, + "versionNonce": 528145639, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "file page not cached", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "file page not cached", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1336624804", + "type": "text", + "x": 726, + "y": 322, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 627558822, + "versionNonce": 1775595842, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read from disk, sleep \u2014 major", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read from disk, sleep \u2014 major", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a964941178", + "type": "arrow", + "x": 210, + "y": 182, + "width": 52, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 411578914, + "versionNonce": 264417892, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a604896770", + "type": "arrow", + "x": 442, + "y": 182, + "width": 52, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1252734626, + "versionNonce": 1266862626, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1501956377", + "type": "text", + "x": 468.0, + "y": 168.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 694060450, + "versionNonce": 1705314067, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "trap", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "trap", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1802520428", + "type": "arrow", + "x": 664, + "y": 166, + "width": 52, + "height": 115, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1203913654, + "versionNonce": 372906836, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + -115 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1815229612", + "type": "arrow", + "x": 664, + "y": 176, + "width": 52, + "height": 35, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 795838264, + "versionNonce": 1771125113, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + -35 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a151885908", + "type": "arrow", + "x": 664, + "y": 190, + "width": 52, + "height": 41, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 780971194, + "versionNonce": 599954879, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + 41 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1294335520", + "type": "arrow", + "x": 664, + "y": 200, + "width": 52, + "height": 121, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1260342142, + "versionNonce": 1762342100, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + 121 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t343841825", + "type": "text", + "x": 30, + "y": 286, + "width": 270.6, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1615340128, + "versionNonce": 1511427405, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "measured this chapter (getrusage deltas):", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "measured this chapter (getrusage deltas):", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "t256910424", + "type": "text", + "x": 30, + "y": 308, + "width": 423.50000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 452979141, + "versionNonce": 70538224, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "heap 64 MiB written: minor=16385 major=0 \u2014 one fault per touched page", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "heap 64 MiB written: minor=16385 major=0 \u2014 one fault per touched page", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t769704440", + "type": "text", + "x": 30, + "y": 326, + "width": 514.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1161389993, + "versionNonce": 1487988922, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fault-walk 16 MiB read: minor=32 per 512-page step \u2014 fault-around maps 16 pages/fault", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fault-walk 16 MiB read: minor=32 per 512-page step \u2014 fault-around maps 16 pages/fault", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t288268434", + "type": "text", + "x": 30, + "y": 344, + "width": 538.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 241067145, + "versionNonce": 365246798, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "major=0 everywhere: the walk file was written moments earlier, so it is in the page cache", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "major=0 everywhere: the walk file was written moments earlier, so it is in the page cache", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/15-fault-flow.svg b/assets/diagrams/15-fault-flow.svg new file mode 100644 index 0000000..8a1ef8f --- /dev/null +++ b/assets/diagrams/15-fault-flow.svg @@ -0,0 +1,36 @@ + + + +buf[i] = 1 +first touch of a page + +MMU: no PTE +CPU traps into the kernel + +page-fault handler +look up the VMA + +no VMA / bad perms +SIGSEGV + +anonymous, first write +new zero-filled frame — minor + +file page in page cache +map it + fault-around — minor + +file page not cached +read from disk, sleep — major + + +trap +trap + + + + +measured this chapter (getrusage deltas): +heap 64 MiB written: minor=16385 major=0 — one fault per touched page +fault-walk 16 MiB read: minor=32 per 512-page step — fault-around maps 16 pages/fault +major=0 everywhere: the walk file was written moments earlier, so it is in the page cache + \ No newline at end of file diff --git a/assets/diagrams/16-mapping-page-cache.excalidraw b/assets/diagrams/16-mapping-page-cache.excalidraw new file mode 100644 index 0000000..940df4b --- /dev/null +++ b/assets/diagrams/16-mapping-page-cache.excalidraw @@ -0,0 +1,1284 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r890767072", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 430, + "height": 150, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1455745595, + "versionNonce": 411970460, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t956515133", + "type": "text", + "x": 32, + "y": 26, + "width": 198.00000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1540908278, + "versionNonce": 984796454, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "process A \u2014 shmkv set (writer)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "process A \u2014 shmkv set (writer)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r373441718", + "type": "rectangle", + "x": 490, + "y": 20, + "width": 430, + "height": 150, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1401951765, + "versionNonce": 1179999346, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t188024485", + "type": "text", + "x": 502, + "y": 26, + "width": 198.00000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 308283013, + "versionNonce": 1889878028, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "process B \u2014 shmkv get (reader)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "process B \u2014 shmkv get (reader)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r893083277", + "type": "rectangle", + "x": 20, + "y": 210, + "width": 900, + "height": 130, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1758647773, + "versionNonce": 305784486, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t217546192", + "type": "text", + "x": 32, + "y": 216, + "width": 310.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1387186001, + "versionNonce": 346271167, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel \u2014 one page cache, shared by every mapper", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel \u2014 one page cache, shared by every mapper", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r813838339", + "type": "rectangle", + "x": 20, + "y": 380, + "width": 900, + "height": 100, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1311097465, + "versionNonce": 1957676786, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1588402228", + "type": "text", + "x": 32, + "y": 386, + "width": 40, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 596690859, + "versionNonce": 670069268, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "disk", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "disk", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r617440643", + "type": "rectangle", + "x": 50, + "y": 55, + "width": 230, + "height": 90, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 922720858, + "versionNonce": 271567343, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t313974299", + "type": "text", + "x": 60, + "y": 63, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 385265303, + "versionNonce": 383166263, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mmap MAP_SHARED", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mmap MAP_SHARED", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1748504053", + "type": "text", + "x": 60, + "y": 83, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 895534551, + "versionNonce": 1337865289, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "rw-s in /proc/self/maps", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rw-s in /proc/self/maps", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t841800335", + "type": "text", + "x": 60, + "y": 98, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 476183196, + "versionNonce": 291862678, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "store = plain memory write", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "store = plain memory write", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r246873115", + "type": "rectangle", + "x": 300, + "y": 55, + "width": 130, + "height": 90, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1235388049, + "versionNonce": 773197210, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t551912684", + "type": "text", + "x": 310, + "y": 63, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1780897370, + "versionNonce": 1766353388, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "MAP_PRIVATE", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "MAP_PRIVATE", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1328026029", + "type": "text", + "x": 310, + "y": 83, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 594888706, + "versionNonce": 1762314796, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "COW copy,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "COW copy,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1178392704", + "type": "text", + "x": 310, + "y": 98, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1341770141, + "versionNonce": 1629802822, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "never written back", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "never written back", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1531679939", + "type": "rectangle", + "x": 560, + "y": 55, + "width": 290, + "height": 90, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 250934113, + "versionNonce": 1658031868, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1218356613", + "type": "text", + "x": 570, + "y": 63, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 506635329, + "versionNonce": 823385955, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mmap MAP_SHARED", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mmap MAP_SHARED", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t884635310", + "type": "text", + "x": 570, + "y": 83, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1208264165, + "versionNonce": 754147827, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "rw-s, same inode, same offset", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rw-s, same inode, same offset", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1734897370", + "type": "text", + "x": 570, + "y": 98, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1905471029, + "versionNonce": 816950531, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "load = plain memory read", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "load = plain memory read", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r399181247", + "type": "rectangle", + "x": 310, + "y": 240, + "width": 330, + "height": 80, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1002692732, + "versionNonce": 1484686146, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1818706488", + "type": "text", + "x": 320, + "y": 248, + "width": 215.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 192170674, + "versionNonce": 1217273621, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "page-cache pages of store.kv", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "page-cache pages of store.kv", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t385465686", + "type": "text", + "x": 320, + "y": 268, + "width": 217.8, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1552436773, + "versionNonce": 1329995032, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1034 bytes, one copy for all mappers", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1034 bytes, one copy for all mappers", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1857139700", + "type": "rectangle", + "x": 335, + "y": 400, + "width": 280, + "height": 60, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1353640555, + "versionNonce": 379066231, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t876892419", + "type": "text", + "x": 345, + "y": 408, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1515847552, + "versionNonce": 1479294136, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "store.kv on disk", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "store.kv on disk", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1517701288", + "type": "text", + "x": 345, + "y": 428, + "width": 217.8, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 155036684, + "versionNonce": 1208593572, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "durable only after msync / writeback", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "durable only after msync / writeback", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a278827377", + "type": "arrow", + "x": 165, + "y": 145, + "width": 215, + "height": 95, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 334484322, + "versionNonce": 669001727, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 215, + 95 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1884321488", + "type": "text", + "x": 272.5, + "y": 178.5, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1385276444, + "versionNonce": 737753101, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "store", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "store", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a821859925", + "type": "arrow", + "x": 705, + "y": 145, + "width": 135, + "height": 95, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1563272539, + "versionNonce": 26340491, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -135, + 95 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1734417610", + "type": "text", + "x": 637.5, + "y": 178.5, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 270064088, + "versionNonce": 1913072312, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "load", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "load", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a612169669", + "type": "arrow", + "x": 445, + "y": 240, + "width": 65, + "height": 95, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 272969407, + "versionNonce": 1903400712, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -65, + -95 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t393564356", + "type": "text", + "x": 412.5, + "y": 178.5, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1799690768, + "versionNonce": 109130525, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "first write copies the page", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "first write copies the page", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a904187678", + "type": "arrow", + "x": 440, + "y": 320, + "width": 0, + "height": 80, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1905688358, + "versionNonce": 1145705813, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 80 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t986225767", + "type": "text", + "x": 440.0, + "y": 346.0, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1786997419, + "versionNonce": 277804208, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "msync(MS_SYNC)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "msync(MS_SYNC)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a694165994", + "type": "arrow", + "x": 520, + "y": 320, + "width": 0, + "height": 80, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1277290093, + "versionNonce": 360893883, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 80 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1898337629", + "type": "text", + "x": 520.0, + "y": 346.0, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1284975469, + "versionNonce": 1733310698, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "writeback, eventually", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "writeback, eventually", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1268138046", + "type": "text", + "x": 50, + "y": 350, + "width": 538.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 206889844, + "versionNonce": 988874664, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "A's store and B's load meet in the same physical pages \u2014 no copy, no syscall between them", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "A's store and B's load meet in the same physical pages \u2014 no copy, no syscall between them", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/16-mapping-page-cache.svg b/assets/diagrams/16-mapping-page-cache.svg new file mode 100644 index 0000000..7477330 --- /dev/null +++ b/assets/diagrams/16-mapping-page-cache.svg @@ -0,0 +1,45 @@ + + + +process A — shmkv set (writer) + +process B — shmkv get (reader) + +kernel — one page cache, shared by every mapper + +disk + +mmap MAP_SHARED +rw-s in /proc/self/maps +store = plain memory write + +MAP_PRIVATE +COW copy, +never written back + +mmap MAP_SHARED +rw-s, same inode, same offset +load = plain memory read + +page-cache pages of store.kv +1034 bytes, one copy for all mappers + +store.kv on disk +durable only after msync / writeback + +store +store + +load +load + +first write copies the page +first write copies the page + +msync(MS_SYNC) +msync(MS_SYNC) + +writeback, eventually +writeback, eventually +A's store and B's load meet in the same physical pages — no copy, no syscall between them + \ No newline at end of file diff --git a/assets/diagrams/16-shmkv-layout.excalidraw b/assets/diagrams/16-shmkv-layout.excalidraw new file mode 100644 index 0000000..f9c0812 --- /dev/null +++ b/assets/diagrams/16-shmkv-layout.excalidraw @@ -0,0 +1,1528 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1657382498", + "type": "rectangle", + "x": 20, + "y": 55, + "width": 900, + "height": 130, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1566093986, + "versionNonce": 778321724, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t111775654", + "type": "text", + "x": 32, + "y": 61, + "width": 389.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1865422544, + "versionNonce": 807396316, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "store.kv \u2014 1034 bytes = 10-byte header + 4 x 256-byte slots", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "store.kv \u2014 1034 bytes = 10-byte header + 4 x 256-byte slots", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1642377308", + "type": "rectangle", + "x": 190, + "y": 235, + "width": 560, + "height": 120, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 401246196, + "versionNonce": 475118328, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t41049828", + "type": "text", + "x": 202, + "y": 241, + "width": 125.4, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 464482428, + "versionNonce": 1708580347, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one slot, 256 bytes", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one slot, 256 bytes", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r448914963", + "type": "rectangle", + "x": 40, + "y": 90, + "width": 150, + "height": 75, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1041794732, + "versionNonce": 42536564, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t26241842", + "type": "text", + "x": 50, + "y": 98, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1879607609, + "versionNonce": 1297490845, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "magic \"SHKV1\\0\"", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "magic \"SHKV1\\0\"", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t799618450", + "type": "text", + "x": 50, + "y": 118, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 304253093, + "versionNonce": 46619105, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "6 B: 53 48 4b 56 31 00", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "6 B: 53 48 4b 56 31 00", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1305454341", + "type": "rectangle", + "x": 200, + "y": 90, + "width": 150, + "height": 75, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1846525747, + "versionNonce": 229202568, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1934706689", + "type": "text", + "x": 210, + "y": 98, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1944384474, + "versionNonce": 1380345880, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot_count", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot_count", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t892439843", + "type": "text", + "x": 210, + "y": 118, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1932134198, + "versionNonce": 962303727, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "u32 LE: 04 00 00 00", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "u32 LE: 04 00 00 00", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r600052779", + "type": "rectangle", + "x": 360, + "y": 90, + "width": 130, + "height": 75, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 713370813, + "versionNonce": 1574180710, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1067283557", + "type": "text", + "x": 370, + "y": 98, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 301519011, + "versionNonce": 1505609729, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 0", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 0", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t958742383", + "type": "text", + "x": 370, + "y": 118, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 942432592, + "versionNonce": 1292776741, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "256 B", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256 B", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1542907118", + "type": "rectangle", + "x": 500, + "y": 90, + "width": 130, + "height": 75, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 21843934, + "versionNonce": 207590472, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1768082550", + "type": "text", + "x": 510, + "y": 98, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1190748873, + "versionNonce": 1543849467, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1720705269", + "type": "text", + "x": 510, + "y": 118, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1490310589, + "versionNonce": 443119477, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "256 B", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256 B", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r325079420", + "type": "rectangle", + "x": 640, + "y": 90, + "width": 130, + "height": 75, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1256163768, + "versionNonce": 363492669, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1767913382", + "type": "text", + "x": 650, + "y": 98, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1485770868, + "versionNonce": 994192638, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 2", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 2", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t651830676", + "type": "text", + "x": 650, + "y": 118, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1465694081, + "versionNonce": 1976342613, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "256 B", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256 B", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r24927853", + "type": "rectangle", + "x": 780, + "y": 90, + "width": 120, + "height": 75, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 57515233, + "versionNonce": 476138352, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t438802418", + "type": "text", + "x": 790, + "y": 98, + "width": 46.2, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 673793791, + "versionNonce": 318190163, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t669826018", + "type": "text", + "x": 790, + "y": 118, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 759529275, + "versionNonce": 1461929516, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "256 B, empty", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256 B, empty", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r55397865", + "type": "rectangle", + "x": 215, + "y": 270, + "width": 230, + "height": 70, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1309720490, + "versionNonce": 1415475661, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1582634368", + "type": "text", + "x": 225, + "y": 278, + "width": 92.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1426739286, + "versionNonce": 98263257, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "key [0..64)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "key [0..64)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t494865651", + "type": "text", + "x": 225, + "y": 298, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1142902497, + "versionNonce": 1091682689, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "NUL-padded, max 63 B", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "NUL-padded, max 63 B", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1886981135", + "type": "text", + "x": 225, + "y": 313, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1432956707, + "versionNonce": 1850221722, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "key[0] == 0 means empty", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "key[0] == 0 means empty", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r793958643", + "type": "rectangle", + "x": 465, + "y": 270, + "width": 260, + "height": 70, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1768856584, + "versionNonce": 787673086, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1966256124", + "type": "text", + "x": 475, + "y": 278, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1094246049, + "versionNonce": 531127578, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "value [64..256)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "value [64..256)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1410882456", + "type": "text", + "x": 475, + "y": 298, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 738458636, + "versionNonce": 340162817, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "NUL-padded, max 191 B", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "NUL-padded, max 191 B", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1556217916", + "type": "rectangle", + "x": 40, + "y": 400, + "width": 250, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 755779385, + "versionNonce": 1475230885, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t429490790", + "type": "text", + "x": 50, + "y": 408, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 661770797, + "versionNonce": 24463601, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "C++ writes it", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "C++ writes it", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1569047156", + "type": "text", + "x": 50, + "y": 428, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 681352444, + "versionNonce": 622320632, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "memcpy into the mapping", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "memcpy into the mapping", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1514455982", + "type": "rectangle", + "x": 345, + "y": 400, + "width": 250, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 922858295, + "versionNonce": 228424910, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t979536049", + "type": "text", + "x": 355, + "y": 408, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1273926858, + "versionNonce": 1993633281, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Go reads it", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Go reads it", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t76167221", + "type": "text", + "x": 355, + "y": 428, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1282892903, + "versionNonce": 187416994, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "[]byte over the same pages", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "[]byte over the same pages", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1649565151", + "type": "rectangle", + "x": 650, + "y": 400, + "width": 250, + "height": 55, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 206400297, + "versionNonce": 1511754268, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1415478232", + "type": "text", + "x": 660, + "y": 408, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 97669463, + "versionNonce": 384518743, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Rust reads it", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Rust reads it", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t593598713", + "type": "text", + "x": 660, + "y": 428, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1508616168, + "versionNonce": 1514205631, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "&map[off..off+256]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "&map[off..off+256]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1992717996", + "type": "arrow", + "x": 525, + "y": 165, + "width": 55, + "height": 70, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1916836990, + "versionNonce": 477895464, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -55, + 70 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t347970324", + "type": "text", + "x": 497.5, + "y": 186.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 298911375, + "versionNonce": 959116673, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "zoom", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "zoom", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a548433781", + "type": "arrow", + "x": 165, + "y": 400, + "width": 135, + "height": 42, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 478457160, + "versionNonce": 576304480, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 135, + -42 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a81523570", + "type": "arrow", + "x": 470, + "y": 400, + "width": 0, + "height": 42, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 969846888, + "versionNonce": 1529425611, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -42 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1345319668", + "type": "arrow", + "x": 775, + "y": 400, + "width": 125, + "height": 42, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 857223238, + "versionNonce": 994030001, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -125, + -42 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t118274910", + "type": "text", + "x": 40, + "y": 35, + "width": 756.2500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 385792489, + "versionNonce": 828959048, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Every field is bytes with a stated width and order (u32 little-endian) \u2014 no language repr, padding, or endianness can leak in", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Every field is bytes with a stated width and order (u32 little-endian) \u2014 no language repr, padding, or endianness can leak in", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t77511169", + "type": "text", + "x": 40, + "y": 375, + "width": 611.0500000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 480004449, + "versionNonce": 520973884, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cmp(1) proves it: a store built by the Rust binary and one built by the C++ binary are byte-identical", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cmp(1) proves it: a store built by the Rust binary and one built by the C++ binary are byte-identical", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/16-shmkv-layout.svg b/assets/diagrams/16-shmkv-layout.svg new file mode 100644 index 0000000..1a07167 --- /dev/null +++ b/assets/diagrams/16-shmkv-layout.svg @@ -0,0 +1,49 @@ + + + +store.kv — 1034 bytes = 10-byte header + 4 x 256-byte slots + +one slot, 256 bytes + +magic "SHKV1\0" +6 B: 53 48 4b 56 31 00 + +slot_count +u32 LE: 04 00 00 00 + +slot 0 +256 B + +slot 1 +256 B + +slot 2 +256 B + +slot 3 +256 B, empty + +key [0..64) +NUL-padded, max 63 B +key[0] == 0 means empty + +value [64..256) +NUL-padded, max 191 B + +C++ writes it +memcpy into the mapping + +Go reads it +[]byte over the same pages + +Rust reads it +&map[off..off+256] + +zoom +zoom + + + +Every field is bytes with a stated width and order (u32 little-endian) — no language repr, padding, or endianness can leak in +cmp(1) proves it: a store built by the Rust binary and one built by the C++ binary are byte-identical + \ No newline at end of file diff --git a/assets/diagrams/17-arena-vs-gc.excalidraw b/assets/diagrams/17-arena-vs-gc.excalidraw new file mode 100644 index 0000000..2070391 --- /dev/null +++ b/assets/diagrams/17-arena-vs-gc.excalidraw @@ -0,0 +1,2114 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1544485872", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 296, + "height": 390, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 390434465, + "versionNonce": 1491778214, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t907605102", + "type": "text", + "x": 32, + "y": 26, + "width": 323.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1541197642, + "versionNonce": 1123902092, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "free one by one \u2014 glibc malloc / Rust GlobalAlloc", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "free one by one \u2014 glibc malloc / Rust GlobalAlloc", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1341588355", + "type": "rectangle", + "x": 332, + "y": 20, + "width": 296, + "height": 390, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1151514298, + "versionNonce": 710796221, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1578659726", + "type": "text", + "x": 344, + "y": 26, + "width": 250.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 106136804, + "versionNonce": 1804520691, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "arena / pool \u2014 pmr, bumpalo, sync.Pool", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "arena / pool \u2014 pmr, bumpalo, sync.Pool", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r944997165", + "type": "rectangle", + "x": 644, + "y": 20, + "width": 296, + "height": 390, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1076119688, + "versionNonce": 743429830, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1561178390", + "type": "text", + "x": 656, + "y": 26, + "width": 158.4, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 639569570, + "versionNonce": 549971243, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tracing GC \u2014 the Go heap", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tracing GC \u2014 the Go heap", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1444451091", + "type": "rectangle", + "x": 48, + "y": 58, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1803825566, + "versionNonce": 1847762259, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t632071268", + "type": "text", + "x": 58, + "y": 66, + "width": 207.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 502695434, + "versionNonce": 475502784, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "200,000 short-lived strings", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "200,000 short-lived strings", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t882716393", + "type": "text", + "x": 58, + "y": 86, + "width": 193.60000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1965491263, + "versionNonce": 1743438073, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "key / frag / value per iteration", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "key / frag / value per iteration", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1204226460", + "type": "rectangle", + "x": 48, + "y": 150, + "width": 240, + "height": 66, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 639109369, + "versionNonce": 1518662164, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1738720481", + "type": "text", + "x": 58, + "y": 158, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1891846709, + "versionNonce": 1991961135, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "main arena + tcache", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "main arena + tcache", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1738556309", + "type": "text", + "x": 58, + "y": 178, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1186083497, + "versionNonce": 485485665, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "64 cached chunks/size class,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "64 cached chunks/size class,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1135546272", + "type": "text", + "x": 58, + "y": 193, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1578334247, + "versionNonce": 1839564980, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "then small/large bins", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "then small/large bins", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1826344532", + "type": "rectangle", + "x": 48, + "y": 256, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1708217430, + "versionNonce": 460087397, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1123778948", + "type": "text", + "x": 58, + "y": 264, + "width": 207.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1624673432, + "versionNonce": 1114574967, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pages stay with the process", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pages stay with the process", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t267957661", + "type": "text", + "x": 58, + "y": 284, + "width": 205.70000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 4100582, + "versionNonce": 779736550, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mallinfo2: arena 6340KB after free", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mallinfo2: arena 6340KB after free", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r750578177", + "type": "rectangle", + "x": 48, + "y": 340, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 422767152, + "versionNonce": 1250575847, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1459387761", + "type": "text", + "x": 58, + "y": 348, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1014698368, + "versionNonce": 1553458201, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "C++ 3908KB / Rust 2548KB", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "C++ 3908KB / Rust 2548KB", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t781407961", + "type": "text", + "x": 58, + "y": 368, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 785084946, + "versionNonce": 1696926254, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one free() per object", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one free() per object", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1531637141", + "type": "rectangle", + "x": 360, + "y": 58, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1861365993, + "versionNonce": 1524746494, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1606364974", + "type": "text", + "x": 370, + "y": 66, + "width": 200.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 492356944, + "versionNonce": 1416769583, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "same 200,000 intermediates", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "same 200,000 intermediates", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t170466089", + "type": "text", + "x": 370, + "y": 86, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 480198292, + "versionNonce": 1169851068, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "batch of 1,000 at a time", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "batch of 1,000 at a time", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r393196623", + "type": "rectangle", + "x": 360, + "y": 150, + "width": 240, + "height": 66, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1428849595, + "versionNonce": 413426562, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1453954681", + "type": "text", + "x": 370, + "y": 158, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1327774026, + "versionNonce": 1061876710, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one reusable slab", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one reusable slab", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t178224147", + "type": "text", + "x": 370, + "y": 178, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 814492623, + "versionNonce": 569484713, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "256KiB, bump pointer;", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "256KiB, bump pointer;", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1007899197", + "type": "text", + "x": 370, + "y": 193, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1749040395, + "versionNonce": 1043471531, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "survivors copied out", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "survivors copied out", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1996721169", + "type": "rectangle", + "x": 360, + "y": 256, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 977675689, + "versionNonce": 1104815990, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1477786474", + "type": "text", + "x": 370, + "y": 264, + "width": 231.00000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1001553249, + "versionNonce": 1564690583, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reset() / ~resource / Pool.Put", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reset() / ~resource / Pool.Put", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t669963898", + "type": "text", + "x": 370, + "y": 284, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1516045810, + "versionNonce": 1218310892, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "whole batch reclaimed in O(1)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "whole batch reclaimed in O(1)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1325945910", + "type": "rectangle", + "x": 360, + "y": 340, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1529090341, + "versionNonce": 1034448751, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1266044281", + "type": "text", + "x": 370, + "y": 348, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 554100183, + "versionNonce": 962915868, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "C++ 4240KB / Rust 2544KB", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "C++ 4240KB / Rust 2544KB", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1851534677", + "type": "text", + "x": 370, + "y": 368, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1777015735, + "versionNonce": 1931754718, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "zero per-object frees", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "zero per-object frees", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r361053552", + "type": "rectangle", + "x": 672, + "y": 58, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 873893055, + "versionNonce": 1396155506, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t886308323", + "type": "text", + "x": 682, + "y": 66, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1871777572, + "versionNonce": 210930946, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "same churn, now garbage", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "same churn, now garbage", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1204318269", + "type": "text", + "x": 682, + "y": 86, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1810130488, + "versionNonce": 1368469409, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "collector must find it", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "collector must find it", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1033346718", + "type": "rectangle", + "x": 672, + "y": 150, + "width": 240, + "height": 66, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 570185729, + "versionNonce": 761644482, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1873531508", + "type": "text", + "x": 682, + "y": 158, + "width": 184.8, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 431125213, + "versionNonce": 1746681588, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pacer: GOGC / GOMEMLIMIT", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pacer: GOGC / GOMEMLIMIT", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1451470456", + "type": "text", + "x": 682, + "y": 178, + "width": 175.45000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 422164263, + "versionNonce": 1911036256, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "trigger when heap grows GOGC%", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "trigger when heap grows GOGC%", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t205549112", + "type": "text", + "x": 682, + "y": 193, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1934827189, + "versionNonce": 7942661, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "or nears the soft limit", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "or nears the soft limit", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1587551098", + "type": "rectangle", + "x": 672, + "y": 256, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 231692550, + "versionNonce": 1727134335, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1490177256", + "type": "text", + "x": 682, + "y": 264, + "width": 177.10000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1572784370, + "versionNonce": 154094809, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "concurrent mark + sweep", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "concurrent mark + sweep", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1376133754", + "type": "text", + "x": 682, + "y": 284, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1759672997, + "versionNonce": 1833960559, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "gc_cycles: 9 (default)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "gc_cycles: 9 (default)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r106832487", + "type": "rectangle", + "x": 672, + "y": 340, + "width": 240, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 926790328, + "versionNonce": 72673762, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1452424152", + "type": "text", + "x": 682, + "y": 348, + "width": 130.9, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 698305573, + "versionNonce": 232039922, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "8064KB @ 9 cycles", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "8064KB @ 9 cycles", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1478533092", + "type": "text", + "x": 682, + "y": 368, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 438827750, + "versionNonce": 303832614, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "GOMEMLIMIT=8MiB: 5676KB @ 47", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "GOMEMLIMIT=8MiB: 5676KB @ 47", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1079136911", + "type": "arrow", + "x": 168, + "y": 110, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1261747489, + "versionNonce": 467431266, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t141957449", + "type": "text", + "x": 168.0, + "y": 116.0, + "width": 60.50000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 875902295, + "versionNonce": 1846145981, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "malloc x N", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "malloc x N", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1259769891", + "type": "arrow", + "x": 168, + "y": 216, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1452859477, + "versionNonce": 734589248, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1114426770", + "type": "text", + "x": 168.0, + "y": 222.0, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 184256813, + "versionNonce": 95122870, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "free x N", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "free x N", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a7632529", + "type": "arrow", + "x": 168, + "y": 308, + "width": 0, + "height": 32, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 560732002, + "versionNonce": 757902891, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a555941827", + "type": "arrow", + "x": 480, + "y": 110, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1867954472, + "versionNonce": 647526901, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1695808989", + "type": "text", + "x": 480.0, + "y": 116.0, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1695348682, + "versionNonce": 1910672170, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "bump += size", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bump += size", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1265359382", + "type": "arrow", + "x": 480, + "y": 216, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 362530606, + "versionNonce": 1313708822, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1458556830", + "type": "text", + "x": 480.0, + "y": 222.0, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 991104701, + "versionNonce": 1805259603, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1 release / batch", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1 release / batch", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a572966120", + "type": "arrow", + "x": 480, + "y": 308, + "width": 0, + "height": 32, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 95446019, + "versionNonce": 429838007, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1041981842", + "type": "arrow", + "x": 792, + "y": 110, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1864461731, + "versionNonce": 1453526260, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t862716058", + "type": "text", + "x": 792.0, + "y": 116.0, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 847060114, + "versionNonce": 1626659896, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "allocate; drop refs", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "allocate; drop refs", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1307673843", + "type": "arrow", + "x": 792, + "y": 216, + "width": 0, + "height": 40, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 508825533, + "versionNonce": 1548613044, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 40 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1846634913", + "type": "text", + "x": 792.0, + "y": 222.0, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1498870217, + "versionNonce": 1036440090, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cycle fires", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cycle fires", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a988648862", + "type": "arrow", + "x": 792, + "y": 308, + "width": 0, + "height": 32, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 671385891, + "versionNonce": 1773553065, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 32 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1432285323", + "type": "text", + "x": 480, + "y": 430, + "width": 605.0, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1030537759, + "versionNonce": 958480272, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "all numbers: allocbench --allocs 200000 on the reference host \u2014 peak_rss is getrusage ru_maxrss (KB)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "all numbers: allocbench --allocs 200000 on the reference host \u2014 peak_rss is getrusage ru_maxrss (KB)", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/17-arena-vs-gc.svg b/assets/diagrams/17-arena-vs-gc.svg new file mode 100644 index 0000000..476a55a --- /dev/null +++ b/assets/diagrams/17-arena-vs-gc.svg @@ -0,0 +1,70 @@ + + + +free one by one — glibc malloc / Rust GlobalAlloc + +arena / pool — pmr, bumpalo, sync.Pool + +tracing GC — the Go heap + +200,000 short-lived strings +key / frag / value per iteration + +main arena + tcache +64 cached chunks/size class, +then small/large bins + +pages stay with the process +mallinfo2: arena 6340KB after free + +C++ 3908KB / Rust 2548KB +one free() per object + +same 200,000 intermediates +batch of 1,000 at a time + +one reusable slab +256KiB, bump pointer; +survivors copied out + +reset() / ~resource / Pool.Put +whole batch reclaimed in O(1) + +C++ 4240KB / Rust 2544KB +zero per-object frees + +same churn, now garbage +collector must find it + +pacer: GOGC / GOMEMLIMIT +trigger when heap grows GOGC% +or nears the soft limit + +concurrent mark + sweep +gc_cycles: 9 (default) + +8064KB @ 9 cycles +GOMEMLIMIT=8MiB: 5676KB @ 47 + +malloc x N +malloc x N + +free x N +free x N + + +bump += size +bump += size + +1 release / batch +1 release / batch + + +allocate; drop refs +allocate; drop refs + +cycle fires +cycle fires + +all numbers: allocbench --allocs 200000 on the reference host — peak_rss is getrusage ru_maxrss (KB) + \ No newline at end of file diff --git a/assets/diagrams/18-log-topology.excalidraw b/assets/diagrams/18-log-topology.excalidraw new file mode 100644 index 0000000..6df7a70 --- /dev/null +++ b/assets/diagrams/18-log-topology.excalidraw @@ -0,0 +1,2205 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r458077152", + "type": "rectangle", + "x": 20, + "y": 40, + "width": 920, + "height": 185, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1108738861, + "versionNonce": 1684417545, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1287208160", + "type": "text", + "x": 32, + "y": 46, + "width": 250.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1778773428, + "versionNonce": 957874884, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon supervise (pid 2633754) \u2014 capture", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon supervise (pid 2633754) \u2014 capture", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1775920623", + "type": "rectangle", + "x": 20, + "y": 250, + "width": 920, + "height": 150, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 850087404, + "versionNonce": 311782682, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t540877146", + "type": "text", + "x": 32, + "y": 256, + "width": 204.60000000000002, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 26732644, + "versionNonce": 1595502727, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon tail (pid 2634759) \u2014 relay", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon tail (pid 2634759) \u2014 relay", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1780140679", + "type": "rectangle", + "x": 20, + "y": 425, + "width": 920, + "height": 150, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 161427379, + "versionNonce": 230051985, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t668823488", + "type": "text", + "x": 32, + "y": 431, + "width": 132.0, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 465123551, + "versionNonce": 1947459553, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel FIFO + reader", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel FIFO + reader", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r288203013", + "type": "rectangle", + "x": 40, + "y": 90, + "width": 175, + "height": 105, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1980723432, + "versionNonce": 834057187, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1944218700", + "type": "text", + "x": 50, + "y": 98, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 215382546, + "versionNonce": 603788256, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "sh (child 2633756)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "sh (child 2633756)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t43154621", + "type": "text", + "x": 50, + "y": 118, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 307705054, + "versionNonce": 1603888684, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd 1 \u2192 pipe:[16462630]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd 1 \u2192 pipe:[16462630]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t786787241", + "type": "text", + "x": 50, + "y": 133, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 26464219, + "versionNonce": 1347887615, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd 2 \u2192 pipe:[16462631]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd 2 \u2192 pipe:[16462631]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t916019881", + "type": "text", + "x": 50, + "y": 148, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1853427090, + "versionNonce": 1993056715, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "mask reset at spawn", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "mask reset at spawn", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1638629895", + "type": "rectangle", + "x": 255, + "y": 75, + "width": 165, + "height": 55, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1751190039, + "versionNonce": 1692166665, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1392808914", + "type": "text", + "x": 265, + "y": 83, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1781870536, + "versionNonce": 161308597, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stdout pipe", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stdout pipe", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1103140129", + "type": "text", + "x": 265, + "y": 103, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 843027711, + "versionNonce": 1242408361, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "inode 16462630", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "inode 16462630", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1257638041", + "type": "rectangle", + "x": 255, + "y": 145, + "width": 165, + "height": 55, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1000276188, + "versionNonce": 1414380910, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t94265402", + "type": "text", + "x": 265, + "y": 153, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1989666508, + "versionNonce": 1539398867, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stderr pipe", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stderr pipe", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t199668212", + "type": "text", + "x": 265, + "y": 173, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1782673484, + "versionNonce": 1180605375, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "inode 16462631", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "inode 16462631", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1809717149", + "type": "rectangle", + "x": 465, + "y": 85, + "width": 215, + "height": 110, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 601821733, + "versionNonce": 643594336, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t946925195", + "type": "text", + "x": 475, + "y": 93, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 534807331, + "versionNonce": 245068912, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one poll(2) loop", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one poll(2) loop", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1529283392", + "type": "text", + "x": 475, + "y": 113, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 424203686, + "versionNonce": 961801978, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "signalfd 4 \u00b7 pidfd 6", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "signalfd 4 \u00b7 pidfd 6", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t391646831", + "type": "text", + "x": 475, + "y": 128, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1807623268, + "versionNonce": 1677483428, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe read ends 5 and 7", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe read ends 5 and 7", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1639891503", + "type": "text", + "x": 475, + "y": 143, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 293461408, + "versionNonce": 1319136585, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "LineRelay tags each line", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "LineRelay tags each line", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1989318051", + "type": "rectangle", + "x": 725, + "y": 95, + "width": 190, + "height": 90, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1204060678, + "versionNonce": 798461791, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1157436494", + "type": "text", + "x": 735, + "y": 103, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 700342026, + "versionNonce": 140533344, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "live.log \u2014 fd 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "live.log \u2014 fd 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t59625999", + "type": "text", + "x": 735, + "y": 123, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1161413687, + "versionNonce": 1797918968, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "O_WRONLY|O_APPEND", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "O_WRONLY|O_APPEND", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1867242119", + "type": "text", + "x": 735, + "y": 138, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 698297716, + "versionNonce": 333397699, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "\"[out] \u2026\" / \"[err] \u2026\"", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "\"[out] \u2026\" / \"[err] \u2026\"", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r779030387", + "type": "rectangle", + "x": 40, + "y": 295, + "width": 200, + "height": 85, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 412301882, + "versionNonce": 1518565470, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t21903406", + "type": "text", + "x": 50, + "y": 303, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 645328532, + "versionNonce": 779495048, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "live.log \u2014 fd 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "live.log \u2014 fd 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t504787443", + "type": "text", + "x": 50, + "y": 323, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 235147861, + "versionNonce": 404453317, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "O_RDONLY, fdinfo pos 10", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "O_RDONLY, fdinfo pos 10", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t934905244", + "type": "text", + "x": 50, + "y": 338, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1439432540, + "versionNonce": 1761156607, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "offset advances only", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "offset advances only", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1948672332", + "type": "text", + "x": 50, + "y": 353, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 799301062, + "versionNonce": 176858857, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "for delivered bytes", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "for delivered bytes", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r710565293", + "type": "rectangle", + "x": 465, + "y": 295, + "width": 215, + "height": 85, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 760352680, + "versionNonce": 1348076990, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1420691776", + "type": "text", + "x": 475, + "y": 303, + "width": 138.60000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1724116216, + "versionNonce": 1817016709, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "FIFO writer \u2014 fd 4", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "FIFO writer \u2014 fd 4", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1157869643", + "type": "text", + "x": 475, + "y": 323, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 603245601, + "versionNonce": 213869011, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fdinfo flags 02100001", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fdinfo flags 02100001", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1020362220", + "type": "text", + "x": 475, + "y": 338, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 544592431, + "versionNonce": 751933600, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "O_NONBLOCK cleared", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "O_NONBLOCK cleared", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1460321405", + "type": "text", + "x": 475, + "y": 353, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 320289369, + "versionNonce": 1523096681, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "after ENXIO wait loop", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "after ENXIO wait loop", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r158447892", + "type": "rectangle", + "x": 465, + "y": 470, + "width": 215, + "height": 65, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 84821498, + "versionNonce": 481844007, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t641330794", + "type": "text", + "x": 475, + "y": 478, + "width": 69.30000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 795178078, + "versionNonce": 1529146481, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "live.fifo", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "live.fifo", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1764250770", + "type": "text", + "x": 475, + "y": 498, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1467683358, + "versionNonce": 1448442539, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "S_IFIFO, mode 0644", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "S_IFIFO, mode 0644", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1190706649", + "type": "rectangle", + "x": 735, + "y": 465, + "width": 180, + "height": 80, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1176001253, + "versionNonce": 205472017, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1893431733", + "type": "text", + "x": 745, + "y": 473, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 148511089, + "versionNonce": 1555457976, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cat \u2014 fd 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cat \u2014 fd 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1206409933", + "type": "text", + "x": 745, + "y": 493, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1928183806, + "versionNonce": 1967655012, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "detach \u2192 EPIPE upstream", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "detach \u2192 EPIPE upstream", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1785737719", + "type": "text", + "x": 745, + "y": 508, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 366934128, + "versionNonce": 1556743636, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "next reader loses nothing", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "next reader loses nothing", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a221705674", + "type": "arrow", + "x": 215, + "y": 115, + "width": 38, + "height": 12, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1129745273, + "versionNonce": 1367328207, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 38, + -12 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1274763319", + "type": "text", + "x": 234.0, + "y": 95.0, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 668418879, + "versionNonce": 536970078, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "write(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "write(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a347180902", + "type": "arrow", + "x": 215, + "y": 165, + "width": 38, + "height": 7, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1208183701, + "versionNonce": 1075304718, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 38, + 7 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t450763793", + "type": "text", + "x": 234.0, + "y": 154.5, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 865012917, + "versionNonce": 734902731, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "write(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "write(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1843115581", + "type": "arrow", + "x": 420, + "y": 103, + "width": 43, + "height": 12, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1353595863, + "versionNonce": 1577521508, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 43, + 12 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1490445533", + "type": "text", + "x": 441.5, + "y": 95.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 919873529, + "versionNonce": 1433910478, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "POLLIN", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "POLLIN", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1463270604", + "type": "arrow", + "x": 420, + "y": 172, + "width": 43, + "height": 12, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1624102437, + "versionNonce": 1333716683, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 43, + -12 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t41040115", + "type": "text", + "x": 441.5, + "y": 152.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1815769588, + "versionNonce": 439400545, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "POLLIN", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "POLLIN", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a816995806", + "type": "arrow", + "x": 680, + "y": 140, + "width": 43, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 377585042, + "versionNonce": 332435291, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 43, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t471015068", + "type": "text", + "x": 701.5, + "y": 126.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1445736338, + "versionNonce": 1880607241, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "append", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "append", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a444192340", + "type": "arrow", + "x": 820, + "y": 185, + "width": 680, + "height": 108, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 721194310, + "versionNonce": 1754925251, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -680, + 108 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1354604596", + "type": "text", + "x": 480.0, + "y": 225.0, + "width": 248.05, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 65293188, + "versionNonce": 1859702240, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tail follows the same file, from offset 0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tail follows the same file, from offset 0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1574884534", + "type": "arrow", + "x": 240, + "y": 337, + "width": 223, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 589918919, + "versionNonce": 1163085415, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 223, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1391324195", + "type": "text", + "x": 351.5, + "y": 323.0, + "width": 235.95000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1526893899, + "versionNonce": 1805412222, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "splice(3, NULL, 4, NULL, 65536, 0) = 22", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "splice(3, NULL, 4, NULL, 65536, 0) = 22", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1209574647", + "type": "arrow", + "x": 572, + "y": 380, + "width": 0, + "height": 88, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1710563240, + "versionNonce": 1370779286, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 88 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1748030729", + "type": "text", + "x": 572.0, + "y": 410.0, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 817503724, + "versionNonce": 1253304474, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pages move, no user copy", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pages move, no user copy", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1918116528", + "type": "arrow", + "x": 680, + "y": 502, + "width": 53, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 177106067, + "versionNonce": 1954670143, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 53, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t605776991", + "type": "text", + "x": 706.5, + "y": 488.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1644722701, + "versionNonce": 510383720, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t27482207", + "type": "text", + "x": 40, + "y": 405, + "width": 828.85, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 518400525, + "versionNonce": 1523950642, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reader killed \u2192 splice = -1 EPIPE \u2192 \"pmon: tail reader detached\" \u2014 the relay survives and re-splices undelivered bytes to the next reader", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reader killed \u2192 splice = -1 EPIPE \u2192 \"pmon: tail reader detached\" \u2014 the relay survives and re-splices undelivered bytes to the next reader", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t272598424", + "type": "text", + "x": 20, + "y": 20, + "width": 910.8000000000001, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 456896549, + "versionNonce": 1812319486, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon v4's log pipeline \u2014 every pid, fd number, pipe inode, flag value, and splice return in this figure comes from the chapter's live runs", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon v4's log pipeline \u2014 every pid, fd number, pipe inode, flag value, and splice return in this figure comes from the chapter's live runs", + "lineHeight": 1.25, + "baseline": 12 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/18-log-topology.svg b/assets/diagrams/18-log-topology.svg new file mode 100644 index 0000000..edd7f06 --- /dev/null +++ b/assets/diagrams/18-log-topology.svg @@ -0,0 +1,75 @@ + + + +pmon supervise (pid 2633754) — capture + +pmon tail (pid 2634759) — relay + +kernel FIFO + reader + +sh (child 2633756) +fd 1 → pipe:[16462630] +fd 2 → pipe:[16462631] +mask reset at spawn + +stdout pipe +inode 16462630 + +stderr pipe +inode 16462631 + +one poll(2) loop +signalfd 4 · pidfd 6 +pipe read ends 5 and 7 +LineRelay tags each line + +live.log — fd 3 +O_WRONLY|O_APPEND +"[out] …" / "[err] …" + +live.log — fd 3 +O_RDONLY, fdinfo pos 10 +offset advances only +for delivered bytes + +FIFO writer — fd 4 +fdinfo flags 02100001 +O_NONBLOCK cleared +after ENXIO wait loop + +live.fifo +S_IFIFO, mode 0644 + +cat — fd 3 +detach → EPIPE upstream +next reader loses nothing + +write(2) +write(2) + +write(2) +write(2) + +POLLIN +POLLIN + +POLLIN +POLLIN + +append +append + +tail follows the same file, from offset 0 +tail follows the same file, from offset 0 + +splice(3, NULL, 4, NULL, 65536, 0) = 22 +splice(3, NULL, 4, NULL, 65536, 0) = 22 + +pages move, no user copy +pages move, no user copy + +read(2) +read(2) +reader killed → splice = -1 EPIPE → "pmon: tail reader detached" — the relay survives and re-splices undelivered bytes to the next reader +pmon v4's log pipeline — every pid, fd number, pipe inode, flag value, and splice return in this figure comes from the chapter's live runs + \ No newline at end of file diff --git a/assets/diagrams/18-pipe-internals.excalidraw b/assets/diagrams/18-pipe-internals.excalidraw new file mode 100644 index 0000000..9fee9c3 --- /dev/null +++ b/assets/diagrams/18-pipe-internals.excalidraw @@ -0,0 +1,1703 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r916207115", + "type": "rectangle", + "x": 20, + "y": 60, + "width": 210, + "height": 180, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 165737333, + "versionNonce": 1675143231, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1810501201", + "type": "text", + "x": 32, + "y": 66, + "width": 178.20000000000002, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1010387261, + "versionNonce": 451088306, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "writer \u2014 sh (child 2633756)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "writer \u2014 sh (child 2633756)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r295863709", + "type": "rectangle", + "x": 260, + "y": 60, + "width": 440, + "height": 330, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1299580089, + "versionNonce": 1329970642, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t166384306", + "type": "text", + "x": 272, + "y": 66, + "width": 310.20000000000005, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 649337801, + "versionNonce": 1337007471, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel \u2014 struct pipe_inode_info, inode 16462630", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel \u2014 struct pipe_inode_info, inode 16462630", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1468725804", + "type": "rectangle", + "x": 730, + "y": 60, + "width": 210, + "height": 180, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 782921918, + "versionNonce": 399547794, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t739303742", + "type": "text", + "x": 742, + "y": 66, + "width": 151.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 732129259, + "versionNonce": 734432187, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reader \u2014 pmon supervise", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reader \u2014 pmon supervise", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1686143387", + "type": "rectangle", + "x": 40, + "y": 110, + "width": 170, + "height": 90, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 829984651, + "versionNonce": 775584021, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t899943223", + "type": "text", + "x": 50, + "y": 118, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1393034663, + "versionNonce": 1202849660, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd 1 \u2014 write end", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd 1 \u2014 write end", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1868970697", + "type": "text", + "x": 50, + "y": 138, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 172252673, + "versionNonce": 534469778, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe:[16462630]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe:[16462630]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t599091063", + "type": "text", + "x": 50, + "y": 153, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 621607673, + "versionNonce": 515074832, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe2(\u2026, O_CLOEXEC)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe2(\u2026, O_CLOEXEC)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1196077756", + "type": "rectangle", + "x": 750, + "y": 110, + "width": 170, + "height": 90, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 712266005, + "versionNonce": 1219163638, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t134422164", + "type": "text", + "x": 760, + "y": 118, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 736822377, + "versionNonce": 339124312, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd 5 \u2014 read end", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd 5 \u2014 read end", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1478081241", + "type": "text", + "x": 760, + "y": 138, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 630029785, + "versionNonce": 298049429, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe:[16462630]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe:[16462630]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t249243373", + "type": "text", + "x": 760, + "y": 153, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1986853953, + "versionNonce": 505642351, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fdinfo flags 02000000", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fdinfo flags 02000000", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r645165569", + "type": "rectangle", + "x": 300, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 876680322, + "versionNonce": 1231789356, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1681858805", + "type": "text", + "x": 310, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1550619970, + "versionNonce": 79521272, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p0", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p0", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r793861061", + "type": "rectangle", + "x": 350, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 949324924, + "versionNonce": 1986698368, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1364774674", + "type": "text", + "x": 360, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 544950829, + "versionNonce": 1392268161, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p1", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p1", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r229125669", + "type": "rectangle", + "x": 400, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 183066699, + "versionNonce": 538802702, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1710582405", + "type": "text", + "x": 410, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1649563571, + "versionNonce": 51291241, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p2", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p2", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r1938926256", + "type": "rectangle", + "x": 450, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 882857398, + "versionNonce": 931888294, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1159103972", + "type": "text", + "x": 460, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1013912866, + "versionNonce": 586733818, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r147820452", + "type": "rectangle", + "x": 500, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 100561109, + "versionNonce": 1078664232, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t886212621", + "type": "text", + "x": 510, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 659108955, + "versionNonce": 756942608, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p4", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p4", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r633103402", + "type": "rectangle", + "x": 550, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 569628575, + "versionNonce": 1628638620, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1020891279", + "type": "text", + "x": 560, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 661542716, + "versionNonce": 985354333, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p5", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p5", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r945523582", + "type": "rectangle", + "x": 600, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1340837908, + "versionNonce": 1813941259, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t823991311", + "type": "text", + "x": 610, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 867741385, + "versionNonce": 1382948823, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p6", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p6", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "r294970629", + "type": "rectangle", + "x": 650, + "y": 130, + "width": 44, + "height": 44, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 36973119, + "versionNonce": 1915956192, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t413667231", + "type": "text", + "x": 660, + "y": 138, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1607738086, + "versionNonce": 1537493556, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p7", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p7", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "a364925171", + "type": "arrow", + "x": 210, + "y": 152, + "width": 88, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 352344404, + "versionNonce": 752989447, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 88, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1329016794", + "type": "text", + "x": 254.0, + "y": 138.0, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1138623838, + "versionNonce": 281732330, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "write(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "write(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1350368313", + "type": "arrow", + "x": 696, + "y": 152, + "width": 52, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1465319739, + "versionNonce": 781588062, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 52, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1118393374", + "type": "text", + "x": 722.0, + "y": 138.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 444634826, + "versionNonce": 222372603, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1900462026", + "type": "arrow", + "x": 322, + "y": 220, + "width": 0, + "height": 42, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 782278756, + "versionNonce": 1810251153, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -42 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1342214923", + "type": "text", + "x": 322.0, + "y": 185.0, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1668055691, + "versionNonce": 1367437381, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "head \u2014 next write lands here", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "head \u2014 next write lands here", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1388179001", + "type": "arrow", + "x": 622, + "y": 220, + "width": 0, + "height": 42, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 519217783, + "versionNonce": 869527009, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -42 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t930259984", + "type": "text", + "x": 622.0, + "y": 185.0, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 565280025, + "versionNonce": 1051798760, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "tail \u2014 read drains here", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "tail \u2014 read drains here", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t182517462", + "type": "text", + "x": 300, + "y": 95, + "width": 278.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 774915033, + "versionNonce": 1135456273, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ring of 16 slots, one page each \u2014 only 8 drawn", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ring of 16 slots, one page each \u2014 only 8 drawn", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t984265744", + "type": "text", + "x": 300, + "y": 252, + "width": 411.40000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 635380619, + "versionNonce": 1504958075, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "capacity: 16 \u00d7 4096 B = 65536 B \u2014 read live with fcntl(F_GETPIPE_SZ)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "capacity: 16 \u00d7 4096 B = 65536 B \u2014 read live with fcntl(F_GETPIPE_SZ)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t170521135", + "type": "text", + "x": 300, + "y": 274, + "width": 344.85, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1002427268, + "versionNonce": 1538123362, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "F_SETPIPE_SZ asked for 1 \u2192 rounded up to 4096 (one page);", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "F_SETPIPE_SZ asked for 1 \u2192 rounded up to 4096 (one page);", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1838436658", + "type": "text", + "x": 300, + "y": 290, + "width": 363.00000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 821129399, + "versionNonce": 1127125310, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ceiling: /proc/sys/fs/pipe-max-size = 1048576 (unprivileged)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ceiling: /proc/sys/fs/pipe-max-size = 1048576 (unprivileged)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1620683807", + "type": "text", + "x": 300, + "y": 318, + "width": 381.15000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1556431915, + "versionNonce": 833789315, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "PIPE_BUF = 4096: one write() of \u2264 4096 B never interleaves with", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "PIPE_BUF = 4096: one write() of \u2264 4096 B never interleaves with", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t106738231", + "type": "text", + "x": 300, + "y": 334, + "width": 369.05, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1160580480, + "versionNonce": 96107958, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "another writer's \u2014 but reads may still return partial chunks:", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "another writer's \u2014 but reads may still return partial chunks:", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1149877978", + "type": "text", + "x": 300, + "y": 350, + "width": 302.5, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 495789187, + "versionNonce": 14336236, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "atomicity is not message framing, and not delivery", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "atomicity is not message framing, and not delivery", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t432027178", + "type": "text", + "x": 30, + "y": 260, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 513842901, + "versionNonce": 1809581337, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "all write ends closed \u2192", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "all write ends closed \u2192", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1948965102", + "type": "text", + "x": 30, + "y": 276, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1874818669, + "versionNonce": 1602838593, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "read() returns 0 (EOF)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "read() returns 0 (EOF)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t293303442", + "type": "text", + "x": 740, + "y": 260, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1068322185, + "versionNonce": 1387079087, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no read end left \u2192 write()", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no read end left \u2192 write()", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t726417679", + "type": "text", + "x": 740, + "y": 276, + "width": 133.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1039983373, + "versionNonce": 1002053472, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "raises SIGPIPE / EPIPE", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "raises SIGPIPE / EPIPE", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1873093052", + "type": "text", + "x": 20, + "y": 24, + "width": 739.2, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 835118941, + "versionNonce": 745785233, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one pipe from the chapter's live supervise run \u2014 both fd numbers, the inode, and every byte count are from /proc", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one pipe from the chapter's live supervise run \u2014 both fd numbers, the inode, and every byte count are from /proc", + "lineHeight": 1.25, + "baseline": 12 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/18-pipe-internals.svg b/assets/diagrams/18-pipe-internals.svg new file mode 100644 index 0000000..043dbca --- /dev/null +++ b/assets/diagrams/18-pipe-internals.svg @@ -0,0 +1,57 @@ + + + +writer — sh (child 2633756) + +kernel — struct pipe_inode_info, inode 16462630 + +reader — pmon supervise + +fd 1 — write end +pipe:[16462630] +pipe2(…, O_CLOEXEC) + +fd 5 — read end +pipe:[16462630] +fdinfo flags 02000000 + +p0 + +p1 + +p2 + +p3 + +p4 + +p5 + +p6 + +p7 + +write(2) +write(2) + +read(2) +read(2) + +head — next write lands here +head — next write lands here + +tail — read drains here +tail — read drains here +ring of 16 slots, one page each — only 8 drawn +capacity: 16 × 4096 B = 65536 B — read live with fcntl(F_GETPIPE_SZ) +F_SETPIPE_SZ asked for 1 → rounded up to 4096 (one page); +ceiling: /proc/sys/fs/pipe-max-size = 1048576 (unprivileged) +PIPE_BUF = 4096: one write() of ≤ 4096 B never interleaves with +another writer's — but reads may still return partial chunks: +atomicity is not message framing, and not delivery +all write ends closed → +read() returns 0 (EOF) +no read end left → write() +raises SIGPIPE / EPIPE +one pipe from the chapter's live supervise run — both fd numbers, the inode, and every byte count are from /proc + \ No newline at end of file diff --git a/assets/diagrams/19-scm-rights-transfer.excalidraw b/assets/diagrams/19-scm-rights-transfer.excalidraw new file mode 100644 index 0000000..03035c2 --- /dev/null +++ b/assets/diagrams/19-scm-rights-transfer.excalidraw @@ -0,0 +1,1545 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1081465470", + "type": "rectangle", + "x": 20, + "y": 40, + "width": 350, + "height": 130, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1271705160, + "versionNonce": 1212123725, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t875429660", + "type": "text", + "x": 32, + "y": 46, + "width": 257.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 761303706, + "versionNonce": 217334016, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon supervise \u2014 fd table (pid 2622904)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon supervise \u2014 fd table (pid 2622904)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r464626351", + "type": "rectangle", + "x": 590, + "y": 40, + "width": 350, + "height": 130, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 938979740, + "versionNonce": 181612087, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1788754849", + "type": "text", + "x": 602, + "y": 46, + "width": 198.00000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1798493035, + "versionNonce": 874094001, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmctl \u2014 fd table (pid 2623753)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmctl \u2014 fd table (pid 2623753)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1003074533", + "type": "rectangle", + "x": 20, + "y": 250, + "width": 920, + "height": 230, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 195525492, + "versionNonce": 1615490685, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1464566782", + "type": "text", + "x": 32, + "y": 256, + "width": 184.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1389779376, + "versionNonce": 43522800, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel \u2014 shared, system-wide", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel \u2014 shared, system-wide", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r430017427", + "type": "rectangle", + "x": 60, + "y": 85, + "width": 270, + "height": 60, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 551254167, + "versionNonce": 1942834667, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1256980326", + "type": "text", + "x": 70, + "y": 93, + "width": 123.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1573372534, + "versionNonce": 1624122711, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 3 (sender)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 3 (sender)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t983107474", + "type": "text", + "x": 70, + "y": 113, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 460377501, + "versionNonce": 163851414, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "close-on-exec: set at open (O_CLOEXEC)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "close-on-exec: set at open (O_CLOEXEC)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r570725292", + "type": "rectangle", + "x": 630, + "y": 85, + "width": 270, + "height": 60, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1310313233, + "versionNonce": 1450127046, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t366643473", + "type": "text", + "x": 640, + "y": 93, + "width": 284.90000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 379201841, + "versionNonce": 1233498707, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "slot 4 (receiver \u2014 new, lowest free)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "slot 4 (receiver \u2014 new, lowest free)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t730702779", + "type": "text", + "x": 640, + "y": 113, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1804424359, + "versionNonce": 1914650208, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "close-on-exec: set by MSG_CMSG_CLOEXEC", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "close-on-exec: set by MSG_CMSG_CLOEXEC", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r112409601", + "type": "rectangle", + "x": 330, + "y": 280, + "width": 300, + "height": 70, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 539874341, + "versionNonce": 1379717986, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t127923205", + "type": "text", + "x": 340, + "y": 288, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 829558299, + "versionNonce": 804434077, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one open file description", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one open file description", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t895565657", + "type": "text", + "x": 340, + "y": 308, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 109847025, + "versionNonce": 996691570, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pos: 47 flags: 02102002", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pos: 47 flags: 02102002", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t827987154", + "type": "text", + "x": 340, + "y": 323, + "width": 223.85000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 568058943, + "versionNonce": 1827242884, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "O_RDWR|O_APPEND|O_CLOEXEC|O_LARGEFILE", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "O_RDWR|O_APPEND|O_CLOEXEC|O_LARGEFILE", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r577703324", + "type": "rectangle", + "x": 330, + "y": 395, + "width": 300, + "height": 58, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1223852746, + "versionNonce": 1258695614, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t362550282", + "type": "text", + "x": 340, + "y": 403, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1717597568, + "versionNonce": 1174787571, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "inode 95934 \u2014 pmon.log", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "inode 95934 \u2014 pmon.log", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t82684651", + "type": "text", + "x": 340, + "y": 423, + "width": 254.10000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1992572269, + "versionNonce": 1239419581, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "47 bytes: both /proc fdinfo agree, pos: 47", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "47 bytes: both /proc fdinfo agree, pos: 47", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1880637611", + "type": "rectangle", + "x": 40, + "y": 300, + "width": 250, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 531185724, + "versionNonce": 217321515, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1332704179", + "type": "text", + "x": 50, + "y": 308, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1584255161, + "versionNonce": 110905408, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "sendmsg(5, \u2026)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "sendmsg(5, \u2026)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t944876928", + "type": "text", + "x": 50, + "y": 328, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1061749457, + "versionNonce": 635529357, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cmsg SOL_SOCKET SCM_RIGHTS", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cmsg SOL_SOCKET SCM_RIGHTS", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1202544077", + "type": "text", + "x": 50, + "y": 343, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 546328534, + "versionNonce": 1448012018, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cmsg_data=[3]", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cmsg_data=[3]", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1054611264", + "type": "rectangle", + "x": 670, + "y": 300, + "width": 250, + "height": 60, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 968397640, + "versionNonce": 1387569301, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t194731901", + "type": "text", + "x": 680, + "y": 308, + "width": 238.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1154709456, + "versionNonce": 420283847, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "recvmsg(3, \u2026, MSG_CMSG_CLOEXEC)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "recvmsg(3, \u2026, MSG_CMSG_CLOEXEC)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1967021015", + "type": "text", + "x": 680, + "y": 328, + "width": 157.3, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 591396197, + "versionNonce": 1843080747, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cmsg SOL_SOCKET SCM_RIGHTS", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cmsg SOL_SOCKET SCM_RIGHTS", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t609708815", + "type": "text", + "x": 680, + "y": 343, + "width": 187.55, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 534455278, + "versionNonce": 1977418230, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "cmsg_data=[4] \u2014 kernel picked 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "cmsg_data=[4] \u2014 kernel picked 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1422619994", + "type": "arrow", + "x": 195, + "y": 145, + "width": 205, + "height": 135, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 970258098, + "versionNonce": 345824259, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 205, + 135 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t123415359", + "type": "text", + "x": 297.5, + "y": 198.5, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 737151032, + "versionNonce": 1454226063, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "points at", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "points at", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1677115947", + "type": "arrow", + "x": 765, + "y": 145, + "width": 205, + "height": 135, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1164650815, + "versionNonce": 1535104384, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -205, + 135 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t356700102", + "type": "text", + "x": 662.5, + "y": 198.5, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1019900616, + "versionNonce": 1230633940, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "points at", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "points at", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a530315278", + "type": "arrow", + "x": 480, + "y": 350, + "width": 0, + "height": 45, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 887304347, + "versionNonce": 303413481, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 45 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t939011011", + "type": "text", + "x": 480.0, + "y": 358.5, + "width": 169.4, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1380343052, + "versionNonce": 123831393, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one offset, one set of flags", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one offset, one set of flags", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a611526352", + "type": "arrow", + "x": 165, + "y": 300, + "width": 0, + "height": 155, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 440072264, + "versionNonce": 397783148, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -155 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1313875225", + "type": "text", + "x": 165.0, + "y": 208.5, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1317257761, + "versionNonce": 852417, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd 3 named in cmsg", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd 3 named in cmsg", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1060779986", + "type": "arrow", + "x": 290, + "y": 330, + "width": 40, + "height": 15, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 771506737, + "versionNonce": 1658392598, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + -15 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1436923161", + "type": "text", + "x": 310.0, + "y": 308.5, + "width": 72.60000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1466574295, + "versionNonce": 1539205876, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "names slot 3", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "names slot 3", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a283559042", + "type": "arrow", + "x": 630, + "y": 315, + "width": 40, + "height": 15, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1118280562, + "versionNonce": 208798476, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 40, + 15 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1776070298", + "type": "text", + "x": 650.0, + "y": 308.5, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1600028623, + "versionNonce": 331392587, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "delivers a reference", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "delivers a reference", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a138145987", + "type": "arrow", + "x": 795, + "y": 300, + "width": 0, + "height": 155, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 802373963, + "versionNonce": 1300207513, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -155 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1415849202", + "type": "text", + "x": 795.0, + "y": 208.5, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 747466193, + "versionNonce": 1203069760, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel installs new slot", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel installs new slot", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1822672558", + "type": "text", + "x": 40, + "y": 20, + "width": 587.4000000000001, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 612465563, + "versionNonce": 16447983, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SCM_RIGHTS: the fd number does not travel \u2014 a reference to the open file description does", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SCM_RIGHTS: the fd number does not travel \u2014 a reference to the open file description does", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "t436504286", + "type": "text", + "x": 40, + "y": 215, + "width": 629.2, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1831556077, + "versionNonce": 1752709335, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ch07's three layers: per-process slot (cloexec bit lives here) \u2192 shared description (pos, flags) \u2192 inode", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ch07's three layers: per-process slot (cloexec bit lives here) \u2192 shared description (pos, flags) \u2192 inode", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1555023657", + "type": "text", + "x": 640, + "y": 470, + "width": 242.00000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 213804258, + "versionNonce": 827483593, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "numbers from the chapter's held-open run", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "numbers from the chapter's held-open run", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/19-scm-rights-transfer.svg b/assets/diagrams/19-scm-rights-transfer.svg new file mode 100644 index 0000000..10dfc23 --- /dev/null +++ b/assets/diagrams/19-scm-rights-transfer.svg @@ -0,0 +1,54 @@ + + + +pmon supervise — fd table (pid 2622904) + +pmctl — fd table (pid 2623753) + +kernel — shared, system-wide + +slot 3 (sender) +close-on-exec: set at open (O_CLOEXEC) + +slot 4 (receiver — new, lowest free) +close-on-exec: set by MSG_CMSG_CLOEXEC + +one open file description +pos: 47 flags: 02102002 +O_RDWR|O_APPEND|O_CLOEXEC|O_LARGEFILE + +inode 95934 — pmon.log +47 bytes: both /proc fdinfo agree, pos: 47 + +sendmsg(5, …) +cmsg SOL_SOCKET SCM_RIGHTS +cmsg_data=[3] + +recvmsg(3, …, MSG_CMSG_CLOEXEC) +cmsg SOL_SOCKET SCM_RIGHTS +cmsg_data=[4] — kernel picked 4 + +points at +points at + +points at +points at + +one offset, one set of flags +one offset, one set of flags + +fd 3 named in cmsg +fd 3 named in cmsg + +names slot 3 +names slot 3 + +delivers a reference +delivers a reference + +kernel installs new slot +kernel installs new slot +SCM_RIGHTS: the fd number does not travel — a reference to the open file description does +ch07's three layers: per-process slot (cloexec bit lives here) → shared description (pos, flags) → inode +numbers from the chapter's held-open run + \ No newline at end of file diff --git a/assets/diagrams/19-uds-control-plane.excalidraw b/assets/diagrams/19-uds-control-plane.excalidraw new file mode 100644 index 0000000..bf2d0fe --- /dev/null +++ b/assets/diagrams/19-uds-control-plane.excalidraw @@ -0,0 +1,1917 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1776295248", + "type": "rectangle", + "x": 20, + "y": 40, + "width": 380, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1411053377, + "versionNonce": 1932484117, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1206908548", + "type": "text", + "x": 32, + "y": 46, + "width": 191.4, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1606470474, + "versionNonce": 110641149, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon supervise (pid 2622904)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon supervise (pid 2622904)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1371419780", + "type": "rectangle", + "x": 560, + "y": 40, + "width": 380, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1036338659, + "versionNonce": 1412595967, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t132131274", + "type": "text", + "x": 572, + "y": 46, + "width": 165.0, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1495664615, + "versionNonce": 1761765645, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon pmctl (pid 2624432)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon pmctl (pid 2624432)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1758148221", + "type": "rectangle", + "x": 20, + "y": 355, + "width": 920, + "height": 95, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 186043918, + "versionNonce": 148006345, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t400086425", + "type": "text", + "x": 32, + "y": 361, + "width": 40, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 764475839, + "versionNonce": 1187189363, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1619777487", + "type": "rectangle", + "x": 40, + "y": 75, + "width": 160, + "height": 56, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 401620671, + "versionNonce": 1368908377, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t859868128", + "type": "text", + "x": 50, + "y": 83, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1427136633, + "versionNonce": 1708092462, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "listener fd 4", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "listener fd 4", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t38487506", + "type": "text", + "x": 50, + "y": 103, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1334850111, + "versionNonce": 1814957888, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "bind /tmp/\u2026/ctl.sock", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bind /tmp/\u2026/ctl.sock", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t43503845", + "type": "text", + "x": 50, + "y": 118, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 265923952, + "versionNonce": 17666030, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SOCK_STREAM|CLOEXEC", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SOCK_STREAM|CLOEXEC", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r490986032", + "type": "rectangle", + "x": 40, + "y": 150, + "width": 160, + "height": 56, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 151678543, + "versionNonce": 1537148295, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t251671331", + "type": "text", + "x": 50, + "y": 158, + "width": 100.10000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 234146570, + "versionNonce": 1108574434, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "accepted fd 5", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "accepted fd 5", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t912026192", + "type": "text", + "x": 50, + "y": 178, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 642138710, + "versionNonce": 1673255280, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SO_PEERCRED first:", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SO_PEERCRED first:", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1571597054", + "type": "text", + "x": 50, + "y": 193, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 806835150, + "versionNonce": 344048620, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uid=1000 pid=2624432", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uid=1000 pid=2624432", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r281489560", + "type": "rectangle", + "x": 40, + "y": 225, + "width": 160, + "height": 52, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 967767067, + "versionNonce": 1664026874, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t56247139", + "type": "text", + "x": 50, + "y": 233, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1452965405, + "versionNonce": 1018590738, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "log fd 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "log fd 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1120319971", + "type": "text", + "x": 50, + "y": 253, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 336681584, + "versionNonce": 1578307682, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon.log", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon.log", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t963622378", + "type": "text", + "x": 50, + "y": 268, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1904389404, + "versionNonce": 723435836, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "O_RDWR|O_APPEND", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "O_RDWR|O_APPEND", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1945930738", + "type": "rectangle", + "x": 225, + "y": 150, + "width": 155, + "height": 56, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1265566287, + "versionNonce": 613077155, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1367122080", + "type": "text", + "x": 235, + "y": 158, + "width": 92.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 473632970, + "versionNonce": 653569441, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "command loop", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "command loop", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t59099479", + "type": "text", + "x": 235, + "y": 178, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1157069768, + "versionNonce": 1644969749, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "status / logfd / stop", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "status / logfd / stop", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1102697529", + "type": "text", + "x": 235, + "y": 193, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1097078782, + "versionNonce": 1851443444, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one line in, one out", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one line in, one out", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r854455779", + "type": "rectangle", + "x": 225, + "y": 225, + "width": 155, + "height": 52, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 207022285, + "versionNonce": 549772728, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t365135237", + "type": "text", + "x": 235, + "y": 233, + "width": 115.50000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1030134680, + "versionNonce": 901380957, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child sleep 300", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child sleep 300", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1537175532", + "type": "text", + "x": 235, + "y": 253, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 81887166, + "versionNonce": 7536173, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pid 2622905", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pid 2622905", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1370886640", + "type": "text", + "x": 235, + "y": 268, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1373259742, + "versionNonce": 1417041438, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "own process group", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "own process group", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1908095482", + "type": "rectangle", + "x": 580, + "y": 75, + "width": 160, + "height": 56, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1166354019, + "versionNonce": 1073454328, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1071992187", + "type": "text", + "x": 590, + "y": 83, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1489459713, + "versionNonce": 1971976119, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "client fd 3", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "client fd 3", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t260226990", + "type": "text", + "x": 590, + "y": 103, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 255661383, + "versionNonce": 1107246826, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "connect ctl.sock", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "connect ctl.sock", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t995277413", + "type": "text", + "x": 590, + "y": 118, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1266221277, + "versionNonce": 393952535, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "writes \"status\\n\"", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "writes \"status\\n\"", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r609043894", + "type": "rectangle", + "x": 765, + "y": 75, + "width": 155, + "height": 56, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1559235185, + "versionNonce": 290252542, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1790989577", + "type": "text", + "x": 775, + "y": 83, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 653570791, + "versionNonce": 65648350, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "reply", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "reply", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1088173249", + "type": "text", + "x": 775, + "y": 103, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1927309863, + "versionNonce": 124504039, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "child=2622905", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "child=2622905", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1562852161", + "type": "text", + "x": 775, + "y": 118, + "width": 127.05000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1894628899, + "versionNonce": 728148243, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "uptime=20s restarts=0", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "uptime=20s restarts=0", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1671328483", + "type": "rectangle", + "x": 120, + "y": 380, + "width": 300, + "height": 48, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 396839301, + "versionNonce": 1543184168, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1255167758", + "type": "text", + "x": 130, + "y": 388, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 434364456, + "versionNonce": 570501659, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "socket inode 16411996", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "socket inode 16411996", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1905210627", + "type": "text", + "x": 130, + "y": 408, + "width": 272.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1245680773, + "versionNonce": 15850468, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "supervisor end \u2014 ss -xp: app,pid=2622904,fd=5", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "supervisor end \u2014 ss -xp: app,pid=2622904,fd=5", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1193191641", + "type": "rectangle", + "x": 540, + "y": 380, + "width": 300, + "height": 48, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1957773007, + "versionNonce": 378303246, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t813077729", + "type": "text", + "x": 550, + "y": 388, + "width": 161.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 429539398, + "versionNonce": 1517838831, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "socket inode 16422777", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "socket inode 16422777", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1266399943", + "type": "text", + "x": 550, + "y": 408, + "width": 248.05, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1784805591, + "versionNonce": 2298299, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "client end \u2014 ss -xp: app,pid=2624432,fd=3", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "client end \u2014 ss -xp: app,pid=2624432,fd=3", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1566369680", + "type": "arrow", + "x": 120, + "y": 131, + "width": 0, + "height": 19, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1348713622, + "versionNonce": 1075010724, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 19 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1922664499", + "type": "text", + "x": 120.0, + "y": 126.5, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 242133753, + "versionNonce": 385775448, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "accept4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "accept4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1601972206", + "type": "arrow", + "x": 200, + "y": 178, + "width": 25, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 964071240, + "versionNonce": 1559288563, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 25, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1749694700", + "type": "arrow", + "x": 302, + "y": 206, + "width": 0, + "height": 19, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 939167848, + "versionNonce": 1551959015, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 19 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1708311299", + "type": "text", + "x": 302.0, + "y": 201.5, + "width": 90.75000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 842932197, + "versionNonce": 527872515, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SIGTERM on stop", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SIGTERM on stop", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a878369799", + "type": "arrow", + "x": 225, + "y": 253, + "width": 25, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 688048654, + "versionNonce": 233895933, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -25, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1254934925", + "type": "text", + "x": 212.5, + "y": 239.0, + "width": 78.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1333123381, + "versionNonce": 121475807, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "stdout+stderr", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "stdout+stderr", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a1463669361", + "type": "arrow", + "x": 660, + "y": 131, + "width": 0, + "height": 249, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 426651961, + "versionNonce": 1139338262, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 249 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t599818808", + "type": "text", + "x": 660.0, + "y": 241.5, + "width": 60.50000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1224400376, + "versionNonce": 62414668, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "connect(2)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "connect(2)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a568356031", + "type": "arrow", + "x": 160, + "y": 206, + "width": 70, + "height": 174, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1321767471, + "versionNonce": 1410554348, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 70, + 174 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1394887099", + "type": "arrow", + "x": 420, + "y": 404, + "width": 120, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 875328769, + "versionNonce": 1508694309, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 120, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t183157070", + "type": "text", + "x": 480.0, + "y": 390.0, + "width": 121.00000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 71650405, + "versionNonce": 283216540, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one stream, two ends", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one stream, two ends", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a268458871", + "type": "arrow", + "x": 740, + "y": 103, + "width": 25, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1050712497, + "versionNonce": 1664655970, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 25, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1334229220", + "type": "text", + "x": 40, + "y": 20, + "width": 422.40000000000003, + "height": 15.0, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1242443195, + "versionNonce": 901551398, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pmon v5 control plane \u2014 every number from the chapter's live run", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pmon v5 control plane \u2014 every number from the chapter's live run", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "t851722053", + "type": "text", + "x": 415, + "y": 325, + "width": 326.70000000000005, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1360131914, + "versionNonce": 1172465449, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "SO_PEERCRED: the kernel, not the client, names uid/pid", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SO_PEERCRED: the kernel, not the client, names uid/pid", + "lineHeight": 1.25, + "baseline": 11 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/19-uds-control-plane.svg b/assets/diagrams/19-uds-control-plane.svg new file mode 100644 index 0000000..325c5e3 --- /dev/null +++ b/assets/diagrams/19-uds-control-plane.svg @@ -0,0 +1,63 @@ + + + +pmon supervise (pid 2622904) + +pmon pmctl (pid 2624432) + +kernel + +listener fd 4 +bind /tmp/…/ctl.sock +SOCK_STREAM|CLOEXEC + +accepted fd 5 +SO_PEERCRED first: +uid=1000 pid=2624432 + +log fd 3 +pmon.log +O_RDWR|O_APPEND + +command loop +status / logfd / stop +one line in, one out + +child sleep 300 +pid 2622905 +own process group + +client fd 3 +connect ctl.sock +writes "status\n" + +reply +child=2622905 +uptime=20s restarts=0 + +socket inode 16411996 +supervisor end — ss -xp: app,pid=2622904,fd=5 + +socket inode 16422777 +client end — ss -xp: app,pid=2624432,fd=3 + +accept4 +accept4 + + +SIGTERM on stop +SIGTERM on stop + +stdout+stderr +stdout+stderr + +connect(2) +connect(2) + + +one stream, two ends +one stream, two ends + +pmon v5 control plane — every number from the chapter's live run +SO_PEERCRED: the kernel, not the client, names uid/pid + \ No newline at end of file diff --git a/assets/diagrams/20-futex-wait-wake.excalidraw b/assets/diagrams/20-futex-wait-wake.excalidraw new file mode 100644 index 0000000..fa67b1f --- /dev/null +++ b/assets/diagrams/20-futex-wait-wake.excalidraw @@ -0,0 +1,1629 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r1608294862", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 280, + "height": 470, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1383452884, + "versionNonce": 609069137, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1257222367", + "type": "text", + "x": 32, + "y": 26, + "width": 191.4, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1204499459, + "versionNonce": 1502101703, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "watcher process (shmkv watch)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "watcher process (shmkv watch)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1556472046", + "type": "rectangle", + "x": 320, + "y": 20, + "width": 280, + "height": 470, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 82044555, + "versionNonce": 87906945, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t523467150", + "type": "text", + "x": 332, + "y": 26, + "width": 40, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1549899000, + "versionNonce": 954521761, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "kernel", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "kernel", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r490106247", + "type": "rectangle", + "x": 620, + "y": 20, + "width": 300, + "height": 470, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 989137996, + "versionNonce": 861447489, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1215828838", + "type": "text", + "x": 632, + "y": 26, + "width": 184.8, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1804692217, + "versionNonce": 191927207, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "writer process (shmkv serve)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "writer process (shmkv serve)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1193098437", + "type": "rectangle", + "x": 40, + "y": 55, + "width": 240, + "height": 58, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1089046289, + "versionNonce": 108826906, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1886147501", + "type": "text", + "x": 50, + "y": 63, + "width": 192.50000000000003, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1507639685, + "versionNonce": 1312685001, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seqlock read: nothing new", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seqlock read: nothing new", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t954121974", + "type": "text", + "x": 50, + "y": 83, + "width": 199.65, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 546162782, + "versionNonce": 6033391, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "counter still 3; futex word w = 3", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "counter still 3; futex word w = 3", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r334631128", + "type": "rectangle", + "x": 40, + "y": 133, + "width": 240, + "height": 58, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1039045707, + "versionNonce": 653324781, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t370306768", + "type": "text", + "x": 50, + "y": 141, + "width": 200.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1648291906, + "versionNonce": 1885962184, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex(FUTEX_WAIT, w=3, 2s)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex(FUTEX_WAIT, w=3, 2s)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t867992207", + "type": "text", + "x": 50, + "y": 161, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1741114342, + "versionNonce": 1737518437, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "shared futex - no PRIVATE flag", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "shared futex - no PRIVATE flag", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r80618496", + "type": "rectangle", + "x": 340, + "y": 133, + "width": 240, + "height": 72, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 82884382, + "versionNonce": 118924793, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1940350627", + "type": "text", + "x": 350, + "y": 141, + "width": 154.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1062062378, + "versionNonce": 1501231317, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "re-check word == 3 ?", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "re-check word == 3 ?", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t953408214", + "type": "text", + "x": 350, + "y": 161, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1828709074, + "versionNonce": 965965045, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "yes: sleep on wait queue", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "yes: sleep on wait queue", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1037716932", + "type": "text", + "x": 350, + "y": 176, + "width": 163.35000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1975637958, + "versionNonce": 1036372356, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "keyed by (inode, offset 12)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "keyed by (inode, offset 12)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r378574678", + "type": "rectangle", + "x": 640, + "y": 55, + "width": 260, + "height": 88, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 35765163, + "versionNonce": 1522789187, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1780012717", + "type": "text", + "x": 650, + "y": 63, + "width": 200.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 336461036, + "versionNonce": 1786594046, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seqlock write (publish k4)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seqlock write (publish k4)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1432063376", + "type": "text", + "x": 650, + "y": 83, + "width": 229.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 514092065, + "versionNonce": 256327574, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seq 6 -> 7 (odd), mutate slot+counter,", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seq 6 -> 7 (odd), mutate slot+counter,", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t451155104", + "type": "text", + "x": 650, + "y": 98, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1427000440, + "versionNonce": 1646645462, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seq 7 -> 8 (even)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seq 7 -> 8 (even)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r224117031", + "type": "rectangle", + "x": 640, + "y": 168, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 959594388, + "versionNonce": 612811803, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1358471749", + "type": "text", + "x": 650, + "y": 176, + "width": 154.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 773293638, + "versionNonce": 1299646135, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "store futex word = 4", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "store futex word = 4", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t696322507", + "type": "text", + "x": 650, + "y": 196, + "width": 96.80000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 358966788, + "versionNonce": 1304899048, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "release ordering", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "release ordering", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1618137323", + "type": "rectangle", + "x": 640, + "y": 251, + "width": 260, + "height": 58, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 335540108, + "versionNonce": 413201646, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1069822175", + "type": "text", + "x": 650, + "y": 259, + "width": 200.20000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 725812269, + "versionNonce": 983095435, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex(FUTEX_WAKE, INT_MAX)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex(FUTEX_WAKE, INT_MAX)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t424513391", + "type": "text", + "x": 650, + "y": 279, + "width": 145.20000000000002, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1119471112, + "versionNonce": 430660220, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "strace showed: returns 1", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "strace showed: returns 1", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1634274314", + "type": "rectangle", + "x": 340, + "y": 251, + "width": 240, + "height": 58, + "angle": 0, + "strokeColor": "#888888", + "backgroundColor": "#f4f4f4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 551681630, + "versionNonce": 565327692, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t616484575", + "type": "text", + "x": 350, + "y": 259, + "width": 169.4, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 198875009, + "versionNonce": 1775247366, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "wake every task queued", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "wake every task queued", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t919930312", + "type": "text", + "x": 350, + "y": 279, + "width": 139.15, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1623081166, + "versionNonce": 1168049459, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "on this (inode, offset)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "on this (inode, offset)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1114592624", + "type": "rectangle", + "x": 40, + "y": 340, + "width": 240, + "height": 72, + "angle": 0, + "strokeColor": "#2f6db5", + "backgroundColor": "#eef4fb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1928986050, + "versionNonce": 1047297870, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t9384762", + "type": "text", + "x": 50, + "y": 348, + "width": 154.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 884706525, + "versionNonce": 115992803, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "FUTEX_WAIT returns 0", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "FUTEX_WAIT returns 0", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t638102467", + "type": "text", + "x": 50, + "y": 368, + "width": 151.25, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1169813135, + "versionNonce": 1723312869, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "seqlock read: counter = 4", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "seqlock read: counter = 4", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t259382545", + "type": "text", + "x": 50, + "y": 383, + "width": 181.50000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1771603830, + "versionNonce": 1017700771, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "back-fill ids from 8-slot ring", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "back-fill ids from 8-slot ring", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r233772912", + "type": "rectangle", + "x": 40, + "y": 432, + "width": 540, + "height": 46, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 16053537, + "versionNonce": 1706990569, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1284611577", + "type": "text", + "x": 50, + "y": 440, + "width": 408.1, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1004533180, + "versionNonce": 467417282, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "lost-wakeup guard: if the word moved before sleeping,", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "lost-wakeup guard: if the word moved before sleeping,", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1677260602", + "type": "text", + "x": 50, + "y": 460, + "width": 350.90000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1973035039, + "versionNonce": 400744860, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "FUTEX_WAIT returns EAGAIN and the watcher re-reads instead", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "FUTEX_WAIT returns EAGAIN and the watcher re-reads instead", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a566013298", + "type": "arrow", + "x": 160, + "y": 113, + "width": 0, + "height": 20, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1731013027, + "versionNonce": 1107878356, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 20 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a963664644", + "type": "arrow", + "x": 280, + "y": 162, + "width": 60, + "height": 6, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1115568958, + "versionNonce": 708228396, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 60, + 6 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1980981540", + "type": "text", + "x": 310.0, + "y": 151.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 128502583, + "versionNonce": 1879880370, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "syscall", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "syscall", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a53021329", + "type": "arrow", + "x": 770, + "y": 143, + "width": 0, + "height": 25, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1901349441, + "versionNonce": 587579660, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 25 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a718780252", + "type": "arrow", + "x": 770, + "y": 226, + "width": 0, + "height": 25, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1541777763, + "versionNonce": 1915139704, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 25 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a660938161", + "type": "arrow", + "x": 640, + "y": 280, + "width": 60, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1835476917, + "versionNonce": 674109148, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -60, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1279555950", + "type": "text", + "x": 610.0, + "y": 266.0, + "width": 42.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 848409026, + "versionNonce": 1014703753, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "syscall", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "syscall", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a124140661", + "type": "arrow", + "x": 400, + "y": 309, + "width": 180, + "height": 31, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 984817371, + "versionNonce": 982757493, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -180, + 31 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1798465748", + "type": "text", + "x": 310.0, + "y": 310.5, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 488625242, + "versionNonce": 1826802580, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "runnable again", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "runnable again", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a268291805", + "type": "arrow", + "x": 460, + "y": 205, + "width": 0, + "height": 46, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 229928073, + "versionNonce": 1470995676, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 46 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t1548482099", + "type": "text", + "x": 40, + "y": 498, + "width": 885.5000000000001, + "height": 14.375, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 782022145, + "versionNonce": 60411503, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "The kernel provides exactly one primitive: sleep until this 32-bit word changes, wake sleepers on it. Everything else is userspace protocol.", + "fontSize": 11.5, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "The kernel provides exactly one primitive: sleep until this 32-bit word changes, wake sleepers on it. Everything else is userspace protocol.", + "lineHeight": 1.25, + "baseline": 11.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/20-futex-wait-wake.svg b/assets/diagrams/20-futex-wait-wake.svg new file mode 100644 index 0000000..8da9d3b --- /dev/null +++ b/assets/diagrams/20-futex-wait-wake.svg @@ -0,0 +1,53 @@ + + + +watcher process (shmkv watch) + +kernel + +writer process (shmkv serve) + +seqlock read: nothing new +counter still 3; futex word w = 3 + +futex(FUTEX_WAIT, w=3, 2s) +shared futex - no PRIVATE flag + +re-check word == 3 ? +yes: sleep on wait queue +keyed by (inode, offset 12) + +seqlock write (publish k4) +seq 6 -> 7 (odd), mutate slot+counter, +seq 7 -> 8 (even) + +store futex word = 4 +release ordering + +futex(FUTEX_WAKE, INT_MAX) +strace showed: returns 1 + +wake every task queued +on this (inode, offset) + +FUTEX_WAIT returns 0 +seqlock read: counter = 4 +back-fill ids from 8-slot ring + +lost-wakeup guard: if the word moved before sleeping, +FUTEX_WAIT returns EAGAIN and the watcher re-reads instead + + +syscall +syscall + + + +syscall +syscall + +runnable again +runnable again + +The kernel provides exactly one primitive: sleep until this 32-bit word changes, wake sleepers on it. Everything else is userspace protocol. + \ No newline at end of file diff --git a/assets/diagrams/20-ipc-matrix.excalidraw b/assets/diagrams/20-ipc-matrix.excalidraw new file mode 100644 index 0000000..a5498cd --- /dev/null +++ b/assets/diagrams/20-ipc-matrix.excalidraw @@ -0,0 +1,1669 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "r316693186", + "type": "rectangle", + "x": 20, + "y": 20, + "width": 900, + "height": 300, + "angle": 0, + "strokeColor": "#dddddd", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1560812585, + "versionNonce": 205098156, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1583768908", + "type": "text", + "x": 32, + "y": 26, + "width": 528.0, + "height": 15.0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 474004812, + "versionNonce": 1698325641, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one-way notification latency, 8-byte payload, 2000 serialized rounds (this host)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "one-way notification latency, 8-byte payload, 2000 serialized rounds (this host)", + "lineHeight": 1.25, + "baseline": 12 + }, + { + "id": "r1474019072", + "type": "rectangle", + "x": 40, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#e8870c", + "backgroundColor": "#fff8ef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 785230571, + "versionNonce": 1859704301, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t646403410", + "type": "text", + "x": 50, + "y": 78, + "width": 77.0, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 984499045, + "versionNonce": 1568758802, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex wake", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex wake", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1186232913", + "type": "text", + "x": 50, + "y": 98, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1530180430, + "versionNonce": 229350248, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 <1 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 <1 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1242099879", + "type": "text", + "x": 50, + "y": 113, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 507104752, + "versionNonce": 1826941378, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 4 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 4 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t548154175", + "type": "text", + "x": 50, + "y": 128, + "width": 108.9, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1758060606, + "versionNonce": 1411570951, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "0-2 syscalls/round", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "0-2 syscalls/round", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1174448817", + "type": "rectangle", + "x": 190, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1263118343, + "versionNonce": 812320674, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t163773923", + "type": "text", + "x": 200, + "y": 78, + "width": 53.900000000000006, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1584372572, + "versionNonce": 922946772, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "eventfd", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "eventfd", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t1895518902", + "type": "text", + "x": 200, + "y": 98, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 278745003, + "versionNonce": 1032962983, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 2 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 2 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t728976741", + "type": "text", + "x": 200, + "y": 113, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 434055674, + "versionNonce": 707141279, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 6 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 6 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1128508299", + "type": "text", + "x": 200, + "y": 128, + "width": 114.95, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1665625264, + "versionNonce": 1851433811, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "fd: inherit or pass", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fd: inherit or pass", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r918687566", + "type": "rectangle", + "x": 340, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1962900243, + "versionNonce": 1543614382, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1008818079", + "type": "text", + "x": 350, + "y": 78, + "width": 40, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1103488884, + "versionNonce": 1176832133, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "pipe", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "pipe", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t91548779", + "type": "text", + "x": 350, + "y": 98, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 135978853, + "versionNonce": 1975424013, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 2 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 2 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1147170692", + "type": "text", + "x": 350, + "y": 113, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1967239363, + "versionNonce": 1546211449, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 5 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 5 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t841741303", + "type": "text", + "x": 350, + "y": 128, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1795548432, + "versionNonce": 1976928437, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ch. 18 channel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ch. 18 channel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r608137454", + "type": "rectangle", + "x": 490, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1015826750, + "versionNonce": 242965133, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t44023647", + "type": "text", + "x": 500, + "y": 78, + "width": 61.60000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 284371126, + "versionNonce": 832463491, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "POSIX mq", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "POSIX mq", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t116064119", + "type": "text", + "x": 500, + "y": 98, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 864755625, + "versionNonce": 1166580857, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 2 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 2 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1490205213", + "type": "text", + "x": 500, + "y": 113, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1125434390, + "versionNonce": 1508593668, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 14 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 14 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t340371372", + "type": "text", + "x": 500, + "y": 128, + "width": 102.85000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1539518292, + "versionNonce": 1947018010, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "named, priorities", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "named, priorities", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1917322228", + "type": "rectangle", + "x": 640, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 13441832, + "versionNonce": 180225821, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1458131054", + "type": "text", + "x": 650, + "y": 78, + "width": 84.7, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 607353440, + "versionNonce": 224285995, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "UNIX socket", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "UNIX socket", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t922057538", + "type": "text", + "x": 650, + "y": 98, + "width": 48.400000000000006, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 686846318, + "versionNonce": 1679328990, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 3 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 3 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t569823386", + "type": "text", + "x": 650, + "y": 113, + "width": 54.45, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 535983711, + "versionNonce": 725921617, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 12 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 12 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1603672886", + "type": "text", + "x": 650, + "y": 128, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1890585487, + "versionNonce": 1967863174, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "ch. 19 channel", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "ch. 19 channel", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r365561363", + "type": "rectangle", + "x": 790, + "y": 70, + "width": 130, + "height": 88, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "#111111", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1107790768, + "versionNonce": 1408723525, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1867893046", + "type": "text", + "x": 800, + "y": 78, + "width": 107.80000000000001, + "height": 17.5, + "angle": 0, + "strokeColor": "#ffffff", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1098421530, + "versionNonce": 1229273524, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "1 ms poll loop", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1 ms poll loop", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t527040854", + "type": "text", + "x": 800, + "y": 98, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1280515977, + "versionNonce": 1884405689, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p50 1056 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p50 1056 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t509662098", + "type": "text", + "x": 800, + "y": 113, + "width": 66.55000000000001, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1849325556, + "versionNonce": 801817660, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "p99 1083 us", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "p99 1083 us", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1176312365", + "type": "text", + "x": 800, + "y": 128, + "width": 84.7, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1624176566, + "versionNonce": 1505406197, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no kernel help", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no kernel help", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1631537451", + "type": "rectangle", + "x": 40, + "y": 200, + "width": 430, + "height": 88, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1795758519, + "versionNonce": 1963180619, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t242426448", + "type": "text", + "x": 50, + "y": 208, + "width": 238.70000000000002, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 552132834, + "versionNonce": 387296436, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "scheduler evidence (4 s window)", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "scheduler evidence (4 s window)", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t986714982", + "type": "text", + "x": 50, + "y": 228, + "width": 242.00000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 199091292, + "versionNonce": 747455068, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex watcher: 8 voluntary ctxt switches", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex watcher: 8 voluntary ctxt switches", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1681631747", + "type": "text", + "x": 50, + "y": 243, + "width": 260.15000000000003, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 850775482, + "versionNonce": 174128919, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll consumer: 3774 voluntary ctxt switches", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll consumer: 3774 voluntary ctxt switches", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "r1422948589", + "type": "rectangle", + "x": 490, + "y": 200, + "width": 430, + "height": 88, + "angle": 0, + "strokeColor": "#999999", + "backgroundColor": "#ffffff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1809947595, + "versionNonce": 1720486882, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "t1341618739", + "type": "text", + "x": 500, + "y": 208, + "width": 146.3, + "height": 17.5, + "angle": 0, + "strokeColor": "#111111", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 964960092, + "versionNonce": 1218892550, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "rusage, 2000 rounds", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rusage, 2000 rounds", + "lineHeight": 1.25, + "baseline": 14 + }, + { + "id": "t852506778", + "type": "text", + "x": 500, + "y": 228, + "width": 344.85, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1488972354, + "versionNonce": 1694187156, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "futex: 120 voluntary switches (fast path skips the sleep)", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "futex: 120 voluntary switches (fast path skips the sleep)", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1589647938", + "type": "text", + "x": 500, + "y": 243, + "width": 284.35, + "height": 13.75, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1310155057, + "versionNonce": 1029053268, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "poll: 2002 - one nap per round, by construction", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "poll: 2002 - one nap per round, by construction", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "a669230746", + "type": "arrow", + "x": 170, + "y": 114, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 898253255, + "versionNonce": 820096302, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1239669892", + "type": "arrow", + "x": 320, + "y": 114, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1888200939, + "versionNonce": 1065049136, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a776438909", + "type": "arrow", + "x": 470, + "y": 114, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1349987192, + "versionNonce": 603377639, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1779603200", + "type": "arrow", + "x": 620, + "y": 114, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#555555", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1483912707, + "versionNonce": 166384211, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "a1289420184", + "type": "arrow", + "x": 770, + "y": 114, + "width": 20, + "height": 0, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "seed": 1188533951, + "versionNonce": 1133930480, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 20, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "t144196100", + "type": "text", + "x": 780.0, + "y": 100.0, + "width": 40, + "height": 13.75, + "angle": 0, + "strokeColor": "#b8650a", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1602005978, + "versionNonce": 193683099, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "~350x", + "fontSize": 11, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "~350x", + "lineHeight": 1.25, + "baseline": 11 + }, + { + "id": "t1731282318", + "type": "text", + "x": 40, + "y": 175, + "width": 695.75, + "height": 14.375, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1588623114, + "versionNonce": 1235363704, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Latency climbs left to right; every measured channel except the poll loop lands within one order of magnitude.", + "fontSize": 11.5, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Latency climbs left to right; every measured channel except the poll loop lands within one order of magnitude.", + "lineHeight": 1.25, + "baseline": 11.5 + }, + { + "id": "t1866167969", + "type": "text", + "x": 40, + "y": 335, + "width": 771.6500000000001, + "height": 14.375, + "angle": 0, + "strokeColor": "#1d1d1d", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 335971210, + "versionNonce": 221670838, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Numbers are plainly bounded by this host (idle 8-core/16-thread Fedora 44 box); rankings, not absolutes, are the takeaway.", + "fontSize": 11.5, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Numbers are plainly bounded by this host (idle 8-core/16-thread Fedora 44 box); rankings, not absolutes, are the takeaway.", + "lineHeight": 1.25, + "baseline": 11.5 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": null + }, + "files": {} +} \ No newline at end of file diff --git a/assets/diagrams/20-ipc-matrix.svg b/assets/diagrams/20-ipc-matrix.svg new file mode 100644 index 0000000..5acad5b --- /dev/null +++ b/assets/diagrams/20-ipc-matrix.svg @@ -0,0 +1,52 @@ + + + +one-way notification latency, 8-byte payload, 2000 serialized rounds (this host) + +futex wake +p50 <1 us +p99 4 us +0-2 syscalls/round + +eventfd +p50 2 us +p99 6 us +fd: inherit or pass + +pipe +p50 2 us +p99 5 us +ch. 18 channel + +POSIX mq +p50 2 us +p99 14 us +named, priorities + +UNIX socket +p50 3 us +p99 12 us +ch. 19 channel + +1 ms poll loop +p50 1056 us +p99 1083 us +no kernel help + +scheduler evidence (4 s window) +futex watcher: 8 voluntary ctxt switches +poll consumer: 3774 voluntary ctxt switches + +rusage, 2000 rounds +futex: 120 voluntary switches (fast path skips the sleep) +poll: 2002 - one nap per round, by construction + + + + + +~350x +~350x +Latency climbs left to right; every measured channel except the poll loop lands within one order of magnitude. +Numbers are plainly bounded by this host (idle 8-core/16-thread Fedora 44 box); rankings, not absolutes, are the takeaway. + \ No newline at end of file diff --git a/assets/diagrams/README.md b/assets/diagrams/README.md index 48bdabe..1a245c9 100644 --- a/assets/diagrams/README.md +++ b/assets/diagrams/README.md @@ -21,3 +21,20 @@ here as you add them: | `09-event-loop-anatomy` | 9 | One epoll loop over three event sources — inotify, timerfd, signalfd — with the kernel eventpoll interest list and the readiness path back into epoll_wait | | `06-three-concurrency-models` | 6 | parhash's producer/queue/workers/cancellation shape in all three vocabularies, with observed /proc thread counts | | `06-cancellation-drain` | 6 | The SIGINT drain sequence — one flag flips, walker and workers wind down, in-flight hashes finish, exit 130 | +| `16-mapping-page-cache` | 16 | Two processes' MAP_SHARED mappings converging on one set of page-cache pages, msync vs writeback to disk, and a MAP_PRIVATE view forking its copy on first write | +| `16-shmkv-layout` | 16 | The shmkv v0 on-disk format byte for byte — magic, u32 LE slot_count, 256-byte slots split 64/192 — with the three languages reading the same bytes | +| `17-arena-vs-gc` | 17 | Three reclamation strategies side by side — free-one-by-one through glibc malloc, arena/pool batch release, and the Go GC's pacer — annotated with real allocbench peak_rss and gc_cycles numbers | +| `11-process-lifecycle-states` | 11 | Process states as ps STAT letters — running, sleeping, stopped, zombie — and the wait4/waitid reap that frees the pid, annotated with the chapter's live zombie window | +| `11-fork-exec-wait-seq` | 11 | pmon's fork → exec → wait sequence with the CLOEXEC self-pipe carrying the exec verdict, and the clone flavors Go and Rust use for the same dance | +| `19-uds-control-plane` | 19 | pmon v5's supervise/pmctl control plane over a filesystem UNIX stream socket — listener, accepted fd with SO_PEERCRED, and the connected socket-inode pair ss -xp reported | +| `19-scm-rights-transfer` | 19 | SCM_RIGHTS crossing the socket: sender slot 3 and receiver slot 4 pointing at one shared open file description (pos 47, inode 95934), with the real sendmsg/recvmsg cmsg values from the chapter run | +| `12-signal-delivery-paths` | 12 | Disposition, per-thread mask, and pending set in the kernel, feeding the three consumption paths pmon uses — signalfd reads, Go runtime channels, and the self-pipe doorbell | +| `12-supervisor-state-machine` | 12 | pmon supervise's two live states (child running, backoff wait) and the exits, with SIGTERM/SIGINT, SIGHUP reload, and the doubling backoff as transitions | +| `14-credential-sets-caps` | 14 | The three-uid state machine and the five capability sets across pmon's drop — KEEPCAPS → setresuid → capset → AMBIENT_RAISE → execve beside the negative control, annotated with the real /proc status values from systems-target | +| `15-address-space-map` | 15 | One x86-64 process address space top to bottom — [vsyscall], [stack], the mmap region with the 64 MiB anonymous allocation, [heap], data, text — with real addresses and growth arrows | +| `15-fault-flow` | 15 | One touched page's journey — MMU miss, trap, VMA lookup — fanning out to SIGSEGV, anonymous minor fault, page-cache minor fault with fault-around, and major fault, with measured counts | +| `20-futex-wait-wake` | 20 | One shmkv wake end to end — watcher parks in FUTEX_WAIT on the mapped word, writer publishes under the seqlock and calls FUTEX_WAKE, kernel wait queue keyed by (inode, offset) hands the watcher back — with the EAGAIN lost-wakeup guard | +| `20-ipc-matrix` | 20 | The coordination ladder as measured on this host — futex, eventfd, pipe, POSIX mq, UNIX socket, and the 1 ms poll loop, with bench p50/p99 and the voluntary context-switch evidence | +| `13-pid-reuse-race-pidfd` | 13 | The five-step pid-reuse race timeline — spawn, crash, reap, recycle, kill(2) hitting a stranger — above the four-step pidfd lane that pins identity to an fd, annotated with the real fdinfo Pid: and strace evidence from pmon v2 | +| `18-pipe-internals` | 18 | One live pipe as the kernel sees it — write end fd 1 and read end fd 5 around the 16-slot ring of pages, with the measured 65536 B capacity, the F_SETPIPE_SZ page rounding, and what PIPE_BUF atomicity does and does not promise | +| `18-log-topology` | 18 | pmon v4's full log pipeline — child stdio through two O_CLOEXEC pipes into the supervisor's poll loop and the framed log, then tail splicing file→FIFO to a detachable cat reader, annotated with the run's real fds, inodes, flags, and splice return values | diff --git a/examples/11-process-lifecycle/README.md b/examples/11-process-lifecycle/README.md new file mode 100644 index 0000000..f540e9a --- /dev/null +++ b/examples/11-process-lifecycle/README.md @@ -0,0 +1,114 @@ +# 11-process-lifecycle — pmon + +A minimal process monitor, written three times — C++23, Go, and Rust — for +the process lifecycle chapter: spawn a command, wait for it, and report the +two things a supervisor always needs, *how the child died* and *what it cost*. + +``` +pmon run -- CMD [ARGS...] +``` + +## Behavior contract (identical in all three languages) + +| Outcome | Output | Exit | +| --- | --- | --- | +| Child exits `s` | stdout: `pmon: pid

exited status ` + rusage line | `s` | +| Child killed by signal `n` | stdout: `pmon: pid

killed by signal ()` + rusage line | `128+n` | +| exec fails | stderr: `pmon: exec : ` (no report lines) | 127 | +| Bad usage | stderr: `usage: pmon run -- CMD [ARGS...]` | 2 | + +The rusage line has one fixed shape (milliseconds always three digits): + +``` +pmon: rusage maxrss=KB user=.s sys=.s wall=ms +``` + +Sample session (any of the three binaries): + +``` +$ pmon run -- sh -c 'kill -TERM $$' +pmon: pid 181042 killed by signal 15 (TERM) +pmon: rusage maxrss=3864KB user=0.000s sys=0.001s wall=1ms +$ echo $? +143 +$ pmon run -- ./nonexistent +pmon: exec ./nonexistent: No such file or directory +$ echo $? +127 +``` + +Design decisions shared by all three: + +- **The exit code mirrors the child** — the shell convention (`status`, or + `128+signal`, or `127` for "could not exec"), so pmon composes with `&&`, + `make`, and CI the same way the shell itself would. +- **Signal names come from a hand-rolled table** (`15 -> TERM`, `9 -> KILL`, + ..., `SIG` beyond 31), not `strsignal(3)`, whose text is + locale-dependent prose ("Terminated") and differs across runtimes. +- **`` is `strerror(3)`-shaped** (`No such file or directory`). Go's + errno strings are lowercase, so the first letter is capitalized, and a PATH + miss (`exec.ErrNotFound`) is rendered as ENOENT — exactly what `execvp(3)` + reports in the other two. +- **Child stdio is inherited**, so the child's own output interleaves + naturally and the report lines land after the wait. + +### How each language spells the same lifecycle + +- **C++23** (`cpp/`): the chapter's two-step model spelled out — + `fork(2)` + `execvp(3)` in the child, `waitpid(2)` + + `getrusage(RUSAGE_CHILDREN)` in the parent (`posix_spawn(3)` is discussed + in the chapter; fork is used here for teaching). Exec failure travels over + a self-pipe: both ends `O_CLOEXEC`, so a successful exec closes the child's + copy and the parent reads EOF, while a failed exec ships `errno` up before + `_exit(127)`. A move-only RAII `Fd` owns each pipe end; fallible steps + return `std::expected`; output is `std::println`. +- **Go** (`go/`): `os/exec` — whose `Start` performs the same + fork/exec-with-status-pipe dance internally, surfacing exec failure as an + error before any child ran. The wait-side facts come from + `os.ProcessState`: `Sys()` is the raw `syscall.WaitStatus`, `SysUsage()` + the `*syscall.Rusage` that `wait4(2)` filled in for exactly this child. + Errors are wrapped (`fmt.Errorf` + `%w`) and inspected with + `errors.As`/`errors.Is`. +- **Rust 2024** (`rust/`): `std::process::Command::spawn` (again the same + CLOEXEC status-pipe trick under the hood, hence `Err` on exec failure), + then `wait4(2)` via `libc` (re-exported by nix) so a single call yields + both the status and the rusage; `nix::sys::wait::WaitStatus::from_raw` + decodes the raw status and `nix::errno::Errno::desc` renders the + `strerror(3)` text. The wait wrapper is `io::Result` + `?`, with EINTR + retried. + +One semantic wrinkle worth noticing: C++ asks for `RUSAGE_CHILDREN` (all +waited-for children — equivalent here because there is exactly one), while Go +and Rust read the per-child rusage that `wait4(2)` returns. Same numbers, two +kernel interfaces. + +## The demo contract + +Each language directory has a `demo.sh` with the book-wide interface: + +- `./demo.sh build` — build only +- `./demo.sh run [args]` — run the built binary with `args` + (with env `TARGET` set, deploys to that lab VM via + `scripts/lab/deploy-to-vm.sh` instead of running locally) +- `./demo.sh` — build, then a local walkthrough of the four child fates: + clean exit, `exit 42`, death by SIGTERM (exit 143), exec failure (127) + +The top-level `demo.sh` dispatches: `./demo.sh cpp run -- /bin/true`, +`./demo.sh all`, `./demo.sh build`. + +## Verification + +`verify.lua` (run per language with `LSP_LANG` set) asserts observable +behavior, not exit-0: + +- `/bin/true` / `/bin/false` / `sh -c 'exit 42'` report the status and + mirror it in pmon's own exit code; +- the pid in the fate line is the *real* child pid — the child echoes `$$` + and the two must agree; +- `kill -TERM $$` reports `killed by signal 15 (TERM)` with exit 143, + `kill -KILL $$` reports `signal 9 (KILL)` with exit 137; +- `./nonexistent` exits 127 with `pmon: exec ./nonexistent: No such file or + directory` and *no* fate or rusage lines; +- the rusage line matches the exact shape, wall time covers a real 200 ms + sleep, maxrss is nonzero, and a shell busy loop registers child user CPU; +- no arguments prints the usage line and exits 2. diff --git a/examples/11-process-lifecycle/cpp/.clang-format b/examples/11-process-lifecycle/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/11-process-lifecycle/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/11-process-lifecycle/cpp/.clang-tidy b/examples/11-process-lifecycle/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/11-process-lifecycle/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/11-process-lifecycle/cpp/CMakeLists.txt b/examples/11-process-lifecycle/cpp/CMakeLists.txt new file mode 100644 index 0000000..c945061 --- /dev/null +++ b/examples/11-process-lifecycle/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/11-process-lifecycle/cpp/CMakePresets.json b/examples/11-process-lifecycle/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/11-process-lifecycle/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/11-process-lifecycle/cpp/demo.sh b/examples/11-process-lifecycle/cpp/demo.sh new file mode 100755 index 0000000..c61a797 --- /dev/null +++ b/examples/11-process-lifecycle/cpp/demo.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +# Local walkthrough of the child fates pmon distinguishes: clean exit, +# nonzero exit, death by signal, exec failure. Runs the binary directly. +demo() { + local rc + echo "== pmon run -- /bin/true (expect exit 0) ==" + rc=0; "./$BIN" run -- /bin/true || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'exit 42' (expect exit 42) ==" + rc=0; "./$BIN" run -- sh -c 'exit 42' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'kill -TERM \$\$' (expect exit 143) ==" + rc=0; "./$BIN" run -- sh -c 'kill -TERM $$' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- ./no-such-binary (expect exit 127) ==" + rc=0; "./$BIN" run -- ./no-such-binary || rc=$? + echo "exit=$rc" +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + demo + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/11-process-lifecycle/cpp/src/main.cpp b/examples/11-process-lifecycle/cpp/src/main.cpp new file mode 100644 index 0000000..e22b75e --- /dev/null +++ b/examples/11-process-lifecycle/cpp/src/main.cpp @@ -0,0 +1,201 @@ +// pmon run -- CMD [ARGS...] — minimal process monitor for the process +// lifecycle chapter: fork(2) + execvp(3) the command, waitpid(2) for it, +// then report its fate and resource usage. +// +// pmon: pid

exited status child exited normally +// pmon: pid

killed by signal () child died on a signal +// pmon: rusage maxrss=KB user=.s sys=.s wall=ms +// +// pmon's own exit code mirrors the child: the exit status, or 128+signal. +// If exec itself fails, stderr gets "pmon: exec : " and pmon +// exits 127 with no report lines (there was no child run to report on). +// +// The chapter discusses posix_spawn(3); fork/exec is used here because the +// two-step model — and the CLOEXEC self-pipe that reports exec failure back +// to the parent — is exactly what the chapter teaches. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +using namespace std::string_view_literals; + +[[nodiscard]] std::error_code last_error() { + return {errno, std::system_category()}; +} + +// Move-only RAII owner of a file descriptor. +class Fd { +public: + Fd() = default; + explicit Fd(int fd) : fd_{fd} {} + ~Fd() { reset(); } + Fd(Fd&& other) noexcept : fd_{other.release()} {} + Fd& operator=(Fd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = other.release(); + } + return *this; + } + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + + [[nodiscard]] int get() const { return fd_; } + int release() { return std::exchange(fd_, -1); } + void reset() { + if (fd_ >= 0) { + ::close(fd_); + fd_ = -1; + } + } + +private: + int fd_ = -1; +}; + +// Self-pipe for exec-failure reporting: both ends O_CLOEXEC, so a successful +// exec closes the child's copy and the parent reads EOF. +struct ExecPipe { + Fd read_end; + Fd write_end; +}; + +[[nodiscard]] std::expected make_exec_pipe() { + std::array fds{}; + if (::pipe2(fds.data(), O_CLOEXEC) != 0) { + return std::unexpected(last_error()); + } + return ExecPipe{Fd{fds[0]}, Fd{fds[1]}}; +} + +[[nodiscard]] std::expected wait_child(pid_t pid) { + int status = 0; + while (::waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) { + return std::unexpected(last_error()); + } + } + return status; +} + +[[nodiscard]] std::expected children_rusage() { + rusage ru{}; + if (::getrusage(RUSAGE_CHILDREN, &ru) != 0) { + return std::unexpected(last_error()); + } + return ru; +} + +// Names as printed in "killed by signal ()". A hand-rolled table +// (identical across the three implementations) rather than strsignal(3), +// whose text is locale-dependent prose ("Terminated"). +[[nodiscard]] std::string signal_name(int sig) { + static constexpr std::array names{ + "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", + "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM", + "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS"}; + if (sig >= 1 && sig < static_cast(names.size())) { + return std::string{names[sig]}; + } + return std::format("SIG{}", sig); +} + +// ".s" from a timeval, e.g. "0.004s". +[[nodiscard]] std::string format_cpu(const timeval& tv) { + return std::format("{}.{:03}s", tv.tv_sec, tv.tv_usec / 1000); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 4 || argv[1] != "run"sv || argv[2] != "--"sv) { + std::println(stderr, "usage: pmon run -- CMD [ARGS...]"); + return 2; + } + char** child_argv = argv + 3; // already NULL-terminated by the C runtime + + auto pipe = make_exec_pipe(); + if (!pipe) { + std::println(stderr, "pmon: pipe2: {}", pipe.error().message()); + return 1; + } + + const auto start = std::chrono::steady_clock::now(); + const pid_t pid = ::fork(); + if (pid < 0) { + std::println(stderr, "pmon: fork: {}", last_error().message()); + return 1; + } + + if (pid == 0) { + // Child. On exec success the O_CLOEXEC pipe closes itself; on failure + // ship errno to the parent and die with the shell's 127 convention. + pipe->read_end.reset(); + ::execvp(child_argv[0], child_argv); + const int exec_errno = errno; + (void)::write(pipe->write_end.get(), &exec_errno, sizeof exec_errno); + _exit(127); + } + + // Parent: drop our write end so read() returns the moment exec resolves. + pipe->write_end.reset(); + int exec_errno = 0; + ssize_t nread = 0; + do { + nread = ::read(pipe->read_end.get(), &exec_errno, sizeof exec_errno); + } while (nread < 0 && errno == EINTR); + + const auto status = wait_child(pid); + const auto wall = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start); + if (!status) { + std::println(stderr, "pmon: waitpid: {}", status.error().message()); + return 1; + } + + if (nread == static_cast(sizeof exec_errno)) { + const std::error_code ec{exec_errno, std::system_category()}; + std::println(stderr, "pmon: exec {}: {}", child_argv[0], ec.message()); + return 127; + } + + const auto ru = children_rusage(); + if (!ru) { + std::println(stderr, "pmon: getrusage: {}", ru.error().message()); + return 1; + } + + int code = 1; + if (WIFEXITED(*status)) { + code = WEXITSTATUS(*status); + std::println("pmon: pid {} exited status {}", pid, code); + } else if (WIFSIGNALED(*status)) { + const int sig = WTERMSIG(*status); + code = 128 + sig; + std::println("pmon: pid {} killed by signal {} ({})", pid, sig, signal_name(sig)); + } else { + std::println(stderr, "pmon: unexpected wait status {:#x}", *status); + return 1; + } + + std::println("pmon: rusage maxrss={}KB user={} sys={} wall={}ms", ru->ru_maxrss, + format_cpu(ru->ru_utime), format_cpu(ru->ru_stime), wall.count()); + return code; +} diff --git a/examples/11-process-lifecycle/demo.sh b/examples/11-process-lifecycle/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/11-process-lifecycle/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/11-process-lifecycle/go/demo.sh b/examples/11-process-lifecycle/go/demo.sh new file mode 100755 index 0000000..86392bd --- /dev/null +++ b/examples/11-process-lifecycle/go/demo.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +# Local walkthrough of the child fates pmon distinguishes: clean exit, +# nonzero exit, death by signal, exec failure. Runs the binary directly. +demo() { + local rc + echo "== pmon run -- /bin/true (expect exit 0) ==" + rc=0; "./$BIN" run -- /bin/true || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'exit 42' (expect exit 42) ==" + rc=0; "./$BIN" run -- sh -c 'exit 42' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'kill -TERM \$\$' (expect exit 143) ==" + rc=0; "./$BIN" run -- sh -c 'kill -TERM $$' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- ./no-such-binary (expect exit 127) ==" + rc=0; "./$BIN" run -- ./no-such-binary || rc=$? + echo "exit=$rc" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; demo ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/11-process-lifecycle/go/go.mod b/examples/11-process-lifecycle/go/go.mod new file mode 100644 index 0000000..227e33a --- /dev/null +++ b/examples/11-process-lifecycle/go/go.mod @@ -0,0 +1,5 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/11-process-lifecycle/go + +go 1.26 + +toolchain go1.26.5 diff --git a/examples/11-process-lifecycle/go/main.go b/examples/11-process-lifecycle/go/main.go new file mode 100644 index 0000000..034f672 --- /dev/null +++ b/examples/11-process-lifecycle/go/main.go @@ -0,0 +1,137 @@ +// pmon run -- CMD [ARGS...] — minimal process monitor for the process +// lifecycle chapter: spawn CMD, wait for it, report its fate and rusage. +// +// pmon: pid

exited status child exited normally +// pmon: pid

killed by signal () child died on a signal +// pmon: rusage maxrss=KB user=.s sys=.s wall=ms +// +// pmon's own exit code mirrors the child: the exit status, or 128+signal. +// If exec itself fails, stderr gets "pmon: exec : " and pmon +// exits 127 with no report lines. +// +// Where C++ forks and execs by hand, Go's os/exec does the same dance +// internally (fork/exec with a CLOEXEC status pipe — see syscall.forkExec); +// the exec failure that the C++ version ships over its self-pipe surfaces +// here as an error from cmd.Start. The wait-side facts come from +// os.ProcessState: Sys() is the raw wait status, SysUsage() the rusage that +// wait4(2) filled in for exactly this child. +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "syscall" + "time" + "unicode" +) + +// Index 1..31; identical to the C++ and Rust tables. Not strsignal(3) prose. +var sigNames = [...]string{ + "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", + "FPE", "KILL", "USR1", "SEGV", "USR2", "PIPE", "ALRM", "TERM", + "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", +} + +func sigName(n int) string { + if n >= 1 && n < len(sigNames) { + return sigNames[n] + } + return fmt.Sprintf("SIG%d", n) +} + +// ".s" from a wait4 timeval, e.g. "0.004s". +func formatCPU(tv syscall.Timeval) string { + return fmt.Sprintf("%d.%03ds", tv.Sec, tv.Usec/1000) +} + +func capitalize(s string) string { + r := []rune(s) + if len(r) > 0 { + r[0] = unicode.ToUpper(r[0]) + } + return string(r) +} + +// reason renders a spawn failure in strerror(3) shape. Go's errno strings are +// lowercase ("no such file or directory"), so the first letter is capitalized +// to match what the C++ and Rust versions print; a PATH miss (exec.ErrNotFound) +// is what execvp(3) reports as ENOENT. +func reason(err error) string { + var errno syscall.Errno + if errors.As(err, &errno) { + return capitalize(errno.Error()) + } + if errors.Is(err, exec.ErrNotFound) { + return "No such file or directory" + } + return capitalize(err.Error()) +} + +// waitChild waits for the started command; a child that ran and failed is not +// an error here (exec.ExitError carries the ProcessState we want), while a +// wait-machinery failure is wrapped and propagated. +func waitChild(cmd *exec.Cmd) (*os.ProcessState, error) { + err := cmd.Wait() + var exitErr *exec.ExitError + if err != nil && !errors.As(err, &exitErr) { + return nil, fmt.Errorf("wait: %w", err) + } + return cmd.ProcessState, nil +} + +func monitor(name string, args []string) int { + cmd := exec.Command(name, args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + + start := time.Now() + if err := cmd.Start(); err != nil { + fmt.Fprintf(os.Stderr, "pmon: exec %s: %s\n", name, reason(err)) + return 127 + } + state, err := waitChild(cmd) + wall := time.Since(start) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + + ws, ok := state.Sys().(syscall.WaitStatus) + if !ok { + fmt.Fprintln(os.Stderr, "pmon: no wait status for child") + return 1 + } + ru, ok := state.SysUsage().(*syscall.Rusage) + if !ok { + fmt.Fprintln(os.Stderr, "pmon: no rusage for child") + return 1 + } + + var code int + switch { + case ws.Exited(): + code = ws.ExitStatus() + fmt.Printf("pmon: pid %d exited status %d\n", state.Pid(), code) + case ws.Signaled(): + sig := int(ws.Signal()) + code = 128 + sig + fmt.Printf("pmon: pid %d killed by signal %d (%s)\n", state.Pid(), sig, sigName(sig)) + default: + fmt.Fprintf(os.Stderr, "pmon: unexpected wait status %#x\n", uint32(ws)) + return 1 + } + + fmt.Printf("pmon: rusage maxrss=%dKB user=%s sys=%s wall=%dms\n", + ru.Maxrss, formatCPU(ru.Utime), formatCPU(ru.Stime), wall.Milliseconds()) + return code +} + +func main() { + if len(os.Args) < 4 || os.Args[1] != "run" || os.Args[2] != "--" { + fmt.Fprintln(os.Stderr, "usage: pmon run -- CMD [ARGS...]") + os.Exit(2) + } + os.Exit(monitor(os.Args[3], os.Args[4:])) +} diff --git a/examples/11-process-lifecycle/rust/Cargo.lock b/examples/11-process-lifecycle/rust/Cargo.lock new file mode 100644 index 0000000..c825f1e --- /dev/null +++ b/examples/11-process-lifecycle/rust/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "pmon" +version = "0.1.0" +dependencies = [ + "nix", +] diff --git a/examples/11-process-lifecycle/rust/Cargo.toml b/examples/11-process-lifecycle/rust/Cargo.toml new file mode 100644 index 0000000..b741942 --- /dev/null +++ b/examples/11-process-lifecycle/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pmon" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +nix = { version = "0.31", features = ["process"] } diff --git a/examples/11-process-lifecycle/rust/demo.sh b/examples/11-process-lifecycle/rust/demo.sh new file mode 100755 index 0000000..ec1c7c7 --- /dev/null +++ b/examples/11-process-lifecycle/rust/demo.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +# Local walkthrough of the child fates pmon distinguishes: clean exit, +# nonzero exit, death by signal, exec failure. Runs the binary directly. +demo() { + local rc + echo "== pmon run -- /bin/true (expect exit 0) ==" + rc=0; "./$BIN" run -- /bin/true || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'exit 42' (expect exit 42) ==" + rc=0; "./$BIN" run -- sh -c 'exit 42' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- sh -c 'kill -TERM \$\$' (expect exit 143) ==" + rc=0; "./$BIN" run -- sh -c 'kill -TERM $$' || rc=$? + echo "exit=$rc" + + echo "== pmon run -- ./no-such-binary (expect exit 127) ==" + rc=0; "./$BIN" run -- ./no-such-binary || rc=$? + echo "exit=$rc" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; demo ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/11-process-lifecycle/rust/rust-toolchain.toml b/examples/11-process-lifecycle/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/11-process-lifecycle/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/11-process-lifecycle/rust/src/main.rs b/examples/11-process-lifecycle/rust/src/main.rs new file mode 100644 index 0000000..697abc8 --- /dev/null +++ b/examples/11-process-lifecycle/rust/src/main.rs @@ -0,0 +1,132 @@ +//! pmon run -- CMD [ARGS...] — minimal process monitor for the process +//! lifecycle chapter: spawn CMD, wait for it, report its fate and rusage. +//! +//! ```text +//! pmon: pid

exited status child exited normally +//! pmon: pid

killed by signal () child died on a signal +//! pmon: rusage maxrss=KB user=.s sys=.s wall=ms +//! ``` +//! +//! pmon's own exit code mirrors the child: the exit status, or 128+signal. +//! If exec itself fails, stderr gets "pmon: exec : " and pmon +//! exits 127 with no report lines. +//! +//! `std::process::Command` does the fork/exec (with the same CLOEXEC +//! status-pipe trick the C++ version spells out, which is why an exec failure +//! comes back as an `Err` from `spawn`). The wait side goes through wait4(2) +//! directly so one call yields both the wait status and the child's rusage; +//! nix decodes the raw status and renders errno in strerror(3) shape. + +use std::env; +use std::io; +use std::process::Command; +use std::time::Instant; + +use nix::errno::Errno; +use nix::libc; +use nix::sys::wait::WaitStatus; +use nix::unistd::Pid; + +/// Index 1..31; identical to the C++ and Go tables. Not strsignal(3) prose. +const SIG_NAMES: [&str; 32] = [ + "", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", "USR2", + "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG", + "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", +]; + +fn sig_name(n: i32) -> String { + match usize::try_from(n).ok().and_then(|i| SIG_NAMES.get(i)) { + Some(name) if n >= 1 => (*name).to_string(), + _ => format!("SIG{n}"), + } +} + +/// ".s" from a wait4 timeval, e.g. "0.004s". +fn format_cpu(tv: libc::timeval) -> String { + format!("{}.{:03}s", tv.tv_sec, tv.tv_usec / 1000) +} + +/// strerror(3)-shaped reason for a spawn failure; `Errno::desc` carries the +/// classic text ("No such file or directory") without io::Error's +/// "(os error 2)" suffix. +fn reason(err: &io::Error) -> String { + match err.raw_os_error() { + Some(code) => Errno::from_raw(code).desc().to_string(), + None => err.to_string(), + } +} + +/// wait4(2): reap `pid`, yielding the decoded wait status and its rusage. +fn wait4(pid: i32) -> io::Result<(WaitStatus, libc::rusage)> { + let mut raw_status = 0i32; + // SAFETY: zero-filled rusage is a valid out-param for wait4(2). + let mut rusage: libc::rusage = unsafe { std::mem::zeroed() }; + loop { + // SAFETY: raw_status and rusage are exclusively borrowed, valid + // out-pointers for the duration of the call. + let rc = unsafe { libc::wait4(pid, &mut raw_status, 0, &mut rusage) }; + if rc == pid { + let status = WaitStatus::from_raw(Pid::from_raw(pid), raw_status) + .map_err(|errno| io::Error::from_raw_os_error(errno as i32))?; + return Ok((status, rusage)); + } + let err = io::Error::last_os_error(); + if err.raw_os_error() != Some(libc::EINTR) { + return Err(err); + } + } +} + +fn monitor(name: &str, args: &[String]) -> i32 { + let start = Instant::now(); + let child = match Command::new(name).args(args).spawn() { + Ok(child) => child, + Err(err) => { + eprintln!("pmon: exec {name}: {}", reason(&err)); + return 127; + } + }; + let pid = child.id() as i32; + + let (status, rusage) = match wait4(pid) { + Ok(pair) => pair, + Err(err) => { + eprintln!("pmon: wait4: {err}"); + return 1; + } + }; + let wall = start.elapsed().as_millis(); + + let code = match status { + WaitStatus::Exited(_, code) => { + println!("pmon: pid {pid} exited status {code}"); + code + } + WaitStatus::Signaled(_, signal, _) => { + let n = signal as i32; + println!("pmon: pid {pid} killed by signal {n} ({})", sig_name(n)); + 128 + n + } + other => { + eprintln!("pmon: unexpected wait status: {other:?}"); + return 1; + } + }; + + println!( + "pmon: rusage maxrss={}KB user={} sys={} wall={wall}ms", + rusage.ru_maxrss, + format_cpu(rusage.ru_utime), + format_cpu(rusage.ru_stime), + ); + code +} + +fn main() { + let args: Vec = env::args().collect(); + if args.len() < 4 || args[1] != "run" || args[2] != "--" { + eprintln!("usage: pmon run -- CMD [ARGS...]"); + std::process::exit(2); + } + std::process::exit(monitor(&args[3], &args[4..])); +} diff --git a/examples/11-process-lifecycle/verify.lua b/examples/11-process-lifecycle/verify.lua new file mode 100644 index 0000000..6ebc440 --- /dev/null +++ b/examples/11-process-lifecycle/verify.lua @@ -0,0 +1,119 @@ +-- Verify pmon (11-process-lifecycle): behavioral checks for the process +-- lifecycle monitor. Invoked from the example directory with LSP_LANG +-- (cpp|go|rust) and EXAMPLE_DIR set. +-- +-- What a passing run proves: pmon reports the child's REAL pid (matched +-- against $$ echoed by the child itself); exit statuses 0/1/42 are reported +-- and mirrored in pmon's own exit code; SIGTERM/SIGKILL deaths are reported +-- as "killed by signal ()" with exit 128+n; the rusage line has the +-- exact maxrss/user/sys/wall shape with plausible values (wall covers a real +-- 200 ms sleep, maxrss is nonzero, user CPU registers for a busy loop); and +-- an exec failure prints only "pmon: exec : " on stderr with +-- exit 127 — no fate or rusage lines for a child that never ran. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" +-- The demo contract forwards args verbatim, so the binary's own CLI +-- ("run -- CMD [ARGS...]") appears after the demo.sh "run" verb. +local pmon = demo .. " run --" +-- maxrss=KB user=.s sys=.s wall=ms, ms always 3 digits. +local rusage_pat = + "pmon: rusage maxrss=%d+KB user=%d+%.%d%d%ds sys=%d+%.%d%d%ds wall=%d+ms" + +-- Boolean assertions routed through expect_exit so they are counted. +local function expect_true(cond, label) + checks.expect_exit({ exit = cond and 0 or 1 }, 0, label) +end + +-- 1. Clean exit: reported and mirrored, rusage line in exact shape. +local r = checks.run(pmon .. " /bin/true") +checks.expect_exit(r, 0, lang .. ": /bin/true mirrors exit 0") +checks.expect_match(r.out, "pmon: pid %d+ exited status 0", + lang .. ": reports clean exit") +checks.expect_match(r.out, rusage_pat, lang .. ": rusage line has exact shape") + +-- 2. Nonzero exit: /bin/false -> 1. +r = checks.run(pmon .. " /bin/false") +checks.expect_exit(r, 1, lang .. ": /bin/false mirrors exit 1") +checks.expect_match(r.out, "pmon: pid %d+ exited status 1", + lang .. ": reports status 1") + +-- 3. Arbitrary status is mirrored verbatim. +r = checks.run(pmon .. " sh -c 'exit 42'") +checks.expect_exit(r, 42, lang .. ": exit 42 is mirrored") +checks.expect_match(r.out, "pmon: pid %d+ exited status 42", + lang .. ": reports status 42") + +-- 4. The reported pid is the actual child pid: the child prints its own $$ +-- and pmon's fate line must name the same process. +r = checks.run(pmon .. " sh -c 'echo childpid=$$'") +checks.expect_exit(r, 0, lang .. ": child echoing its pid exits 0") +local child_pid = r.out:match("childpid=(%d+)") +expect_true(child_pid ~= nil, lang .. ": child stdout passes through pmon") +if child_pid then + checks.expect_match(r.out, "pmon: pid " .. child_pid .. " exited status 0", + lang .. ": fate line names the real child pid") +end + +-- 5. Death by SIGTERM: named, numbered, mirrored as 128+15. +r = checks.run(pmon .. " sh -c 'kill -TERM $$'") +checks.expect_exit(r, 143, lang .. ": SIGTERM death mirrors exit 143") +checks.expect_match(r.out, "pmon: pid %d+ killed by signal 15 %(TERM%)", + lang .. ": reports killed by signal 15 (TERM)") +checks.expect_match(r.out, rusage_pat, + lang .. ": rusage still reported for a signaled child") + +-- 6. Death by SIGKILL: 128+9. +r = checks.run(pmon .. " sh -c 'kill -KILL $$'") +checks.expect_exit(r, 137, lang .. ": SIGKILL death mirrors exit 137") +checks.expect_match(r.out, "pmon: pid %d+ killed by signal 9 %(KILL%)", + lang .. ": reports killed by signal 9 (KILL)") + +-- 7. Exec failure: strerror-shaped reason on stderr, exit 127, and no +-- fate/rusage lines (there was no child run to report on). +r = checks.run(pmon .. " ./nonexistent") +checks.expect_exit(r, 127, lang .. ": exec failure exits 127") +checks.expect_match(r.out, "pmon: exec %./nonexistent: No such file or directory", + lang .. ": exec failure names cmd and strerror reason") +expect_true(r.out:find("exited status") == nil, + lang .. ": no fate line on exec failure") +expect_true(r.out:find("rusage") == nil, + lang .. ": no rusage line on exec failure") + +-- 8. Wall time is real: a 200 ms sleep must show up in wall=ms. +r = checks.run(pmon .. " sh -c 'sleep 0.2'") +checks.expect_exit(r, 0, lang .. ": sleep 0.2 exits 0") +local wall = tonumber(r.out:match("wall=(%d+)ms")) +expect_true(wall ~= nil and wall >= 150, + lang .. ": wall time covers the 200 ms sleep (got " .. tostring(wall) .. "ms)") +expect_true(wall ~= nil and wall < 10000, + lang .. ": wall time is not absurd for a 200 ms sleep") + +-- 9. rusage values are the child's, and real: maxrss nonzero, and a shell +-- busy loop registers user CPU (sec*1000+ms > 20ms). +r = checks.run(pmon .. " sh -c 'i=0; while [ $i -lt 200000 ]; do i=$((i+1)); done'") +checks.expect_exit(r, 0, lang .. ": busy loop exits 0") +local rss = tonumber(r.out:match("maxrss=(%d+)KB")) +expect_true(rss ~= nil and rss > 0, lang .. ": maxrss is nonzero") +local usec, ums = r.out:match("user=(%d+)%.(%d+)s") +local user_ms = usec and (tonumber(usec) * 1000 + tonumber(ums)) or nil +expect_true(user_ms ~= nil and user_ms > 20, + lang .. ": busy loop registers child user CPU (got " .. + tostring(user_ms) .. "ms)") + +-- 10. Bad usage: exit 2 with the usage line. +r = checks.run(demo) +checks.expect_exit(r, 2, lang .. ": missing subcommand exits 2") +checks.expect_match(r.out, "usage: pmon run %-%- CMD %[ARGS%.%.%.%]", + lang .. ": prints usage line") + +checks.finish() diff --git a/examples/12-signals-and-safe-handling/README.md b/examples/12-signals-and-safe-handling/README.md new file mode 100644 index 0000000..0aa6b00 --- /dev/null +++ b/examples/12-signals-and-safe-handling/README.md @@ -0,0 +1,110 @@ +# 12-signals-and-safe-handling — pmon v1, the supervising loop + +Chapter 12 grows `pmon` from "run one child and report" into a real +supervisor — and uses the restart loop as a vehicle for the chapter's actual +subject: how to consume signals *safely*. Each language deliberately uses a +different mechanism, because the three mechanisms together are the lesson. + +``` +pmon run -- CMD [ARGS...] # v0: run once, report +pmon supervise [--max-restarts N] [--backoff-ms B] -- CMD # v1: restart on failure +``` + +## Behavior (identical across C++, Go, and Rust) + +- `run -- CMD` starts CMD with inherited stdio, prints + `pmon: started pid

`, waits, prints + `pmon: child

exited status=` (or `killed signal=`), and exits + with the child's status (`128+N` for a signal death). +- `supervise` (defaults: `--max-restarts 5`, `--backoff-ms 100`): + - a child that exits **0** ends supervision: exit 0. + - a child that exits nonzero or dies to a signal is restarted after a + backoff that **doubles** each time (B, 2B, 4B, ...): + `pmon: restart # (backoff ms)`, then a fresh + `pmon: started pid `. After N failed restarts: + `pmon: giving up after restarts`, exit 1. + - **SIGTERM / SIGINT**: forward SIGTERM to the child, reap it, print + `pmon: shutting down (SIGTERM|SIGINT)`, exit 0. + - **SIGHUP**: print `pmon: reload requested`, SIGTERM + reap the current + child (its exit is not reported and does not count as a crash), start a + replacement immediately, and reset the backoff and restart budget. +- No/bad arguments print a usage block on stderr and exit 2; runtime errors + print `pmon: error: ...` on stderr and exit 1. + +## Three signal mechanisms, one contract + +The restart loop is identical; how each language *hears* a signal is not. +None of them runs meaningful code in signal context — that is the point. + +- **C++ — signalfd(2) + poll(2), zero handlers.** SIGTERM/SIGINT/SIGHUP/ + SIGCHLD are blocked with `sigprocmask(2)` and read as `signalfd_siginfo` + records from a file descriptor inside a `poll` loop (the backoff is just + the poll timeout). Nothing ever executes asynchronously, so the + async-signal-safe function list stops mattering — except between `fork()` + and `execvp()`, where the child restores an empty mask (an inherited + blocked SIGTERM survives exec and would make the child unkillable) using + only async-signal-safe calls. +- **Go — signal.Notify + select.** The runtime owns the actual handlers and + turns delivery into channel sends. Supervision is one `select` over three + channels: signals, child exits from a `cmd.Wait()` goroutine, and the + backoff `time.Timer`. Child liveness and signal forwarding go through + `golang.org/x/sys/unix`. +- **Rust — the self-pipe trick via signal-hook.** Each signal is registered + twice: `signal_hook::flag::register` sets a per-signal `AtomicBool`, and + `low_level::pipe::register_raw` (which takes its dup of the pipe's write + end as an `OwnedFd` and makes it non-blocking) writes a doorbell byte — + `write(2)` is async-signal-safe. The main loop polls the doorbell's read + end with `nix::poll`, drains it, checks the flags, and asks + `waitpid(WNOHANG)` what actually happened. SIGCHLD needs no flag: the + wait status is the source of truth. + +## Demo contract + +Each language directory has a `demo.sh` with the book-wide interface: + +- `./demo.sh build` — build only +- `./demo.sh run [args]` — run the built binary (builds first if needed) +- With env `TARGET` set, `run` deploys the binary to that lab VM via + `scripts/lab/deploy-to-vm.sh` instead of running locally. + +The top-level `demo.sh` dispatches: `./demo.sh cpp run supervise -- sleep 5` +execs `cpp/demo.sh run supervise -- sleep 5`. The binary is always named +`app`. + +Try it: + +```sh +./demo.sh build +./demo.sh cpp run supervise --max-restarts 2 --backoff-ms 200 -- sh -c 'exit 1' +./demo.sh go run supervise -- sleep 300 & # then: kill -HUP %1 ; kill -TERM %1 +./demo.sh rust run run -- sh -c 'exit 3' ; echo "propagated: $?" +``` + +## Verification + +`verify.lua` (run per language with `LSP_LANG` set) asserts behavior, not +exit codes alone: + +- `run` propagates exit 3 and maps a SIGKILL death to exit 137 with the + matching report lines; +- the crash loop against `sh -c 'exit 1'` with `--max-restarts 2 + --backoff-ms 120` prints `restart #1 (backoff 120ms)` and `restart #2 + (backoff 240ms)`, starts three distinct child pids, gives up, exits 1, + and measurably sleeps (elapsed >= 340 ms by `date +%s%N`); +- SIGTERM and SIGINT to a live supervisor produce the shutdown line, exit + 0, and a reaped child (`kill -0` on the child pid fails afterwards); +- SIGHUP produces the reload line and a replacement child whose pid + differs, with the old pid dead and the replacement alive — and a + follow-up SIGTERM still shuts everything down cleanly. + +Run it standalone from this directory: + +```sh +LSP_LANG=rust REPO_ROOT=../.. lua verify.lua +``` + +or through the orchestrator: + +```sh +python3 ../../scripts/test-all-examples.py --only 12-signals-and-safe-handling +``` diff --git a/examples/12-signals-and-safe-handling/cpp/.clang-format b/examples/12-signals-and-safe-handling/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/12-signals-and-safe-handling/cpp/.clang-tidy b/examples/12-signals-and-safe-handling/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/12-signals-and-safe-handling/cpp/CMakeLists.txt b/examples/12-signals-and-safe-handling/cpp/CMakeLists.txt new file mode 100644 index 0000000..c945061 --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/12-signals-and-safe-handling/cpp/CMakePresets.json b/examples/12-signals-and-safe-handling/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/12-signals-and-safe-handling/cpp/demo.sh b/examples/12-signals-and-safe-handling/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/12-signals-and-safe-handling/cpp/src/main.cpp b/examples/12-signals-and-safe-handling/cpp/src/main.cpp new file mode 100644 index 0000000..959e412 --- /dev/null +++ b/examples/12-signals-and-safe-handling/cpp/src/main.cpp @@ -0,0 +1,366 @@ +// pmon v1 — a mini process supervisor (chapter 12: signals and safe handling). +// +// pmon run -- CMD [ARGS...] +// pmon supervise [--max-restarts N] [--backoff-ms B] -- CMD [ARGS...] +// +// The C++ take on async-signal-safe design: install NO signal handlers at +// all. SIGTERM/SIGINT/SIGHUP/SIGCHLD are blocked with sigprocmask(2) and +// converted into ordinary file-descriptor reads via signalfd(2), consumed +// from a plain poll(2) loop. No code ever runs in signal context, so nothing +// has to be async-signal-safe — except the tiny window in the forked child +// between fork() and execvp(), which restores an empty signal mask (a +// blocked, inherited SIGTERM would survive exec and make the supervised +// child unkillable). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono; + +class UniqueFd { + public: + UniqueFd() = default; + explicit UniqueFd(int fd) : fd_(fd) {} + UniqueFd(const UniqueFd&) = delete; + UniqueFd& operator=(const UniqueFd&) = delete; + UniqueFd(UniqueFd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {} + UniqueFd& operator=(UniqueFd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = std::exchange(other.fd_, -1); + } + return *this; + } + ~UniqueFd() { reset(); } + [[nodiscard]] int get() const { return fd_; } + [[nodiscard]] bool valid() const { return fd_ >= 0; } + + private: + void reset() { + if (fd_ >= 0) { + ::close(fd_); + fd_ = -1; + } + } + int fd_ = -1; +}; + +[[nodiscard]] std::error_code errno_ec() { + return {errno, std::generic_category()}; +} + +[[noreturn]] void die(std::string_view what, std::error_code ec) { + std::println(stderr, "pmon: error: {}: {}", what, ec.message()); + std::exit(1); +} + +[[noreturn]] void usage() { + std::println(stderr, "usage: pmon run -- CMD [ARGS...]"); + std::println(stderr, + " pmon supervise [--max-restarts N] [--backoff-ms B] " + "-- CMD [ARGS...]"); + std::exit(2); +} + +// Fork + exec CMD. The child restores an empty signal mask before execvp so +// it does not inherit the parent's blocked TERM/INT/HUP/CHLD set; after +// fork() only async-signal-safe calls happen (sigprocmask, execvp, write, +// _exit). +[[nodiscard]] std::expected +spawn(const std::vector& cmd) { + std::vector argv; + argv.reserve(cmd.size() + 1); + for (const auto& arg : cmd) { + argv.push_back(const_cast(arg.c_str())); + } + argv.push_back(nullptr); + + const pid_t pid = ::fork(); + if (pid < 0) { + return std::unexpected(errno_ec()); + } + if (pid == 0) { + sigset_t none; + sigemptyset(&none); + ::sigprocmask(SIG_SETMASK, &none, nullptr); + ::execvp(argv[0], argv.data()); + constexpr char msg[] = "pmon: error: exec failed\n"; + [[maybe_unused]] auto n = ::write(STDERR_FILENO, msg, sizeof msg - 1); + ::_exit(127); + } + std::println("pmon: started pid {}", pid); + return pid; +} + +struct ChildExit { + int status = 0; // exit status when !signaled + int signo = 0; // terminating signal when signaled + bool signaled = false; +}; + +[[nodiscard]] ChildExit decode(int wstatus) { + if (WIFSIGNALED(wstatus)) { + return {.status = 0, .signo = WTERMSIG(wstatus), .signaled = true}; + } + return {.status = WEXITSTATUS(wstatus), .signo = 0, .signaled = false}; +} + +void report(pid_t pid, const ChildExit& ce) { + if (ce.signaled) { + std::println("pmon: child {} killed signal={}", pid, ce.signo); + } else { + std::println("pmon: child {} exited status={}", pid, ce.status); + } +} + +// Blocking reap that tolerates EINTR (cannot fire while the supervisor's +// mask blocks everything we watch, but `run` reaps with the default mask). +[[nodiscard]] std::expected reap_blocking(pid_t pid) { + int wstatus = 0; + for (;;) { + if (::waitpid(pid, &wstatus, 0) >= 0) { + return decode(wstatus); + } + if (errno != EINTR) { + return std::unexpected(errno_ec()); + } + } +} + +int run_once(const std::vector& cmd) { + const auto pid = spawn(cmd); + if (!pid) { + die("fork", pid.error()); + } + const auto ce = reap_blocking(*pid); + if (!ce) { + die("waitpid", ce.error()); + } + report(*pid, *ce); + return ce->signaled ? 128 + ce->signo : ce->status; +} + +struct SuperviseOpts { + int max_restarts = 5; + long backoff_ms = 100; + std::vector cmd; +}; + +int supervise(const SuperviseOpts& opts) { + // Block the signals we care about, then receive them as fd reads. + sigset_t mask; + sigemptyset(&mask); + for (const int signo : {SIGTERM, SIGINT, SIGHUP, SIGCHLD}) { + sigaddset(&mask, signo); + } + if (::sigprocmask(SIG_BLOCK, &mask, nullptr) < 0) { + die("sigprocmask", errno_ec()); + } + UniqueFd sfd(::signalfd(-1, &mask, SFD_CLOEXEC)); + if (!sfd.valid()) { + die("signalfd", errno_ec()); + } + + auto start = [&] { + const auto pid = spawn(opts.cmd); + if (!pid) { + die("fork", pid.error()); + } + return *pid; + }; + // SIGTERM the child and reap it. Used by shutdown and reload paths. + auto stop_child = [&](pid_t pid) { + if (::kill(pid, SIGTERM) == 0 || errno == ESRCH) { + if (const auto ce = reap_blocking(pid); !ce) { + die("waitpid", ce.error()); + } + } + }; + + pid_t child = start(); + bool running = true; // false: no child alive, waiting out a backoff + int restarts = 0; + long backoff = opts.backoff_ms; + steady_clock::time_point deadline{}; + + for (;;) { + int timeout = -1; + if (!running) { + const auto left = deadline - steady_clock::now(); + timeout = + left <= 0ns + ? 0 + : static_cast( + duration_cast(left).count()) + + 1; + } + pollfd pfd{.fd = sfd.get(), .events = POLLIN, .revents = 0}; + const int nready = ::poll(&pfd, 1, timeout); + if (nready < 0) { + if (errno == EINTR) { + continue; + } + die("poll", errno_ec()); + } + if (nready == 0) { + // Backoff elapsed: bring the child back. + child = start(); + running = true; + continue; + } + + signalfd_siginfo si{}; + const ssize_t nread = ::read(sfd.get(), &si, sizeof si); + if (nread != static_cast(sizeof si)) { + die("read signalfd", errno_ec()); + } + + switch (si.ssi_signo) { + case SIGTERM: + case SIGINT: { + if (running) { + stop_child(child); + } + std::println("pmon: shutting down ({})", + si.ssi_signo == SIGTERM ? "SIGTERM" : "SIGINT"); + return 0; + } + case SIGHUP: { + std::println("pmon: reload requested"); + if (running) { + stop_child(child); + } + restarts = 0; + backoff = opts.backoff_ms; + child = start(); + running = true; + break; + } + case SIGCHLD: { + int wstatus = 0; + const pid_t reaped = ::waitpid(child, &wstatus, WNOHANG); + if (!running || reaped != child) { + break; // stale notification (e.g. already reaped on reload) + } + const ChildExit ce = decode(wstatus); + report(child, ce); + if (!ce.signaled && ce.status == 0) { + return 0; + } + if (restarts >= opts.max_restarts) { + std::println("pmon: giving up after {} restarts", + opts.max_restarts); + return 1; + } + ++restarts; + std::println("pmon: restart #{} (backoff {}ms)", restarts, backoff); + deadline = steady_clock::now() + milliseconds(backoff); + backoff *= 2; + running = false; + break; + } + default: + break; + } + } +} + +// Parse "[flags] -- CMD [ARGS...]" for supervise. +[[nodiscard]] std::expected +parse_supervise(const std::vector& args) { + SuperviseOpts opts; + std::size_t i = 0; + auto numeric_flag = [&](const char* name, long min, + long& out) -> std::expected { + if (i + 1 >= args.size()) { + return std::unexpected(std::string(name) + " needs a value"); + } + const std::string& value = args[++i]; + char* end = nullptr; + errno = 0; + const long parsed = std::strtol(value.c_str(), &end, 10); + if (errno != 0 || end == value.c_str() || *end != '\0' || + parsed < min) { + return std::unexpected("bad value for " + std::string(name)); + } + out = parsed; + return {}; + }; + while (i < args.size()) { + const std::string& arg = args[i]; + if (arg == "--") { + ++i; + break; + } + if (arg == "--max-restarts") { + long v = 0; + if (auto r = numeric_flag("--max-restarts", 0, v); !r) { + return std::unexpected(r.error()); + } + opts.max_restarts = static_cast(v); + } else if (arg == "--backoff-ms") { + long v = 0; + if (auto r = numeric_flag("--backoff-ms", 1, v); !r) { + return std::unexpected(r.error()); + } + opts.backoff_ms = v; + } else { + return std::unexpected("unknown flag " + arg); + } + ++i; + } + for (; i < args.size(); ++i) { + opts.cmd.push_back(args[i]); + } + if (opts.cmd.empty()) { + return std::unexpected("no command after --"); + } + return opts; +} + +} // namespace + +int main(int argc, char** argv) { + std::setvbuf(stdout, nullptr, _IOLBF, 0); // line-buffered even into a file + + const std::vector args(argv + 1, argv + argc); + if (args.empty()) { + usage(); + } + const std::string& sub = args[0]; + const std::vector rest(args.begin() + 1, args.end()); + + if (sub == "run") { + if (rest.size() < 2 || rest[0] != "--") { + usage(); + } + return run_once({rest.begin() + 1, rest.end()}); + } + if (sub == "supervise") { + const auto opts = parse_supervise(rest); + if (!opts) { + std::println(stderr, "pmon: error: {}", opts.error()); + usage(); + } + return supervise(*opts); + } + usage(); +} diff --git a/examples/12-signals-and-safe-handling/demo.sh b/examples/12-signals-and-safe-handling/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/12-signals-and-safe-handling/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/12-signals-and-safe-handling/go/demo.sh b/examples/12-signals-and-safe-handling/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/12-signals-and-safe-handling/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/12-signals-and-safe-handling/go/go.mod b/examples/12-signals-and-safe-handling/go/go.mod new file mode 100644 index 0000000..1a5ae1e --- /dev/null +++ b/examples/12-signals-and-safe-handling/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/12-signals-and-safe-handling/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/12-signals-and-safe-handling/go/go.sum b/examples/12-signals-and-safe-handling/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/12-signals-and-safe-handling/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/12-signals-and-safe-handling/go/main.go b/examples/12-signals-and-safe-handling/go/main.go new file mode 100644 index 0000000..29a0166 --- /dev/null +++ b/examples/12-signals-and-safe-handling/go/main.go @@ -0,0 +1,248 @@ +// pmon v1 — a mini process supervisor (chapter 12: signals and safe handling). +// +// pmon run -- CMD [ARGS...] +// pmon supervise [--max-restarts N] [--backoff-ms B] -- CMD [ARGS...] +// +// The Go take on async-signal-safe design: the runtime already owns the +// signal handlers (they do nothing but forward a note to the runtime), and +// os/signal turns delivery into ordinary channel sends. Supervision is then +// a single select over three channels: signals from signal.Notify, child +// exits from a wait goroutine, and a backoff timer. No user code ever runs +// in signal context. +package main + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "strconv" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +const usageText = `usage: pmon run -- CMD [ARGS...] + pmon supervise [--max-restarts N] [--backoff-ms B] -- CMD [ARGS...]` + +func usage() { + fmt.Fprintln(os.Stderr, usageText) + os.Exit(2) +} + +func die(err error) { + fmt.Fprintf(os.Stderr, "pmon: error: %v\n", err) + os.Exit(1) +} + +// childExit is the decoded wait status of a reaped child. +type childExit struct { + status int + signo int + signaled bool +} + +func decode(ps *os.ProcessState) childExit { + ws, ok := ps.Sys().(syscall.WaitStatus) + if ok && ws.Signaled() { + return childExit{signo: int(ws.Signal()), signaled: true} + } + return childExit{status: ps.ExitCode()} +} + +func report(pid int, ce childExit) { + if ce.signaled { + fmt.Printf("pmon: child %d killed signal=%d\n", pid, ce.signo) + } else { + fmt.Printf("pmon: child %d exited status=%d\n", pid, ce.status) + } +} + +// spawn starts CMD with inherited stdio and prints the started line. +func spawn(argv []string) (*exec.Cmd, error) { + cmd := exec.Command(argv[0], argv[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("start %s: %w", argv[0], err) + } + fmt.Printf("pmon: started pid %d\n", cmd.Process.Pid) + return cmd, nil +} + +func runOnce(argv []string) int { + cmd, err := spawn(argv) + if err != nil { + die(err) + } + _ = cmd.Wait() // error carries the nonzero status; ProcessState has it all + ce := decode(cmd.ProcessState) + report(cmd.Process.Pid, ce) + if ce.signaled { + return 128 + ce.signo + } + return ce.status +} + +func signame(s os.Signal) string { + switch s { + case unix.SIGTERM: + return "SIGTERM" + case unix.SIGINT: + return "SIGINT" + default: + return s.String() + } +} + +type superviseOpts struct { + maxRestarts int + backoffMs int64 + argv []string +} + +func supervise(opts superviseOpts) int { + sigCh := make(chan os.Signal, 8) + signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT, unix.SIGHUP) + + // One in-flight wait goroutine per live child; the buffered channel lets + // it finish even when the supervisor consumes the exit via stopChild. + waitCh := make(chan childExit, 1) + start := func() *exec.Cmd { + cmd, err := spawn(opts.argv) + if err != nil { + die(err) + } + go func() { + _ = cmd.Wait() + waitCh <- decode(cmd.ProcessState) + }() + return cmd + } + // SIGTERM the child and consume its exit. Used by shutdown and reload. + stopChild := func(cmd *exec.Cmd) { + _ = unix.Kill(cmd.Process.Pid, unix.SIGTERM) // ESRCH: already exiting + <-waitCh + } + + cmd := start() + restarts := 0 + backoff := opts.backoffMs + var timer *time.Timer // non-nil: no child alive, waiting out a backoff + + for { + var timerCh <-chan time.Time + if timer != nil { + timerCh = timer.C + } + select { + case s := <-sigCh: + switch s { + case unix.SIGTERM, unix.SIGINT: + if timer == nil { + stopChild(cmd) + } + fmt.Printf("pmon: shutting down (%s)\n", signame(s)) + return 0 + case unix.SIGHUP: + fmt.Println("pmon: reload requested") + if timer == nil { + stopChild(cmd) + } else { + timer.Stop() + timer = nil + } + restarts = 0 + backoff = opts.backoffMs + cmd = start() + } + case ce := <-waitCh: + // The child exited on its own. + report(cmd.Process.Pid, ce) + if !ce.signaled && ce.status == 0 { + return 0 + } + if restarts >= opts.maxRestarts { + fmt.Printf("pmon: giving up after %d restarts\n", opts.maxRestarts) + return 1 + } + restarts++ + fmt.Printf("pmon: restart #%d (backoff %dms)\n", restarts, backoff) + timer = time.NewTimer(time.Duration(backoff) * time.Millisecond) + backoff *= 2 + case <-timerCh: + // Backoff elapsed: bring the child back. + timer = nil + cmd = start() + } + } +} + +func parseSupervise(args []string) (superviseOpts, error) { + opts := superviseOpts{maxRestarts: 5, backoffMs: 100} + i := 0 + numericFlag := func(name string, min int64) (int64, error) { + if i+1 >= len(args) { + return 0, fmt.Errorf("%s needs a value", name) + } + i++ + v, err := strconv.ParseInt(args[i], 10, 64) + if err != nil || v < min { + return 0, fmt.Errorf("bad value for %s", name) + } + return v, nil + } + for i < len(args) { + switch args[i] { + case "--": + i++ + opts.argv = args[i:] + if len(opts.argv) == 0 { + return opts, fmt.Errorf("no command after --") + } + return opts, nil + case "--max-restarts": + v, err := numericFlag("--max-restarts", 0) + if err != nil { + return opts, err + } + opts.maxRestarts = int(v) + case "--backoff-ms": + v, err := numericFlag("--backoff-ms", 1) + if err != nil { + return opts, err + } + opts.backoffMs = v + default: + return opts, fmt.Errorf("unknown flag %s", args[i]) + } + i++ + } + return opts, fmt.Errorf("no command after --") +} + +func main() { + args := os.Args[1:] + if len(args) == 0 { + usage() + } + sub, rest := args[0], args[1:] + switch sub { + case "run": + if len(rest) < 2 || rest[0] != "--" { + usage() + } + os.Exit(runOnce(rest[1:])) + case "supervise": + opts, err := parseSupervise(rest) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: error: %v\n", err) + usage() + } + os.Exit(supervise(opts)) + default: + usage() + } +} diff --git a/examples/12-signals-and-safe-handling/rust/Cargo.lock b/examples/12-signals-and-safe-handling/rust/Cargo.lock new file mode 100644 index 0000000..ccd0fc9 --- /dev/null +++ b/examples/12-signals-and-safe-handling/rust/Cargo.lock @@ -0,0 +1,99 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "pmon" +version = "1.0.0" +dependencies = [ + "anyhow", + "nix", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/examples/12-signals-and-safe-handling/rust/Cargo.toml b/examples/12-signals-and-safe-handling/rust/Cargo.toml new file mode 100644 index 0000000..70387ea --- /dev/null +++ b/examples/12-signals-and-safe-handling/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pmon" +version = "1.0.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0" +nix = { version = "0.31", features = ["poll", "process", "signal"] } +signal-hook = "0.4" diff --git a/examples/12-signals-and-safe-handling/rust/demo.sh b/examples/12-signals-and-safe-handling/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/12-signals-and-safe-handling/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/12-signals-and-safe-handling/rust/rust-toolchain.toml b/examples/12-signals-and-safe-handling/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/12-signals-and-safe-handling/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/12-signals-and-safe-handling/rust/src/main.rs b/examples/12-signals-and-safe-handling/rust/src/main.rs new file mode 100644 index 0000000..e909fbb --- /dev/null +++ b/examples/12-signals-and-safe-handling/rust/src/main.rs @@ -0,0 +1,295 @@ +// pmon v1 — a mini process supervisor (chapter 12: signals and safe handling). +// +// pmon run -- CMD [ARGS...] +// pmon supervise [--max-restarts N] [--backoff-ms B] -- CMD [ARGS...] +// +// The Rust take on async-signal-safe design: the classic self-pipe trick, +// with the unsafe parts delegated to the signal-hook crate. Each watched +// signal is registered twice — flag::register sets a per-signal AtomicBool, +// and low_level::pipe::register_raw writes one byte to a "doorbell" pipe +// (write(2) is async-signal-safe; the write end is non-blocking so a full +// pipe can never wedge the handler). The main loop is then ordinary code: +// poll(2) on the doorbell with the backoff as the timeout, drain it, and +// look at the flags plus waitpid(WNOHANG) to see what happened. + +use std::io::{PipeWriter, Read}; +use std::os::fd::{AsFd, OwnedFd}; +use std::process::Command; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, bail}; +use nix::errno::Errno; +use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; +use nix::sys::signal::{Signal, kill}; +use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid}; +use nix::unistd::Pid; +use signal_hook::consts::{SIGCHLD, SIGHUP, SIGINT, SIGTERM}; + +const USAGE: &str = "usage: pmon run -- CMD [ARGS...]\n pmon supervise \ + [--max-restarts N] [--backoff-ms B] -- CMD [ARGS...]"; + +fn usage() -> ! { + eprintln!("{USAGE}"); + std::process::exit(2); +} + +fn die(err: anyhow::Error) -> ! { + eprintln!("pmon: error: {err:#}"); + std::process::exit(1); +} + +/// Decoded exit of a reaped child. +#[derive(Clone, Copy)] +struct ChildExit { + status: i32, + signo: i32, + signaled: bool, +} + +impl ChildExit { + fn clean(self) -> bool { + !self.signaled && self.status == 0 + } +} + +fn report(pid: i32, ce: ChildExit) { + if ce.signaled { + println!("pmon: child {pid} killed signal={}", ce.signo); + } else { + println!("pmon: child {pid} exited status={}", ce.status); + } +} + +/// Spawn CMD with inherited stdio and print the started line. +fn spawn(argv: &[String]) -> Result { + let child = Command::new(&argv[0]) + .args(&argv[1..]) + .spawn() + .with_context(|| format!("start {}", argv[0]))?; + let pid = child.id() as i32; + println!("pmon: started pid {pid}"); + // The Child handle is dropped without wait(): pmon reaps via waitpid(2) + // so one code path serves both subcommands. Drop does not reap. + Ok(pid) +} + +fn decode_wait(ws: WaitStatus) -> Option { + match ws { + WaitStatus::Exited(_, code) => { + Some(ChildExit { status: code, signo: 0, signaled: false }) + } + WaitStatus::Signaled(_, sig, _) => { + Some(ChildExit { status: 0, signo: sig as i32, signaled: true }) + } + _ => None, // StillAlive, stopped, continued: not an exit + } +} + +/// Blocking reap tolerating EINTR (signal-hook handlers may interrupt it). +fn reap_blocking(pid: i32) -> Result { + loop { + match waitpid(Pid::from_raw(pid), None) { + Ok(ws) => { + if let Some(ce) = decode_wait(ws) { + return Ok(ce); + } + } + Err(Errno::EINTR) => continue, + Err(e) => return Err(e).context("waitpid"), + } + } +} + +fn run_once(argv: &[String]) -> Result { + let pid = spawn(argv)?; + let ce = reap_blocking(pid)?; + report(pid, ce); + Ok(if ce.signaled { 128 + ce.signo } else { ce.status }) +} + +struct SuperviseOpts { + max_restarts: u32, + backoff_ms: u64, + argv: Vec, +} + +/// One registered signal: the flag half of the flag + doorbell pair. The +/// handler owns its dup of the pipe's write end as an OwnedFd; signal-hook +/// makes it non-blocking so the handler can never wedge on a full pipe. +fn flag_for(signal: i32, doorbell: &PipeWriter) -> Result> { + let flag = Arc::new(AtomicBool::new(false)); + signal_hook::flag::register(signal, Arc::clone(&flag)).context("flag::register")?; + let owned: OwnedFd = doorbell.as_fd().try_clone_to_owned().context("dup")?; + signal_hook::low_level::pipe::register_raw(signal, owned) + .context("pipe::register_raw")?; + Ok(flag) +} + +fn supervise(opts: &SuperviseOpts) -> Result { + // The doorbell: handlers write a byte, the main loop polls the read end. + let (mut reader, writer) = std::io::pipe().context("pipe")?; + let got_term = flag_for(SIGTERM, &writer)?; + let got_int = flag_for(SIGINT, &writer)?; + let got_hup = flag_for(SIGHUP, &writer)?; + // SIGCHLD needs no flag: waitpid(WNOHANG) is the source of truth. + signal_hook::low_level::pipe::register_raw(SIGCHLD, writer.into()) + .context("pipe::register_raw")?; + + // SIGTERM the child and reap it. Used by shutdown and reload paths. + let stop_child = |pid: i32| -> Result<()> { + match kill(Pid::from_raw(pid), Signal::SIGTERM) { + Ok(()) | Err(Errno::ESRCH) => {} + Err(e) => return Err(e).context("kill"), + } + reap_blocking(pid).map(|_| ()) + }; + + let mut child = spawn(&opts.argv)?; + let mut running = true; // false: no child alive, waiting out a backoff + let mut restarts: u32 = 0; + let mut backoff = opts.backoff_ms; + let mut deadline = Instant::now(); + + loop { + let timeout = if running { + PollTimeout::NONE + } else { + let left = deadline.saturating_duration_since(Instant::now()); + let ms = left.as_millis().saturating_add(1).min(u16::MAX as u128); + if left.is_zero() { PollTimeout::ZERO } else { PollTimeout::from(ms as u16) } + }; + let nready = { + let mut fds = [PollFd::new(reader.as_fd(), PollFlags::POLLIN)]; + match poll(&mut fds, timeout) { + Ok(n) => n, + Err(Errno::EINTR) => continue, + Err(e) => return Err(e).context("poll"), + } + }; + if nready == 0 { + if Instant::now() < deadline { + continue; // clamped timeout; keep waiting + } + // Backoff elapsed: bring the child back. + child = spawn(&opts.argv)?; + running = true; + continue; + } + + // Drain the doorbell (poll said readable; coalesced bytes are fine). + let mut buf = [0u8; 64]; + let _ = reader.read(&mut buf).context("read doorbell")?; + + let term = got_term.swap(false, Ordering::SeqCst); + let intr = got_int.swap(false, Ordering::SeqCst); + if term || intr { + if running { + stop_child(child)?; + } + println!( + "pmon: shutting down ({})", + if term { "SIGTERM" } else { "SIGINT" } + ); + return Ok(0); + } + + if got_hup.swap(false, Ordering::SeqCst) { + println!("pmon: reload requested"); + if running { + stop_child(child)?; + } + restarts = 0; + backoff = opts.backoff_ms; + child = spawn(&opts.argv)?; + running = true; + continue; + } + + // SIGCHLD (or a stale doorbell byte): see whether our child exited. + if !running { + continue; + } + match waitpid(Pid::from_raw(child), Some(WaitPidFlag::WNOHANG)) { + Ok(ws) => { + let Some(ce) = decode_wait(ws) else { continue }; + report(child, ce); + if ce.clean() { + return Ok(0); + } + if restarts >= opts.max_restarts { + println!("pmon: giving up after {} restarts", opts.max_restarts); + return Ok(1); + } + restarts += 1; + println!("pmon: restart #{restarts} (backoff {backoff}ms)"); + deadline = Instant::now() + Duration::from_millis(backoff); + backoff *= 2; + running = false; + } + Err(Errno::EINTR) | Err(Errno::ECHILD) => continue, + Err(e) => return Err(e).context("waitpid"), + } + } +} + +fn parse_supervise(args: &[String]) -> Result { + let mut opts = + SuperviseOpts { max_restarts: 5, backoff_ms: 100, argv: Vec::new() }; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--" => { + opts.argv = args[i + 1..].to_vec(); + if opts.argv.is_empty() { + bail!("no command after --"); + } + return Ok(opts); + } + "--max-restarts" => { + i += 1; + let v = args.get(i).and_then(|s| s.parse::().ok()); + opts.max_restarts = + v.ok_or_else(|| anyhow::anyhow!("bad value for --max-restarts"))?; + } + "--backoff-ms" => { + i += 1; + let v = args + .get(i) + .and_then(|s| s.parse::().ok()) + .filter(|&v| v >= 1); + opts.backoff_ms = + v.ok_or_else(|| anyhow::anyhow!("bad value for --backoff-ms"))?; + } + other => bail!("unknown flag {other}"), + } + i += 1; + } + bail!("no command after --") +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + usage(); + } + let (sub, rest) = (args[0].as_str(), &args[1..]); + let code = match sub { + "run" => { + if rest.len() < 2 || rest[0] != "--" { + usage(); + } + run_once(&rest[1..]).unwrap_or_else(|e| die(e)) + } + "supervise" => { + let opts = parse_supervise(rest).unwrap_or_else(|e| { + eprintln!("pmon: error: {e:#}"); + usage(); + }); + supervise(&opts).unwrap_or_else(|e| die(e)) + } + _ => usage(), + }; + std::process::exit(code); +} diff --git a/examples/12-signals-and-safe-handling/verify.lua b/examples/12-signals-and-safe-handling/verify.lua new file mode 100644 index 0000000..6bc248b --- /dev/null +++ b/examples/12-signals-and-safe-handling/verify.lua @@ -0,0 +1,156 @@ +-- Verify pmon v1: assert supervisor BEHAVIOR, not exit-0. +-- * v0 `run` still propagates child exit status and signal deaths +-- * crash loop: exact restart lines, doubling backoff values, distinct +-- child pids, giving-up line, exit 1, and real elapsed sleep time +-- * SIGTERM/SIGINT: clean shutdown line, exit 0, child actually reaped +-- * SIGHUP: reload line, replacement child with a new pid, old pid dead +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set. Plain Lua 5.1+ (works under luajit). + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local bins = { + cpp = "cpp/build/release/app", + go = "go/bin/app", + rust = "rust/target/release/app", +} +local bin = bins[lang] + +local function demo(args) + return checks.run("./demo.sh " .. lang .. " run " .. args) +end + +-- Boolean assertion via the checks pattern API. +local function expect_true(cond, label) + checks.expect_match(cond and "yes" or "no", "yes", label) +end + +local ws = checks.run("mktemp -d").out:gsub("%s+$", "") +assert(ws:match("^/"), "mktemp -d must yield an absolute path") + +-- Ensure the binary exists (the orchestrator builds before verifying; a +-- standalone run should fail loudly here instead of deep in a scenario). +local r = checks.run("test -x " .. bin) +checks.expect_exit(r, 0, lang .. ": built binary " .. bin .. " exists") + +-- 1. Usage: bare invocation and a missing "--" both refuse with exit 2. +r = demo("") +checks.expect_exit(r, 2, lang .. ": bare invocation exits 2") +checks.expect_match(r.out, "usage: pmon run %-%- CMD", lang .. ": prints usage") +r = demo("run echo hi") +checks.expect_exit(r, 2, lang .. ": run without -- exits 2") + +-- 2. v0 `run`: exit status propagates; signal deaths become 128+N. +r = demo("run -- sh -c 'exit 3'") +checks.expect_exit(r, 3, lang .. ": run propagates child exit 3") +checks.expect_match(r.out, "pmon: child %d+ exited status=3", + lang .. ": run reports child status") +r = demo("run -- sh -c 'kill -9 $$'") +checks.expect_exit(r, 137, lang .. ": run maps SIGKILL death to exit 137") +checks.expect_match(r.out, "pmon: child %d+ killed signal=9", + lang .. ": run reports the killing signal") + +-- 3. supervise with a clean child: no restart, exit 0. +r = demo("supervise --max-restarts 3 -- true") +checks.expect_exit(r, 0, lang .. ": clean child ends supervision with 0") +checks.expect_match(r.out, "pmon: child %d+ exited status=0", + lang .. ": clean exit reported") +expect_true(not r.out:find("pmon: restart"), + lang .. ": clean exit triggers no restart") + +-- 4. Crash loop: two restarts with doubling backoff, then give up. +r = checks.run(string.format([[ + start=$(date +%%s%%N) + ./demo.sh %s run supervise --max-restarts 2 --backoff-ms 120 -- sh -c 'exit 1' + code=$? + end=$(date +%%s%%N) + echo "code=$code elapsed_ms=$(( (end - start) / 1000000 ))" +]], lang)) +checks.expect_match(r.out, "pmon: restart #1 %(backoff 120ms%)", + lang .. ": first restart uses the base backoff") +checks.expect_match(r.out, "pmon: restart #2 %(backoff 240ms%)", + lang .. ": second backoff doubles") +checks.expect_match(r.out, "pmon: giving up after 2 restarts", + lang .. ": gives up when the budget is spent") +checks.expect_match(r.out, "code=1 ", lang .. ": crash loop exits 1") +local pids = {} +for pid in r.out:gmatch("pmon: started pid (%d+)") do pids[#pids + 1] = pid end +expect_true(#pids == 3, lang .. ": three children were started (got " .. #pids .. ")") +expect_true(#pids == 3 and pids[1] ~= pids[2] and pids[2] ~= pids[3] + and pids[1] ~= pids[3], lang .. ": each restart is a fresh pid") +local elapsed = tonumber(r.out:match("elapsed_ms=(%d+)")) +expect_true(elapsed ~= nil and elapsed >= 340, + lang .. ": backoffs really slept 120+240ms (elapsed " .. + tostring(elapsed) .. "ms)") + +-- 5. SIGTERM and SIGINT: forward SIGTERM, reap the child, exit 0. +for _, sig in ipairs({ "TERM", "INT" }) do + r = checks.run(string.format([[ + log=%s/sig%s.log + ./%s supervise -- sleep 30 > "$log" 2>&1 & + pid=$! + i=0; while [ $i -lt 50 ]; do + grep -q "started pid" "$log" && break; sleep 0.1; i=$((i+1)); done + child=$(awk '/started pid/{print $4}' "$log" | head -1) + kill -%s $pid + wait $pid; echo "supervisor_exit=$?" + sleep 0.1 + if kill -0 "$child" 2>/dev/null; then echo "child=alive"; else echo "child=gone"; fi + cat "$log" + ]], ws, sig, bin, sig)) + checks.expect_match(r.out, "pmon: shutting down %(SIG" .. sig .. "%)", + lang .. ": SIG" .. sig .. " prints the shutdown line") + checks.expect_match(r.out, "supervisor_exit=0", + lang .. ": SIG" .. sig .. " shuts down with exit 0") + checks.expect_match(r.out, "child=gone", + lang .. ": SIG" .. sig .. " leaves no child process behind") +end + +-- 6. SIGHUP: reload line, replacement child, backoff/restart state reset. +r = checks.run(string.format([[ + log=%s/hup.log + ./%s supervise -- sleep 30 > "$log" 2>&1 & + pid=$! + i=0; while [ $i -lt 50 ]; do + grep -q "started pid" "$log" && break; sleep 0.1; i=$((i+1)); done + first=$(awk '/started pid/{print $4}' "$log" | head -1) + kill -HUP $pid + i=0; while [ $i -lt 50 ]; do + [ "$(grep -c "started pid" "$log")" -ge 2 ] && break; sleep 0.1; i=$((i+1)); done + second=$(awk '/started pid/{print $4}' "$log" | sed -n 2p) + sleep 0.2 + if kill -0 "$first" 2>/dev/null; then echo "first=alive"; else echo "first=gone"; fi + if kill -0 "$second" 2>/dev/null; then echo "second=alive"; else echo "second=gone"; fi + [ -n "$first" ] && [ -n "$second" ] && [ "$first" != "$second" ] \ + && echo "distinct=yes" || echo "distinct=no" + kill -TERM $pid + wait $pid; echo "supervisor_exit=$?" + sleep 0.1 + if kill -0 "$second" 2>/dev/null; then echo "final=alive"; else echo "final=gone"; fi + cat "$log" +]], ws, bin)) +checks.expect_match(r.out, "pmon: reload requested", + lang .. ": SIGHUP prints the reload line") +checks.expect_match(r.out, "distinct=yes", + lang .. ": reload starts a replacement child with a new pid") +checks.expect_match(r.out, "first=gone", + lang .. ": reload terminates the old child") +checks.expect_match(r.out, "second=alive", + lang .. ": replacement child is running after reload") +checks.expect_match(r.out, "supervisor_exit=0", + lang .. ": post-reload SIGTERM still shuts down cleanly") +checks.expect_match(r.out, "final=gone", + lang .. ": shutdown reaps the replacement child too") +expect_true(not r.out:find("pmon: restart #"), + lang .. ": reload is not counted as a crash restart") + +checks.run("rm -rf " .. ws) +checks.finish() diff --git a/examples/13-pidfd-modern-process-mgmt/README.md b/examples/13-pidfd-modern-process-mgmt/README.md new file mode 100644 index 0000000..3903628 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/README.md @@ -0,0 +1,92 @@ +# 13 — pidfd: modern process management + +`pmon` v2, the chapter's process supervisor, implemented three times — C++23, +Go, and Rust — with identical observable behavior (CLI, output lines, exit +codes). This iteration adds a second supervision engine built on **pidfd**, +the modern child becomes-a-file-descriptor API, and makes it the default: + +``` +usage: pmon + run -- CMD [ARGS...] spawn CMD, wait, mirror its exit + supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T] + -- CMD [ARGS...] restart CMD on abnormal exit + (defaults: pidfd, N=3, T=10000) +``` + +## What each version added + +- **v0 — `run`**: spawn a command, wait for it, report and mirror its exit + (`status` → same exit code, fatal signal → `128+signo`). +- **v1 — `supervise --engine sigchld`**: restart the child on abnormal exit, + driven the classic way: SIGCHLD arrives (via a signalfd in C++/Rust, + `signal.Notify` in Go), then a `WNOHANG` wait reaps and classifies it. +- **v2 — `supervise --engine pidfd`** (default): no SIGCHLD anywhere. + `pidfd_open(2)` turns the child pid into a file descriptor; `poll(2)` on it + reports the exit as ordinary readability; `waitid(P_PIDFD, ...)` reaps + exactly the process behind the fd (a recycled pid can never be reaped or + signalled by mistake); the stop path delivers SIGTERM through the fd with + `pidfd_send_signal(2)`. + +## Output contract (identical in all three languages) + +| Stream | Line | When | +|---|---|---| +| stderr | `pmon: engine=pidfd child= pidfd=` | each spawn, pidfd engine | +| stderr | `pmon: engine=sigchld child=` | each spawn, sigchld engine | +| stderr | `pmon: run child=` | `run` spawn | +| stdout | `pmon: child= exited status=` | child exited normally | +| stdout | `pmon: child= killed signal=` | child died by signal | +| stdout | `pmon: restart /` | before each respawn | +| stdout | `pmon: giving up after restarts` | budget exhausted (exit 1) | +| stdout | `pmon: exiting (signal)` / `pmon: exiting (timeout)` | stop paths (exit 0) | + +A clean child exit (`status=0`) ends supervision with exit 0 and no restart. +SIGINT/SIGTERM to `pmon` and the `--timeout-ms` window both TERM the child, +reap it, and leave with exit 0. Usage errors exit 2; a command that cannot be +spawned exits 1 with `pmon: spawn: : ...`. + +## Per-language mechanics + +- **C++23** (`cpp/src/main.cpp`) — raw `syscall(SYS_pidfd_open)` and + `syscall(SYS_pidfd_send_signal)` on purpose (the libc wrappers are recent), + `poll(2)` over `{pidfd, signalfd}`, `waitid(P_PIDFD)` to reap. RAII `Fd` and + `SpawnAttrs` owners, `std::expected` error paths, `std::println`, + `posix_spawnp` with `POSIX_SPAWN_SETSIGMASK` so the child does not inherit + the blocked signalfd mask. +- **Go 1.26** (`go/main.go`) — `unix.PidfdOpen` + a goroutine parked in + `unix.Poll`, exit delivered over a channel into one `select` alongside + `signal.Notify` and the deadline; a raw `waitid(P_PIDFD)` via + `unix.Syscall6` with an explicit `siginfo_t` layout (x/sys keeps the union + opaque); stop path via `unix.PidfdSendSignal`. Errors are wrapped with `%w`. +- **Rust edition 2024** (`rust/src/main.rs`) — `rustix::process::pidfd_open` + returning an `OwnedFd`, `rustix::event::poll` over the pidfd + a nix + `SignalFd`, `rustix::process::waitid(WaitId::PidFd)`, stop path via + `rustix::process::pidfd_send_signal`. `Result` + `?` throughout. + Note: `std::process::Command` does **not** reset the child's signal mask, so + `pre_exec` clears it — otherwise a supervised `sleep` inherits a blocked + SIGTERM and can never be stopped. + +## Try it + +```bash +./demo.sh cpp run supervise --max-restarts 2 -- sh -c 'exit 1' +./demo.sh go run supervise --engine sigchld -- sh -c 'exit 0' +./demo.sh rust run supervise --timeout-ms 500 -- sleep 30 +./demo.sh cpp run run -- sh -c 'kill -9 $$' # mirrors as exit 137 +``` + +## Verification (behavioral) + +`verify.lua` drives every path and asserts effects, not exit-0: + +- `run` mirrors exit 0/3 and SIGKILL (exit 137, `killed signal=9`). +- The **pidfd engine supervises a crashing child**: exactly 3 spawn lines + each carrying a real `pidfd=` number, 3 observed crashes, + `restart 1/2` then `restart 2/2` in order, the giving-up line, exit 1. +- The **sigchld engine still works**: same run shape, no `pidfd=` field + anywhere in its output, and its restart lines are byte-identical to the + pidfd engine's. +- Both stop paths terminate the supervised child (`killed signal=15`), print + the `exiting (...)` line, exit 0 — and a `pgrep` process-tree check proves + no supervised `sleep` outlives its supervisor. +- Usage errors exit 2; an unspawnable command exits 1 and names the command. diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/.clang-format b/examples/13-pidfd-modern-process-mgmt/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/.clang-tidy b/examples/13-pidfd-modern-process-mgmt/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/CMakeLists.txt b/examples/13-pidfd-modern-process-mgmt/cpp/CMakeLists.txt new file mode 100644 index 0000000..c945061 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/CMakePresets.json b/examples/13-pidfd-modern-process-mgmt/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/demo.sh b/examples/13-pidfd-modern-process-mgmt/cpp/demo.sh new file mode 100755 index 0000000..3651e6f --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/demo.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + # exec so a backgrounded `run supervise ...` can be signalled directly + exec "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run supervise --timeout-ms 2000 -- sh -c 'exit 0' + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/13-pidfd-modern-process-mgmt/cpp/src/main.cpp b/examples/13-pidfd-modern-process-mgmt/cpp/src/main.cpp new file mode 100644 index 0000000..c772d28 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/cpp/src/main.cpp @@ -0,0 +1,447 @@ +// pmon v2 — a tiny process supervisor grown chapter by chapter. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child, driven by +// SIGCHLD delivered as readable data through a signalfd. +// v2 (`supervise --engine pidfd`, the DEFAULT) drops the SIGCHLD dependence +// entirely: pidfd_open(2) turns the child into a pollable file +// descriptor (raw SYS_pidfd_open on purpose — the glibc wrapper is +// recent), poll(2) reports the exit as ordinary readability, +// waitid(P_PIDFD) reaps exactly that child, and the stop path signals +// through the pidfd with pidfd_send_signal(2) — no pid-reuse race +// anywhere on the path. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace { + +using std::chrono::milliseconds; +using std::chrono::steady_clock; + +[[nodiscard]] std::error_code last_error() { + return {errno, std::system_category()}; +} + +// --------------------------------------------------------------------------- +// RAII owners — every fd and every spawn attribute has exactly one. +// --------------------------------------------------------------------------- + +class Fd { +public: + Fd() = default; + explicit Fd(int fd) : fd_{fd} {} + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& other) noexcept : fd_{std::exchange(other.fd_, -1)} {} + Fd& operator=(Fd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = std::exchange(other.fd_, -1); + } + return *this; + } + ~Fd() { reset(); } + + void reset() noexcept { + if (fd_ >= 0) { + ::close(fd_); + } + fd_ = -1; + } + [[nodiscard]] int get() const noexcept { return fd_; } + +private: + int fd_ = -1; +}; + +[[nodiscard]] std::expected checked(int fd) { + if (fd < 0) { + return std::unexpected(last_error()); + } + return Fd{fd}; +} + +class SpawnAttrs { +public: + SpawnAttrs() { ::posix_spawnattr_init(&attr_); } + SpawnAttrs(const SpawnAttrs&) = delete; + SpawnAttrs& operator=(const SpawnAttrs&) = delete; + ~SpawnAttrs() { ::posix_spawnattr_destroy(&attr_); } + [[nodiscard]] posix_spawnattr_t* get() noexcept { return &attr_; } + +private: + posix_spawnattr_t attr_{}; +}; + +// --------------------------------------------------------------------------- +// Spawning and status plumbing shared by every subcommand. +// --------------------------------------------------------------------------- + +// The supervisor blocks signals for its signalfd; the child must NOT inherit +// that mask (a supervised `sleep` with SIGTERM blocked would never stop), so +// posix_spawn resets the child's mask to empty. +[[nodiscard]] std::expected +spawn_child(const std::vector& cmd) { + std::vector argv; + argv.reserve(cmd.size() + 1); + for (const auto& s : cmd) { + argv.push_back(const_cast(s.c_str())); + } + argv.push_back(nullptr); + + SpawnAttrs attrs; + sigset_t empty; + sigemptyset(&empty); + ::posix_spawnattr_setsigmask(attrs.get(), &empty); + ::posix_spawnattr_setflags(attrs.get(), POSIX_SPAWN_SETSIGMASK); + + pid_t pid{}; + const int rc = ::posix_spawnp(&pid, argv[0], nullptr, attrs.get(), argv.data(), environ); + if (rc != 0) { + return std::unexpected(std::error_code{rc, std::system_category()}); + } + return pid; +} + +struct ChildStatus { + bool exited; // true: value is the exit status; false: value is the signal + int value; +}; + +[[nodiscard]] ChildStatus status_from(const siginfo_t& si) { + if (si.si_code == CLD_EXITED) { + return {.exited = true, .value = si.si_status}; + } + return {.exited = false, .value = si.si_status}; // CLD_KILLED / CLD_DUMPED +} + +// Prints the exit-observation line and returns the mirrored exit code. +int report_exit(pid_t pid, ChildStatus st) { + if (st.exited) { + std::println("pmon: child={} exited status={}", pid, st.value); + } else { + std::println("pmon: child={} killed signal={}", pid, st.value); + } + std::fflush(stdout); + return st.exited ? st.value : 128 + st.value; +} + +// Block `sigs` process-wide and return a signalfd that delivers them as data. +[[nodiscard]] std::expected +make_signalfd(std::initializer_list sigs) { + sigset_t mask; + sigemptyset(&mask); + for (const int s : sigs) { + sigaddset(&mask, s); + } + if (::sigprocmask(SIG_BLOCK, &mask, nullptr) != 0) { + return std::unexpected(last_error()); + } + return checked(::signalfd(-1, &mask, SFD_CLOEXEC)); +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror. +// --------------------------------------------------------------------------- + +int cmd_run(const std::vector& cmd) { + const auto pid = spawn_child(cmd); + if (!pid) { + std::println(stderr, "pmon: spawn: {}: {}", cmd[0], pid.error().message()); + return 1; + } + std::println(stderr, "pmon: run child={}", *pid); + siginfo_t si{}; + while (::waitid(P_PID, static_cast(*pid), &si, WEXITED) != 0) { + if (errno != EINTR) { + std::println(stderr, "pmon: waitid: {}", last_error().message()); + return 1; + } + } + return report_exit(*pid, status_from(si)); +} + +// --------------------------------------------------------------------------- +// v1/v2: supervise — two engines, one restart policy. +// --------------------------------------------------------------------------- + +enum class Engine : std::uint8_t { pidfd, sigchld }; +enum class Stop : std::uint8_t { signal, timeout }; +using Outcome = std::variant; + +[[nodiscard]] int remaining_ms(steady_clock::time_point deadline) { + const auto left = + std::chrono::duration_cast(deadline - steady_clock::now()).count(); + return left < 0 ? 0 : static_cast(left); +} + +// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +[[nodiscard]] std::expected reap_pidfd(const Fd& pidfd) { + siginfo_t si{}; + while (::waitid(P_PIDFD, static_cast(pidfd.get()), &si, WEXITED) != 0) { + if (errno != EINTR) { + return std::unexpected(last_error()); + } + } + return status_from(si); +} + +// pidfd engine: the child's exit is just readability on a file descriptor. +[[nodiscard]] std::expected +wait_pidfd(const Fd& pidfd, const Fd& sigfd, steady_clock::time_point deadline) { + for (;;) { + std::array fds{{ + {.fd = pidfd.get(), .events = POLLIN, .revents = 0}, + {.fd = sigfd.get(), .events = POLLIN, .revents = 0}, + }}; + const int n = ::poll(fds.data(), fds.size(), remaining_ms(deadline)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(last_error()); + } + if (n == 0) { + return Stop::timeout; + } + if ((fds[1].revents & POLLIN) != 0) { + signalfd_siginfo info{}; + std::ignore = ::read(sigfd.get(), &info, sizeof info); + return Stop::signal; + } + if (fds[0].revents != 0) { + return reap_pidfd(pidfd).transform([](ChildStatus st) { return Outcome{st}; }); + } + } +} + +// sigchld engine: SIGCHLD (and INT/TERM) arrive through the signalfd. +[[nodiscard]] std::expected +wait_sigchld(pid_t pid, const Fd& sigfd, steady_clock::time_point deadline) { + for (;;) { + pollfd pfd{.fd = sigfd.get(), .events = POLLIN, .revents = 0}; + const int n = ::poll(&pfd, 1, remaining_ms(deadline)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(last_error()); + } + if (n == 0) { + return Stop::timeout; + } + signalfd_siginfo info{}; + std::ignore = ::read(sigfd.get(), &info, sizeof info); + if (info.ssi_signo != SIGCHLD) { + return Stop::signal; + } + siginfo_t si{}; + si.si_pid = 0; + if (::waitid(P_PID, static_cast(pid), &si, WEXITED | WNOHANG) != 0) { + return std::unexpected(last_error()); + } + if (si.si_pid == pid) { + return Outcome{status_from(si)}; + } + // Coalesced or stale SIGCHLD: our child is still running. + } +} + +// Ask the child to terminate, reap it, and report why we are leaving. +int finish_stopped(Engine engine, pid_t pid, const Fd& pidfd, Stop why) { + std::expected st; + if (engine == Engine::pidfd) { + std::ignore = ::syscall(SYS_pidfd_send_signal, pidfd.get(), SIGTERM, nullptr, 0); + st = reap_pidfd(pidfd); + } else { + std::ignore = ::kill(pid, SIGTERM); + siginfo_t si{}; + for (;;) { + if (::waitid(P_PID, static_cast(pid), &si, WEXITED) == 0) { + st = status_from(si); + break; + } + if (errno != EINTR) { + st = std::unexpected(last_error()); + break; + } + } + } + if (!st) { + std::println(stderr, "pmon: waitid: {}", st.error().message()); + return 1; + } + std::ignore = report_exit(pid, *st); + std::println("pmon: exiting ({})", why == Stop::timeout ? "timeout" : "signal"); + std::fflush(stdout); + return 0; +} + +int cmd_supervise(Engine engine, int max_restarts, int timeout_ms, + const std::vector& cmd) { + auto sigfd = engine == Engine::pidfd ? make_signalfd({SIGINT, SIGTERM}) + : make_signalfd({SIGCHLD, SIGINT, SIGTERM}); + if (!sigfd) { + std::println(stderr, "pmon: signalfd: {}", sigfd.error().message()); + return 1; + } + const auto deadline = steady_clock::now() + milliseconds{timeout_ms}; + int restarts = 0; + for (;;) { + const auto pid = spawn_child(cmd); + if (!pid) { + std::println(stderr, "pmon: spawn: {}: {}", cmd[0], pid.error().message()); + return 1; + } + + Fd pidfd; + std::expected outcome; + if (engine == Engine::pidfd) { + auto pf = checked(static_cast(::syscall(SYS_pidfd_open, *pid, 0))); + if (!pf) { + std::println(stderr, "pmon: pidfd_open: {}", pf.error().message()); + return 1; + } + pidfd = std::move(*pf); + std::println(stderr, "pmon: engine=pidfd child={} pidfd={}", *pid, pidfd.get()); + outcome = wait_pidfd(pidfd, *sigfd, deadline); + } else { + std::println(stderr, "pmon: engine=sigchld child={}", *pid); + outcome = wait_sigchld(*pid, *sigfd, deadline); + } + if (!outcome) { + std::println(stderr, "pmon: wait: {}", outcome.error().message()); + return 1; + } + if (const auto* stop = std::get_if(&*outcome)) { + return finish_stopped(engine, *pid, pidfd, *stop); + } + + const auto st = std::get(*outcome); + std::ignore = report_exit(*pid, st); + if (st.exited && st.value == 0) { + return 0; + } + if (restarts >= max_restarts) { + std::println("pmon: giving up after {} restarts", max_restarts); + std::fflush(stdout); + return 1; + } + ++restarts; + std::println("pmon: restart {}/{}", restarts, max_restarts); + std::fflush(stdout); + } +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +int usage() { + std::println(stderr, "usage: pmon "); + std::println(stderr, + " run -- CMD [ARGS...] spawn CMD, wait, mirror its exit"); + std::println(stderr, + " supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T]"); + std::println(stderr, + " -- CMD [ARGS...] restart CMD on abnormal exit"); + std::println(stderr, + " (defaults: pidfd, N=3, T=10000)"); + return 2; +} + +[[nodiscard]] std::optional parse_int(std::string_view s) { + int value{}; + const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value); + if (ec != std::errc{} || ptr != s.data() + s.size()) { + return std::nullopt; + } + return value; +} + +} // namespace + +int main(int argc, char** argv) { + const std::vector args{argv + 1, argv + argc}; + if (args.empty()) { + return usage(); + } + const auto sep = std::ranges::find(args, std::string_view{"--"}); + if (sep == args.end() || sep + 1 == args.end()) { + return usage(); // both subcommands need `-- CMD [ARGS...]` + } + const std::vector flags{args.begin() + 1, sep}; + std::vector cmd; + for (auto it = sep + 1; it != args.end(); ++it) { + cmd.emplace_back(*it); + } + + if (args[0] == "run") { + if (!flags.empty()) { + return usage(); + } + return cmd_run(cmd); + } + if (args[0] == "supervise") { + auto engine = Engine::pidfd; + int max_restarts = 3; + int timeout_ms = 10'000; + for (std::size_t i = 0; i < flags.size(); i += 2) { + if (i + 1 >= flags.size()) { + return usage(); + } + const auto flag = flags[i]; + const auto value = flags[i + 1]; + if (flag == "--engine") { + if (value == "pidfd") { + engine = Engine::pidfd; + } else if (value == "sigchld") { + engine = Engine::sigchld; + } else { + return usage(); + } + } else if (flag == "--max-restarts") { + const auto n = parse_int(value); + if (!n || *n < 0) { + return usage(); + } + max_restarts = *n; + } else if (flag == "--timeout-ms") { + const auto t = parse_int(value); + if (!t || *t <= 0) { + return usage(); + } + timeout_ms = *t; + } else { + return usage(); + } + } + return cmd_supervise(engine, max_restarts, timeout_ms, cmd); + } + return usage(); +} diff --git a/examples/13-pidfd-modern-process-mgmt/demo.sh b/examples/13-pidfd-modern-process-mgmt/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/13-pidfd-modern-process-mgmt/go/demo.sh b/examples/13-pidfd-modern-process-mgmt/go/demo.sh new file mode 100755 index 0000000..cdfb6b6 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run supervise --timeout-ms 2000 -- sh -c 'exit 0' ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/13-pidfd-modern-process-mgmt/go/go.mod b/examples/13-pidfd-modern-process-mgmt/go/go.mod new file mode 100644 index 0000000..defdd6c --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/13-pidfd-modern-process-mgmt/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/13-pidfd-modern-process-mgmt/go/go.sum b/examples/13-pidfd-modern-process-mgmt/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/13-pidfd-modern-process-mgmt/go/main.go b/examples/13-pidfd-modern-process-mgmt/go/main.go new file mode 100644 index 0000000..87e9538 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/go/main.go @@ -0,0 +1,335 @@ +// pmon v2 — a tiny process supervisor grown chapter by chapter. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child, driven by +// SIGCHLD delivered through signal.Notify and reaped with wait4(WNOHANG). +// v2 (`supervise --engine pidfd`, the DEFAULT) drops the SIGCHLD dependence +// entirely: unix.PidfdOpen turns the child into a pollable file descriptor, +// a goroutine parked in unix.Poll reports the exit over a channel, a raw +// waitid(P_PIDFD, ...) reaps exactly that child, and the stop path signals +// through the fd with unix.PidfdSendSignal — no pid-reuse race anywhere. +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "strconv" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +// childStatus is the reaped state of one child. +type childStatus struct { + exited bool // true: value is the exit status; false: value is the signal + value int +} + +// reportExit prints the exit-observation line and returns the mirrored code. +func reportExit(pid int, st childStatus) int { + if st.exited { + fmt.Printf("pmon: child=%d exited status=%d\n", pid, st.value) + return st.value + } + fmt.Printf("pmon: child=%d killed signal=%d\n", pid, st.value) + return 128 + st.value +} + +func fromWaitStatus(ws syscall.WaitStatus) childStatus { + if ws.Signaled() { + return childStatus{exited: false, value: int(ws.Signal())} + } + return childStatus{exited: true, value: ws.ExitStatus()} +} + +func spawn(cmdline []string) (*exec.Cmd, error) { + c := exec.Command(cmdline[0], cmdline[1:]...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + if err := c.Start(); err != nil { + return nil, fmt.Errorf("spawn: %s: %w", cmdline[0], err) + } + return c, nil +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror. +// --------------------------------------------------------------------------- + +func cmdRun(cmdline []string) int { + c, err := spawn(cmdline) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + pid := c.Process.Pid + fmt.Fprintf(os.Stderr, "pmon: run child=%d\n", pid) + st := childStatus{exited: true, value: 0} + if err := c.Wait(); err != nil { + var ee *exec.ExitError + if !errors.As(err, &ee) { + fmt.Fprintf(os.Stderr, "pmon: wait: %v\n", err) + return 1 + } + st = fromWaitStatus(ee.Sys().(syscall.WaitStatus)) + } + return reportExit(pid, st) +} + +// --------------------------------------------------------------------------- +// v2 plumbing: waitid(P_PIDFD) with an explicit siginfo_t layout — x/sys +// keeps the union opaque, and the CLD_* fields are what we need. +// --------------------------------------------------------------------------- + +type siginfoChld struct { + signo int32 + errno int32 + code int32 + _ int32 // alignment padding before the union on 64-bit + pid int32 + uid uint32 + status int32 + _ [100]byte // pad siginfo_t out to its full 128 bytes +} + +const cldExited = 1 // si_code: child called _exit + +// reapPidfd blocks in waitid(P_PIDFD) until the process behind pidfd is +// reaped — it can never collect a recycled pid. +func reapPidfd(pidfd int) (childStatus, error) { + var si siginfoChld + for { + _, _, errno := unix.Syscall6(unix.SYS_WAITID, uintptr(unix.P_PIDFD), + uintptr(pidfd), uintptr(unsafe.Pointer(&si)), uintptr(unix.WEXITED), 0, 0) + if errno == unix.EINTR { + continue + } + if errno != 0 { + return childStatus{}, fmt.Errorf("waitid: %w", errno) + } + if si.code == cldExited { + return childStatus{exited: true, value: int(si.status)}, nil + } + return childStatus{exited: false, value: int(si.status)}, nil + } +} + +// tryReap collects the child if it has already exited (WNOHANG). +func tryReap(pid int) (childStatus, bool, error) { + var ws syscall.WaitStatus + for { + wpid, err := syscall.Wait4(pid, &ws, syscall.WNOHANG, nil) + if err == syscall.EINTR { + continue + } + if err != nil { + return childStatus{}, false, fmt.Errorf("wait4: %w", err) + } + if wpid != pid { + return childStatus{}, false, nil // coalesced/stale SIGCHLD + } + return fromWaitStatus(ws), true, nil + } +} + +func blockingReap(pid int) (childStatus, error) { + var ws syscall.WaitStatus + for { + _, err := syscall.Wait4(pid, &ws, 0, nil) + if err == syscall.EINTR { + continue + } + if err != nil { + return childStatus{}, fmt.Errorf("wait4: %w", err) + } + return fromWaitStatus(ws), nil + } +} + +// --------------------------------------------------------------------------- +// v1/v2: supervise — two engines, one restart policy. +// --------------------------------------------------------------------------- + +func cmdSupervise(engine string, maxRestarts int, timeout time.Duration, cmdline []string) int { + stop := make(chan os.Signal, 2) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + var chld chan os.Signal + if engine == "sigchld" { + chld = make(chan os.Signal, 16) + signal.Notify(chld, syscall.SIGCHLD) // registered BEFORE the first spawn + } + deadline := time.After(timeout) + + restarts := 0 + for { + c, err := spawn(cmdline) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + pid := c.Process.Pid + + var st childStatus + stopped := "" // "", "signal", "timeout" + + if engine == "pidfd" { + pidfd, err := unix.PidfdOpen(pid, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: pidfd_open: %v\n", err) + return 1 + } + fmt.Fprintf(os.Stderr, "pmon: engine=pidfd child=%d pidfd=%d\n", pid, pidfd) + exited := make(chan struct{}) + go func() { // the pidfd becomes readable exactly when the child exits + fds := []unix.PollFd{{Fd: int32(pidfd), Events: unix.POLLIN}} + for { + if _, err := unix.Poll(fds, -1); err == unix.EINTR { + continue + } + close(exited) + return + } + }() + select { + case <-exited: + case <-stop: + stopped = "signal" + case <-deadline: + stopped = "timeout" + } + if stopped != "" { + _ = unix.PidfdSendSignal(pidfd, unix.SIGTERM, nil, 0) + <-exited + } + st, err = reapPidfd(pidfd) + unix.Close(pidfd) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + } else { + fmt.Fprintf(os.Stderr, "pmon: engine=sigchld child=%d\n", pid) + done := false + for !done && stopped == "" { + select { + case <-chld: + s, reaped, err := tryReap(pid) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + if reaped { + st, done = s, true + } + case <-stop: + stopped = "signal" + case <-deadline: + stopped = "timeout" + } + } + if stopped != "" { + _ = syscall.Kill(pid, syscall.SIGTERM) + if st, err = blockingReap(pid); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + } + } + _ = c.Process.Release() // child already reaped; drop Go's process handle + + reportExit(pid, st) + if stopped != "" { + fmt.Printf("pmon: exiting (%s)\n", stopped) + return 0 + } + if st.exited && st.value == 0 { + return 0 + } + if restarts >= maxRestarts { + fmt.Printf("pmon: giving up after %d restarts\n", maxRestarts) + return 1 + } + restarts++ + fmt.Printf("pmon: restart %d/%d\n", restarts, maxRestarts) + } +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +func usage() int { + fmt.Fprint(os.Stderr, `usage: pmon + run -- CMD [ARGS...] spawn CMD, wait, mirror its exit + supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T] + -- CMD [ARGS...] restart CMD on abnormal exit + (defaults: pidfd, N=3, T=10000) +`) + return 2 +} + +func realMain(args []string) int { + if len(args) == 0 { + return usage() + } + sep := -1 + for i, a := range args { + if a == "--" { + sep = i + break + } + } + if sep < 0 || sep+1 >= len(args) { + return usage() // both subcommands need `-- CMD [ARGS...]` + } + flags := args[1:sep] + cmdline := args[sep+1:] + + switch args[0] { + case "run": + if len(flags) != 0 { + return usage() + } + return cmdRun(cmdline) + case "supervise": + engine := "pidfd" + maxRestarts := 3 + timeoutMs := 10000 + for i := 0; i < len(flags); i += 2 { + if i+1 >= len(flags) { + return usage() + } + switch flags[i] { + case "--engine": + if flags[i+1] != "pidfd" && flags[i+1] != "sigchld" { + return usage() + } + engine = flags[i+1] + case "--max-restarts": + n, err := strconv.Atoi(flags[i+1]) + if err != nil || n < 0 { + return usage() + } + maxRestarts = n + case "--timeout-ms": + t, err := strconv.Atoi(flags[i+1]) + if err != nil || t <= 0 { + return usage() + } + timeoutMs = t + default: + return usage() + } + } + return cmdSupervise(engine, maxRestarts, time.Duration(timeoutMs)*time.Millisecond, cmdline) + } + return usage() +} + +func main() { + os.Exit(realMain(os.Args[1:])) +} diff --git a/examples/13-pidfd-modern-process-mgmt/rust/Cargo.lock b/examples/13-pidfd-modern-process-mgmt/rust/Cargo.lock new file mode 100644 index 0000000..529a7de --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/rust/Cargo.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "pmon" +version = "0.2.0" +dependencies = [ + "anyhow", + "nix", + "rustix", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/examples/13-pidfd-modern-process-mgmt/rust/Cargo.toml b/examples/13-pidfd-modern-process-mgmt/rust/Cargo.toml new file mode 100644 index 0000000..966ac1e --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pmon" +version = "0.2.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +nix = { version = "0.30", features = ["signal", "process"] } +rustix = { version = "1", features = ["event", "process"] } diff --git a/examples/13-pidfd-modern-process-mgmt/rust/demo.sh b/examples/13-pidfd-modern-process-mgmt/rust/demo.sh new file mode 100755 index 0000000..463e344 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/rust/demo.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + # exec so a backgrounded `run supervise ...` can be signalled directly + exec "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run supervise --timeout-ms 2000 -- sh -c 'exit 0' ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/13-pidfd-modern-process-mgmt/rust/rust-toolchain.toml b/examples/13-pidfd-modern-process-mgmt/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/13-pidfd-modern-process-mgmt/rust/src/main.rs b/examples/13-pidfd-modern-process-mgmt/rust/src/main.rs new file mode 100644 index 0000000..56b2ce4 --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/rust/src/main.rs @@ -0,0 +1,347 @@ +// pmon v2 — a tiny process supervisor grown chapter by chapter. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child, driven by +// SIGCHLD delivered as readable data through a signalfd (nix). +// v2 (`supervise --engine pidfd`, the DEFAULT) drops the SIGCHLD dependence +// entirely: rustix::process::pidfd_open turns the child into a pollable +// OwnedFd, rustix::event::poll reports the exit as ordinary readability, +// waitid(WaitId::PidFd) reaps exactly that child, and the stop path +// signals through the fd with pidfd_send_signal — no pid-reuse race +// anywhere on the path. + +use std::os::fd::{AsFd, AsRawFd}; +use std::os::unix::process::{CommandExt, ExitStatusExt}; +use std::process::Command; +use std::time::Instant; + +use anyhow::{Context, Result}; +use nix::sys::signal::{kill, SigSet, Signal as NixSignal}; +use nix::sys::signalfd::{SfdFlags, SignalFd}; +use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; +use nix::unistd::Pid as NixPid; +use rustix::event::{poll, PollFd, PollFlags, Timespec}; +use rustix::process::{ + pidfd_open, pidfd_send_signal, waitid, Pid, PidfdFlags, Signal, WaitId, WaitIdOptions, +}; + +// --------------------------------------------------------------------------- +// Status plumbing shared by every subcommand. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +struct ChildStatus { + exited: bool, // true: value is the exit status; false: value is the signal + value: i32, +} + +/// Prints the exit-observation line and returns the mirrored exit code. +fn report_exit(pid: i32, st: ChildStatus) -> i32 { + if st.exited { + println!("pmon: child={} exited status={}", pid, st.value); + st.value + } else { + println!("pmon: child={} killed signal={}", pid, st.value); + 128 + st.value + } +} + +fn spawn(cmdline: &[String]) -> Result { + let mut cmd = Command::new(&cmdline[0]); + cmd.args(&cmdline[1..]); + // The supervisor blocks signals for its signalfd and the child inherits + // that mask across fork+exec (std does NOT reset it) — a supervised + // `sleep` with SIGTERM blocked would never stop. Clear the mask in the + // child; sigprocmask is async-signal-safe, so pre_exec may call it. + unsafe { + cmd.pre_exec(|| { + SigSet::empty() + .thread_set_mask() + .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) + }); + } + cmd.spawn().with_context(|| format!("spawn: {}", cmdline[0])) +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror. +// --------------------------------------------------------------------------- + +fn cmd_run(cmdline: &[String]) -> Result { + let mut child = spawn(cmdline)?; + let pid = child.id() as i32; + eprintln!("pmon: run child={pid}"); + let status = child.wait().context("wait")?; + let st = match status.signal() { + Some(sig) => ChildStatus { exited: false, value: sig }, + None => ChildStatus { exited: true, value: status.code().unwrap_or(0) }, + }; + Ok(report_exit(pid, st)) +} + +// --------------------------------------------------------------------------- +// v1/v2: supervise — two engines, one restart policy. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, PartialEq)] +enum Engine { + Pidfd, + Sigchld, +} + +#[derive(Clone, Copy)] +enum Stop { + Signal, + Timeout, +} + +enum Outcome { + Reaped(ChildStatus), + Stopped(Stop), +} + +fn remaining(deadline: Instant) -> Timespec { + let left = deadline.saturating_duration_since(Instant::now()); + Timespec { + tv_sec: left.as_secs() as i64, + tv_nsec: i64::from(left.subsec_nanos()), + } +} + +/// Block `sigs` process-wide and return a signalfd delivering them as data. +fn make_signalfd(sigs: &[NixSignal]) -> Result { + let mut mask = SigSet::empty(); + for s in sigs { + mask.add(*s); + } + mask.thread_block().context("sigprocmask")?; + SignalFd::with_flags(&mask, SfdFlags::SFD_CLOEXEC).context("signalfd") +} + +fn status_from_waitid(w: rustix::process::WaitIdStatus) -> ChildStatus { + if let Some(sig) = w.terminating_signal() { + ChildStatus { exited: false, value: sig as i32 } + } else { + ChildStatus { exited: true, value: w.exit_status().unwrap_or(0) } + } +} + +/// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +fn reap_pidfd(pidfd: &impl AsFd) -> Result { + let status = waitid(WaitId::PidFd(pidfd.as_fd()), WaitIdOptions::EXITED) + .context("waitid(P_PIDFD)")? + .context("waitid(P_PIDFD): no status")?; + Ok(status_from_waitid(status)) +} + +/// pidfd engine: the child's exit is just readability on a file descriptor. +fn wait_pidfd(pidfd: &impl AsFd, sigfd: &SignalFd, deadline: Instant) -> Result { + loop { + let mut fds = [ + PollFd::new(pidfd, PollFlags::IN), + PollFd::new(sigfd, PollFlags::IN), + ]; + let n = match poll(&mut fds, Some(&remaining(deadline))) { + Err(rustix::io::Errno::INTR) => continue, + other => other.context("poll")?, + }; + if n == 0 { + return Ok(Outcome::Stopped(Stop::Timeout)); + } + if fds[1].revents().contains(PollFlags::IN) { + let _ = sigfd.read_signal(); + return Ok(Outcome::Stopped(Stop::Signal)); + } + if !fds[0].revents().is_empty() { + return Ok(Outcome::Reaped(reap_pidfd(pidfd)?)); + } + } +} + +/// sigchld engine: SIGCHLD (and INT/TERM) arrive through the signalfd. +fn wait_sigchld(pid: NixPid, sigfd: &SignalFd, deadline: Instant) -> Result { + loop { + let mut fds = [PollFd::new(sigfd, PollFlags::IN)]; + let n = match poll(&mut fds, Some(&remaining(deadline))) { + Err(rustix::io::Errno::INTR) => continue, + other => other.context("poll")?, + }; + if n == 0 { + return Ok(Outcome::Stopped(Stop::Timeout)); + } + let info = sigfd.read_signal().context("read signalfd")?; + let signo = info.map(|si| si.ssi_signo as i32).unwrap_or(0); + if signo != NixSignal::SIGCHLD as i32 { + return Ok(Outcome::Stopped(Stop::Signal)); + } + match waitpid(pid, Some(WaitPidFlag::WNOHANG)).context("waitpid")? { + WaitStatus::Exited(_, code) => { + return Ok(Outcome::Reaped(ChildStatus { exited: true, value: code })); + } + WaitStatus::Signaled(_, sig, _) => { + return Ok(Outcome::Reaped(ChildStatus { exited: false, value: sig as i32 })); + } + _ => {} // coalesced or stale SIGCHLD: our child is still running + } + } +} + +fn blocking_reap(pid: NixPid) -> Result { + match waitpid(pid, None).context("waitpid")? { + WaitStatus::Exited(_, code) => Ok(ChildStatus { exited: true, value: code }), + WaitStatus::Signaled(_, sig, _) => Ok(ChildStatus { exited: false, value: sig as i32 }), + other => anyhow::bail!("waitpid: unexpected status {other:?}"), + } +} + +fn cmd_supervise( + engine: Engine, + max_restarts: u32, + timeout_ms: u64, + cmdline: &[String], +) -> Result { + let sigfd = match engine { + Engine::Pidfd => make_signalfd(&[NixSignal::SIGINT, NixSignal::SIGTERM])?, + Engine::Sigchld => { + // The mask (and the signalfd over it) exist BEFORE the first + // spawn, so an early exit's SIGCHLD is queued, never lost. + make_signalfd(&[NixSignal::SIGCHLD, NixSignal::SIGINT, NixSignal::SIGTERM])? + } + }; + let deadline = Instant::now() + std::time::Duration::from_millis(timeout_ms); + + let mut restarts: u32 = 0; + loop { + let child = spawn(cmdline)?; + let pid = child.id() as i32; + + let (outcome, pidfd) = match engine { + Engine::Pidfd => { + let pidfd = pidfd_open(Pid::from_raw(pid).unwrap(), PidfdFlags::empty()) + .context("pidfd_open")?; + eprintln!("pmon: engine=pidfd child={pid} pidfd={}", pidfd.as_raw_fd()); + (wait_pidfd(&pidfd, &sigfd, deadline)?, Some(pidfd)) + } + Engine::Sigchld => { + eprintln!("pmon: engine=sigchld child={pid}"); + (wait_sigchld(NixPid::from_raw(pid), &sigfd, deadline)?, None) + } + }; + + match outcome { + Outcome::Stopped(why) => { + // Ask the child to terminate, reap it, report why we leave. + let st = match &pidfd { + Some(fd) => { + let _ = pidfd_send_signal(fd, Signal::TERM); + reap_pidfd(fd)? + } + None => { + let _ = kill(NixPid::from_raw(pid), NixSignal::SIGTERM); + blocking_reap(NixPid::from_raw(pid))? + } + }; + let _ = report_exit(pid, st); + let why = match why { + Stop::Timeout => "timeout", + Stop::Signal => "signal", + }; + println!("pmon: exiting ({why})"); + return Ok(0); + } + Outcome::Reaped(st) => { + let _ = report_exit(pid, st); + if st.exited && st.value == 0 { + return Ok(0); + } + if restarts >= max_restarts { + println!("pmon: giving up after {max_restarts} restarts"); + return Ok(1); + } + restarts += 1; + println!("pmon: restart {restarts}/{max_restarts}"); + } + } + } +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +fn usage() -> i32 { + eprint!( + "usage: pmon \n\ + \x20 run -- CMD [ARGS...] spawn CMD, wait, mirror its exit\n\ + \x20 supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T]\n\ + \x20 -- CMD [ARGS...] restart CMD on abnormal exit\n\ + \x20 (defaults: pidfd, N=3, T=10000)\n" + ); + 2 +} + +fn real_main() -> i32 { + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + return usage(); + } + let Some(sep) = args.iter().position(|a| a == "--") else { + return usage(); // both subcommands need `-- CMD [ARGS...]` + }; + if sep + 1 >= args.len() { + return usage(); + } + let flags = &args[1..sep]; + let cmdline = &args[sep + 1..]; + + let result = match args[0].as_str() { + "run" => { + if !flags.is_empty() { + return usage(); + } + cmd_run(cmdline) + } + "supervise" => { + let mut engine = Engine::Pidfd; + let mut max_restarts: u32 = 3; + let mut timeout_ms: u64 = 10_000; + let mut it = flags.iter(); + loop { + let Some(flag) = it.next() else { break }; + let Some(value) = it.next() else { + return usage(); + }; + match flag.as_str() { + "--engine" => match value.as_str() { + "pidfd" => engine = Engine::Pidfd, + "sigchld" => engine = Engine::Sigchld, + _ => return usage(), + }, + "--max-restarts" => match value.parse::() { + Ok(n) => max_restarts = n, + Err(_) => return usage(), + }, + "--timeout-ms" => match value.parse::() { + Ok(t) if t > 0 => timeout_ms = t, + _ => return usage(), + }, + _ => return usage(), + } + } + cmd_supervise(engine, max_restarts, timeout_ms, cmdline) + } + _ => return usage(), + }; + + match result { + Ok(code) => code, + Err(e) => { + eprintln!("pmon: {e:#}"); + 1 + } + } +} + +fn main() { + std::process::exit(real_main()); +} diff --git a/examples/13-pidfd-modern-process-mgmt/verify.lua b/examples/13-pidfd-modern-process-mgmt/verify.lua new file mode 100644 index 0000000..de4703e --- /dev/null +++ b/examples/13-pidfd-modern-process-mgmt/verify.lua @@ -0,0 +1,168 @@ +-- Verify pmon v2 (13-pidfd-modern-process-mgmt) for LSP_LANG. +-- +-- Asserts observable behavior, identical across cpp/go/rust: +-- 1. v0 regression — `run` mirrors the child's exit status and signal +-- 2. pidfd engine (default) supervises a crashing child: per-spawn +-- "engine=pidfd child= pidfd=" lines, restart lines in order, +-- a giving-up line, exit 1 +-- 3. sigchld engine still works and produces byte-identical restart lines +-- 4. clean child exit ends supervision with exit 0 and no restart +-- 5. stop paths — SIGTERM to pmon and the overall timeout both terminate +-- the child (killed signal=15), print the exiting line, exit 0, and +-- leave no orphaned child behind (process-tree check via pgrep) +-- 6. error shapes — usage errors exit 2, spawn failure exits 1 +-- +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set; the interpreter may be lua or luajit. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" + +local function count(out, pattern) + local _, n = out:gsub(pattern, "") + return n +end + +-- All restart lines of one run, in order, as a single string. +local function restart_seq(out) + local t = {} + for line in out:gmatch("pmon: restart %d+/%d+") do + t[#t + 1] = line + end + return table.concat(t, "\n") +end + +-- --------------------------------------------------------------------------- +-- 1. v0 regression: run mirrors exit status and death-by-signal +-- --------------------------------------------------------------------------- + +local ok0 = checks.run(demo .. " run -- sh -c 'exit 0'") +checks.expect_exit(ok0, 0, lang .. ": run mirrors a clean exit as 0") +checks.expect_match(ok0.out, "pmon: run child=%d+", lang .. ": run announces the child pid") +checks.expect_match(ok0.out, "pmon: child=%d+ exited status=0", lang .. ": run reports status 0") + +local ok3 = checks.run(demo .. " run -- sh -c 'exit 3'") +checks.expect_exit(ok3, 3, lang .. ": run mirrors exit status 3") +checks.expect_match(ok3.out, "pmon: child=%d+ exited status=3", lang .. ": run reports status 3") + +local sig9 = checks.run(demo .. " run -- sh -c 'kill -9 $$'") +checks.expect_exit(sig9, 137, lang .. ": run mirrors SIGKILL as 128+9") +checks.expect_match(sig9.out, "pmon: child=%d+ killed signal=9", lang .. ": run reports the killing signal") + +-- --------------------------------------------------------------------------- +-- 2. pidfd engine (default): crashing child is restarted, then given up on +-- --------------------------------------------------------------------------- + +local crash_pidfd = checks.run(demo .. " supervise --max-restarts 2 -- sh -c 'exit 1'") +checks.expect_exit(crash_pidfd, 1, lang .. ": pidfd engine exits 1 after the restart budget") +checks.expect_match(crash_pidfd.out, "pmon: engine=pidfd child=%d+ pidfd=%d+", + lang .. ": pidfd engine announces child pid AND pidfd number") +checks.expect_match("spawns=" .. count(crash_pidfd.out, "pmon: engine=pidfd child=%d+ pidfd=%d+"), + "^spawns=3$", lang .. ": pidfd engine spawns initial child + 2 restarts") +checks.expect_match("exits=" .. count(crash_pidfd.out, "pmon: child=%d+ exited status=1"), + "^exits=3$", lang .. ": every crash is observed and reaped") +checks.expect_match(crash_pidfd.out, + "pmon: restart 1/2.*pmon: restart 2/2.*pmon: giving up after 2 restarts", + lang .. ": restart lines in order, then the giving-up line") + +-- --------------------------------------------------------------------------- +-- 3. sigchld engine: same behavior, byte-identical restart lines +-- --------------------------------------------------------------------------- + +local crash_sigchld = checks.run(demo .. + " supervise --engine sigchld --max-restarts 2 -- sh -c 'exit 1'") +checks.expect_exit(crash_sigchld, 1, lang .. ": sigchld engine exits 1 after the restart budget") +checks.expect_match("spawns=" .. count(crash_sigchld.out, "pmon: engine=sigchld child=%d+"), + "^spawns=3$", lang .. ": sigchld engine announces each spawn without a pidfd") +checks.expect_match("pidfd_lines=" .. count(crash_sigchld.out, "pidfd="), + "^pidfd_lines=0$", lang .. ": sigchld engine output has no pidfd= field") +checks.expect_match(crash_sigchld.out, "pmon: giving up after 2 restarts", + lang .. ": sigchld engine gives up identically") + +local seq_pidfd, seq_sigchld = restart_seq(crash_pidfd.out), restart_seq(crash_sigchld.out) +checks.expect_match(seq_pidfd, "^pmon: restart 1/2\npmon: restart 2/2$", + lang .. ": pidfd restart lines are exactly restart 1/2 then 2/2") +checks.expect_match(seq_pidfd == seq_sigchld and "identical" or + ("pidfd<<" .. seq_pidfd .. ">> sigchld<<" .. seq_sigchld .. ">>"), + "^identical$", lang .. ": restart lines identical across both engines") + +-- --------------------------------------------------------------------------- +-- 4. clean child exit: supervision ends, no restart +-- --------------------------------------------------------------------------- + +local clean = checks.run(demo .. " supervise -- sh -c 'exit 0'") +checks.expect_exit(clean, 0, lang .. ": clean child exit ends supervision with 0") +checks.expect_match(clean.out, "pmon: engine=pidfd child=%d+ pidfd=%d+", + lang .. ": clean run still uses the pidfd engine by default") +checks.expect_match("restarts=" .. count(clean.out, "pmon: restart"), + "^restarts=0$", lang .. ": a clean exit is never restarted") + +-- --------------------------------------------------------------------------- +-- 5. stop paths: SIGTERM to pmon, and the overall timeout +-- --------------------------------------------------------------------------- + +local wd = checks.run("mktemp -d").out:gsub("%s+$", "") +if wd == "" or not wd:match("^/") then + checks.skip("mktemp -d did not yield a directory") +end + +for _, engine in ipairs({ "pidfd", "sigchld" }) do + local stop = checks.run([[ +set -e +]] .. demo .. [[ supervise --engine ]] .. engine .. [[ -- sleep 8.88 > ]] .. wd .. [[/stop.out 2>&1 & +pid=$! +sleep 0.5 +kill -TERM $pid +wait $pid +cat ]] .. wd .. [[/stop.out +]]) + checks.expect_exit(stop, 0, lang .. ": " .. engine .. " engine exits 0 when pmon gets SIGTERM") + checks.expect_match(stop.out, "pmon: child=%d+ killed signal=15", + lang .. ": " .. engine .. " stop path forwards SIGTERM to the child") + checks.expect_match(stop.out, "pmon: exiting %(signal%)", + lang .. ": " .. engine .. " stop path prints the signal exit line") +end + +local timeout = checks.run(demo .. " supervise --timeout-ms 500 -- sleep 7.77") +checks.expect_exit(timeout, 0, lang .. ": timeout stop exits 0") +checks.expect_match(timeout.out, "pmon: child=%d+ killed signal=15", + lang .. ": timeout stop terminates the child via the pidfd") +checks.expect_match(timeout.out, "pmon: exiting %(timeout%)", + lang .. ": timeout stop prints the timeout exit line") + +-- Process-tree check: the supervised sleep must NOT survive its supervisor. +-- ([.] keeps this probe's own shell command line from matching itself.) +local orphans = checks.run("pgrep -f 'sleep 7[.]77'; pgrep -f 'sleep 8[.]88'; true") +checks.expect_match("survivors=<" .. orphans.out:gsub("%s+", "") .. ">", + "^survivors=<>$", lang .. ": no supervised child outlives pmon") + +-- --------------------------------------------------------------------------- +-- 6. error shapes +-- --------------------------------------------------------------------------- + +local bogus = checks.run(demo .. " bogus -- true") +checks.expect_exit(bogus, 2, lang .. ": unknown subcommand exits 2") +checks.expect_match(bogus.out, "usage: pmon", lang .. ": unknown subcommand prints usage") + +local nosep = checks.run(demo .. " supervise sh -c 'exit 0'") +checks.expect_exit(nosep, 2, lang .. ": missing -- separator exits 2") + +local badengine = checks.run(demo .. " supervise --engine epoll -- true") +checks.expect_exit(badengine, 2, lang .. ": unknown engine exits 2") + +local nospawn = checks.run(demo .. " supervise -- /no/such/binary") +checks.expect_exit(nospawn, 1, lang .. ": unspawnable command exits 1") +checks.expect_match(nospawn.out, "pmon: spawn: /no/such/binary", + lang .. ": spawn failure names the command") + +checks.run("rm -rf " .. wd) +checks.finish() diff --git a/examples/14-identity-and-privilege/README.md b/examples/14-identity-and-privilege/README.md new file mode 100644 index 0000000..143fc9e --- /dev/null +++ b/examples/14-identity-and-privilege/README.md @@ -0,0 +1,119 @@ +# 14 — Identity & privilege (`pmon` v3) + +**VM example.** This chapter drops privilege the right way: a root process +hands a command to an unprivileged user while carrying exactly one capability — +`CAP_NET_BIND_SERVICE` — across the identity change and the following +`execve(2)`. The whole lesson is the **order of operations**. Get the sequence +wrong and either the capability evaporates (the command can't bind `:80`) or it +never drops (the command runs as root, and you've proven nothing). + +`pmon drop` becomes user `` and then `exec`s `CMD`. `pmon bindprobe` +tries to bind a TCP port and reports its uid — the observable proof: **binding a +port below 1024 as a non-root uid is impossible without the ambient cap.** + +| Language | How the drop is performed | +|---|---| +| **C++23** | Raw `prctl(2)` / `capset(2)` / `setres[ug]id(2)` in-process, then `execvp`. Single-threaded on purpose (a privilege drop must cover every task, and `execve` only makes sense from one thread). RAII owns the socket; `std::expected` carries every syscall failure; no C-style `perror`/`exit`. | +| **Rust (edition 2024)** | The same raw `libc` sequence, mirroring the C++ path; `OwnedFd` gives the socket RAII and errors flow through `Result`/`?`. Capability structs are `#[repr(C)]`; the two ABI constants libc omits (`_LINUX_CAPABILITY_VERSION_3`, `CAP_NET_BIND_SERVICE`) are defined locally. | +| **Go 1.26** | The idiomatic inversion. A Go program is already multithreaded and `setuid`/`capset` act on the calling thread only, so doing this in-process is a footgun. Instead the drop is arranged **pre-exec** via `syscall.SysProcAttr` — `Credential` (setgid/setgroups/setuid) plus `AmbientCaps` (which the runtime turns into the `PR_SET_KEEPCAPS → capset → PR_CAP_AMBIENT_RAISE` dance in the fork/exec child). Same observable result; the ordering lives in the standard library instead of in our code. | + +All three produce byte-for-byte identical output and exit codes, so one +`verify.lua` covers them. + +## The sequence (C++/Rust in-process path) + +``` +1. prctl(PR_SET_KEEPCAPS, 1) # BEFORE the uid change, so the permitted set + # is not scrubbed when uid goes 0 -> non-zero +2. setgroups([gid]) # groups first... + setresgid(gid, gid, gid) # ...gid before uid (can't set gid once !root) + setresuid(uid, uid, uid) # now unprivileged, permitted caps retained +3. capset(CAP_NET_BIND_SERVICE) # pin it into permitted AND inheritable + # (ambient requires both), drop everything else +4. prctl(PR_CAP_AMBIENT_RAISE, # raise into the ambient set — the only set a + CAP_NET_BIND_SERVICE) # non-root, no-file-cap execve preserves +5. execvp(CMD) # CMD runs as , still able to bind :80 +``` + +Drop `--keep-cap` and steps 1, 3 and 4 vanish: after `execvp` the child has an +empty permitted set (no file caps, non-root uid) and `:80` is denied. That's +the negative control. + +## CLI + +``` +usage: + pmon drop --user [--keep-cap net_bind_service] -- CMD [args...] + pmon bindprobe [--port 80] +``` + +- `drop --user ` — must run as root; becomes `` and `exec`s `CMD`. + With `--keep-cap net_bind_service` it carries `CAP_NET_BIND_SERVICE` across + the drop; without it, `CMD` runs with no capabilities. `CMD`'s own exit + status is propagated. +- `bindprobe [--port 80]` — binds a TCP socket on the port. On success prints + `bindprobe: uid= bound :` to stdout and exits `0`. On failure prints + `bindprobe: bind :: ` to stderr and exits `3`. +- Exit codes: `0` success, `1` runtime error (not root, unknown user, exec + failure), `2` usage error, `3` `bindprobe` bind failure. + +## Demo contract + +Each language directory has the standard `demo.sh`: + +- `./demo.sh build` — build only +- `./demo.sh run [args]` — run the built binary (`app`) +- With env `TARGET` set, `run` deploys to that lab VM via + `scripts/lab/deploy-to-vm.sh`; set `SUDO=1` for the `drop` path, which needs + root on the guest + +The top-level `./demo.sh [cpp|go|rust|all|build]` dispatches per language. + +## Try it (on the lab VM) + +```sh +export LIBVIRT_DEFAULT_URI=qemu:///system TARGET=systems-target +cd examples/14-identity-and-privilege + +# stage a world-readable copy of the exec target (/home/fedora is mode 700, +# unreachable by the dropped user; /tmp is world-accessible) +IP=$(../../scripts/lab/vm-ip.sh systems-target) +scp cpp/build/release/app "fedora@$IP:/tmp/pmon-verify" +ssh "fedora@$IP" 'chmod 755 /tmp/pmon-verify' + +# keep the cap: nobody binds :80 -> "bindprobe: uid=65534 bound :80", exit 0 +SUDO=1 ./demo.sh cpp run drop --user nobody --keep-cap net_bind_service -- /tmp/pmon-verify bindprobe --port 80 + +# negative control: drop the cap -> "bindprobe: bind :80: Permission denied", exit 3 +SUDO=1 ./demo.sh cpp run drop --user nobody -- /tmp/pmon-verify bindprobe --port 80 +``` + +## Verification + +`verify.lua` (run per language under the runner's **vm mode**, +`TARGET=systems-target` with `SUDO=1`) asserts the paired behavior that makes +this example real, not merely exit-0: + +1. **Positive** — `drop --user nobody --keep-cap net_bind_service` running + `bindprobe --port 80` prints `bindprobe: uid=65534 bound :80` and exits `0`. + The uid is captured and asserted **non-zero**: an unprivileged process bound + a privileged port, which is only possible because the ambient cap survived. +2. **Negative control** — the *same* drop **without** `--keep-cap` prints + `bindprobe: bind :80: Permission denied` and exits `3`. Same uid, cap + withheld, port denied — this is what proves assertion 1 came from the + capability and not from a stray root or a lowered + `net.ipv4.ip_unprivileged_port_start`. + +Run it: + +```sh +LIBVIRT_DEFAULT_URI=qemu:///system \ + python3 scripts/test-all-examples.py --only 14-identity-and-privilege --mode vm +``` + +**Why observe the effect, not a trace:** you cannot `strace` across a +`setuid`+`execve` boundary to confirm the cap moved — `ptrace` is dropped at +the privilege transition. The port bind *is* the observation: `:80` bound at +`uid=65534` is a fact the kernel only permits when `CAP_NET_BIND_SERVICE` is in +the effective set. The negative control closes the loop by showing the same +process denied when the cap is withheld. diff --git a/examples/14-identity-and-privilege/cpp/.clang-format b/examples/14-identity-and-privilege/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/14-identity-and-privilege/cpp/.clang-tidy b/examples/14-identity-and-privilege/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/14-identity-and-privilege/cpp/CMakeLists.txt b/examples/14-identity-and-privilege/cpp/CMakeLists.txt new file mode 100644 index 0000000..c945061 --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/14-identity-and-privilege/cpp/CMakePresets.json b/examples/14-identity-and-privilege/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/14-identity-and-privilege/cpp/demo.sh b/examples/14-identity-and-privilege/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/14-identity-and-privilege/cpp/src/main.cpp b/examples/14-identity-and-privilege/cpp/src/main.cpp new file mode 100644 index 0000000..a5053aa --- /dev/null +++ b/examples/14-identity-and-privilege/cpp/src/main.cpp @@ -0,0 +1,248 @@ +// pmon v3 — identity & privilege (chapter 14). +// +// Subcommands: +// pmon drop --user [--keep-cap net_bind_service] -- CMD [args...] +// pmon bindprobe [--port 80] +// +// `drop` runs as root and hands CMD to an UNPRIVILEGED user, optionally +// carrying exactly one capability (CAP_NET_BIND_SERVICE) across the identity +// change and the following execve(2) via the ambient-capability mechanism. +// The whole lesson is the ORDER of operations: +// +// 1. prctl(PR_SET_KEEPCAPS, 1) — before the uid change, so the permitted +// set survives setresuid(2) instead of +// being scrubbed when uid goes 0 -> !0. +// 2. setgroups / setresgid / setresuid — become the target user. +// 3. capset(2) — pin CAP_NET_BIND_SERVICE into the +// permitted AND inheritable sets (ambient +// requires both), dropping everything else. +// 4. prctl(PR_CAP_AMBIENT_RAISE) — raise it into the ambient set, the only +// set that a non-root, non-file-cap execve +// preserves. +// 5. execvp(2) — CMD now runs as yet keeps :80. +// +// Raw prctl(2) / capset(2) / setres*id(2) on purpose — no libcap. RAII owns +// the socket; std::expected carries syscall failures; nothing is handled the +// C way (no scattered perror/exit). Deliberately single-threaded: a privilege +// drop must touch every task, and execve() only makes sense from one thread. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// A syscall failure: what we were attempting, plus the captured errno. +struct SysErr { + std::string what; + int err; +}; + +// Wrap a raw syscall return: <0 becomes an error carrying errno. +std::expected checked(long rc, std::string_view what) { + if (rc < 0) { + return std::unexpected(SysErr{std::string(what), errno}); + } + return {}; +} + +// RAII owner for a file descriptor — closed exactly once on scope exit. +class Fd { + public: + explicit Fd(int fd) noexcept : fd_(fd) {} + ~Fd() { + if (fd_ >= 0) ::close(fd_); + } + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + Fd& operator=(Fd&&) = delete; + + [[nodiscard]] int get() const noexcept { return fd_; } + [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; } + + private: + int fd_ = -1; +}; + +// ---- bindprobe ----------------------------------------------------------- +// +// Bind a TCP socket on and report the effective uid. The whole point: +// binding a port < 1024 is impossible for a non-root process UNLESS it holds +// CAP_NET_BIND_SERVICE. Success at uid!=0 on :80 is the proof the ambient cap +// survived. Failure prints a stable "Permission denied" line and exits 3. +int cmd_bindprobe(std::span args) { + int port = 80; + for (std::size_t i = 0; i < args.size(); ++i) { + std::string_view a = args[i]; + if (a == "--port" && i + 1 < args.size()) { + port = std::atoi(args[++i]); + } else { + std::println(stderr, "bindprobe: unexpected argument: {}", a); + return 2; + } + } + + Fd sock(::socket(AF_INET, SOCK_STREAM, 0)); + if (!sock.valid()) { + std::println(stderr, "bindprobe: socket: {}", std::strerror(errno)); + return 1; + } + int one = 1; + ::setsockopt(sock.get(), SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(static_cast(port)); + + if (::bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)) != 0) { + std::println(stderr, "bindprobe: bind :{}: {}", port, std::strerror(errno)); + return 3; + } + std::println("bindprobe: uid={} bound :{}", ::getuid(), port); + return 0; +} + +// ---- drop ---------------------------------------------------------------- + +// Perform the identity change (and optional cap retention), then execvp CMD. +// Returns only on failure; on success control never comes back. +std::expected arrange_and_exec(const passwd& pw, bool keep_cap, + std::span cmd) { + // (1) Retain the permitted set across the coming uid change. Must precede + // setresuid — after it, it is too late. + if (keep_cap) { + if (auto r = checked(::prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), "PR_SET_KEEPCAPS"); + !r) + return r; + } + + // (2) Become the target user: groups first, gid before uid — once uid drops + // we can no longer change gids. + gid_t gid = pw.pw_gid; + if (auto r = checked(::setgroups(1, &gid), "setgroups"); !r) return r; + if (auto r = checked(::setresgid(gid, gid, gid), "setresgid"); !r) return r; + if (auto r = checked(::setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid), "setresuid"); + !r) + return r; + + // (3) Pin exactly CAP_NET_BIND_SERVICE into permitted+inheritable (ambient + // demands the cap live in both), dropping every other retained cap. + if (keep_cap) { + __user_cap_header_struct hdr{}; + hdr.version = _LINUX_CAPABILITY_VERSION_3; + hdr.pid = 0; + std::array<__user_cap_data_struct, 2> data{}; + const std::uint32_t bit = 1u << CAP_NET_BIND_SERVICE; // cap 10, word 0 + data[0].effective = bit; + data[0].permitted = bit; + data[0].inheritable = bit; + if (auto r = checked(::syscall(SYS_capset, &hdr, data.data()), "capset"); !r) + return r; + + // (4) Raise it into the ambient set — the only set an unprivileged execve + // of a file without file-caps carries forward. + if (auto r = checked(::prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, + CAP_NET_BIND_SERVICE, 0, 0), + "PR_CAP_AMBIENT_RAISE"); + !r) + return r; + } + + // (5) Hand off. CMD now runs as , keeping :80 only if we raised it. + std::vector args(cmd.begin(), cmd.end()); + args.push_back(nullptr); + ::execvp(args[0], args.data()); + return std::unexpected(SysErr{std::string("execvp ") + args[0], errno}); +} + +int cmd_drop(std::span args) { + std::string_view user; + bool keep_cap = false; + std::span cmd; + for (std::size_t i = 0; i < args.size(); ++i) { + std::string_view a = args[i]; + if (a == "--user" && i + 1 < args.size()) { + user = args[++i]; + } else if (a == "--keep-cap" && i + 1 < args.size()) { + std::string_view cap = args[++i]; + if (cap != "net_bind_service") { + std::println(stderr, "drop: unsupported --keep-cap: {}", cap); + return 2; + } + keep_cap = true; + } else if (a == "--") { + cmd = args.subspan(i + 1); + break; + } else { + std::println(stderr, "drop: unexpected argument: {}", a); + return 2; + } + } + if (user.empty()) { + std::println(stderr, "drop: --user is required"); + return 2; + } + if (cmd.empty()) { + std::println(stderr, "drop: missing -- CMD"); + return 2; + } + if (::getuid() != 0) { + std::println(stderr, "drop: must run as root"); + return 1; + } + + errno = 0; + passwd* pw = ::getpwnam(std::string(user).c_str()); + if (pw == nullptr) { + std::println(stderr, "drop: unknown user: {}", user); + return 1; + } + + auto r = arrange_and_exec(*pw, keep_cap, cmd); + // Only reached if a step failed (exec never returns on success). + std::println(stderr, "drop: {}: {}", r.error().what, std::strerror(r.error().err)); + return 1; +} + +void usage() { + std::println(stderr, + "usage:\n" + " pmon drop --user [--keep-cap net_bind_service] -- CMD [args...]\n" + " pmon bindprobe [--port 80]"); +} + +} // namespace + +int main(int argc, char** argv) { + std::span all(argv, static_cast(argc)); + if (all.size() < 2) { + usage(); + return 2; + } + std::string_view sub = all[1]; + std::span rest = all.subspan(2); + if (sub == "drop") return cmd_drop(rest); + if (sub == "bindprobe") return cmd_bindprobe(rest); + usage(); + return 2; +} diff --git a/examples/14-identity-and-privilege/demo.sh b/examples/14-identity-and-privilege/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/14-identity-and-privilege/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/14-identity-and-privilege/go/demo.sh b/examples/14-identity-and-privilege/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/14-identity-and-privilege/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/14-identity-and-privilege/go/go.mod b/examples/14-identity-and-privilege/go/go.mod new file mode 100644 index 0000000..489b762 --- /dev/null +++ b/examples/14-identity-and-privilege/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/14-identity-and-privilege/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/14-identity-and-privilege/go/go.sum b/examples/14-identity-and-privilege/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/14-identity-and-privilege/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/14-identity-and-privilege/go/main.go b/examples/14-identity-and-privilege/go/main.go new file mode 100644 index 0000000..29fbd56 --- /dev/null +++ b/examples/14-identity-and-privilege/go/main.go @@ -0,0 +1,191 @@ +// pmon v3 — identity & privilege (chapter 14). +// +// Subcommands: +// +// pmon drop --user [--keep-cap net_bind_service] -- CMD [args...] +// pmon bindprobe [--port 80] +// +// Go note — why this differs from the C++/Rust in-process drop: +// +// A Go program is multithreaded before main() runs, and setresuid(2) / +// capset(2) act on the CALLING thread only. There is no portable way to make +// every runtime thread drop privilege in lockstep from user code, so doing the +// dance in-process is a footgun. The idiomatic Go answer is to let the runtime +// do it in the child between fork and exec: syscall.SysProcAttr carries a +// Credential (the setgid/setgroups/setresuid) and AmbientCaps (the +// PR_SET_KEEPCAPS -> capset -> PR_CAP_AMBIENT_RAISE sequence) which the fork/exec +// child applies in the correct order for us. Same observable result as the raw +// C++/Rust path — the ordering lives in the standard library instead of here. +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "os/user" + "strconv" + "syscall" + + "golang.org/x/sys/unix" +) + +// errnoText reconstructs libc's strerror(3) spelling (capitalised first letter, +// e.g. "Permission denied") from Go's lower-cased errno string, so the failure +// line is byte-for-byte identical to the C++ and Rust builds. +func errnoText(err error) string { + s := err.Error() + if s == "" { + return s + } + b := []byte(s) + if b[0] >= 'a' && b[0] <= 'z' { + b[0] -= 'a' - 'A' + } + return string(b) +} + +// ---- bindprobe ----------------------------------------------------------- +// +// Bind a TCP socket on and report the effective uid. Binding a port +// below 1024 is impossible for a non-root process without CAP_NET_BIND_SERVICE; +// success at uid!=0 on :80 proves the ambient cap survived the drop+exec. +func cmdBindprobe(args []string) int { + port := 80 + for i := 0; i < len(args); i++ { + switch args[i] { + case "--port": + if i+1 >= len(args) { + fmt.Fprintln(os.Stderr, "bindprobe: --port needs a value") + return 2 + } + p, err := strconv.Atoi(args[i+1]) + if err != nil { + fmt.Fprintf(os.Stderr, "bindprobe: bad port: %s\n", args[i+1]) + return 2 + } + port = p + i++ + default: + fmt.Fprintf(os.Stderr, "bindprobe: unexpected argument: %s\n", args[i]) + return 2 + } + } + + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "bindprobe: socket: %s\n", errnoText(err)) + return 1 + } + defer unix.Close(fd) + _ = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + + sa := &unix.SockaddrInet4{Port: port} // Addr left zero => INADDR_ANY + if err := unix.Bind(fd, sa); err != nil { + fmt.Fprintf(os.Stderr, "bindprobe: bind :%d: %s\n", port, errnoText(err)) + return 3 + } + fmt.Printf("bindprobe: uid=%d bound :%d\n", os.Getuid(), port) + return 0 +} + +// ---- drop ---------------------------------------------------------------- + +func cmdDrop(args []string) int { + var name string + keepCap := false + var cmd []string + for i := 0; i < len(args); i++ { + switch args[i] { + case "--user": + if i+1 >= len(args) { + fmt.Fprintln(os.Stderr, "drop: --user needs a value") + return 2 + } + name = args[i+1] + i++ + case "--keep-cap": + if i+1 >= len(args) { + fmt.Fprintln(os.Stderr, "drop: --keep-cap needs a value") + return 2 + } + if args[i+1] != "net_bind_service" { + fmt.Fprintf(os.Stderr, "drop: unsupported --keep-cap: %s\n", args[i+1]) + return 2 + } + keepCap = true + i++ + case "--": + cmd = args[i+1:] + i = len(args) + default: + fmt.Fprintf(os.Stderr, "drop: unexpected argument: %s\n", args[i]) + return 2 + } + } + if name == "" { + fmt.Fprintln(os.Stderr, "drop: --user is required") + return 2 + } + if len(cmd) == 0 { + fmt.Fprintln(os.Stderr, "drop: missing -- CMD") + return 2 + } + if os.Getuid() != 0 { + fmt.Fprintln(os.Stderr, "drop: must run as root") + return 1 + } + + u, err := user.Lookup(name) + if err != nil { + fmt.Fprintf(os.Stderr, "drop: unknown user: %s\n", name) + return 1 + } + uid, _ := strconv.Atoi(u.Uid) + gid, _ := strconv.Atoi(u.Gid) + + c := exec.Command(cmd[0], cmd[1:]...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + // The runtime's fork/exec child applies these in the kernel-correct order: + // PR_SET_KEEPCAPS, setgroups/setgid, setuid, capset, PR_CAP_AMBIENT_RAISE. + c.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, + } + if keepCap { + c.SysProcAttr.AmbientCaps = []uintptr{unix.CAP_NET_BIND_SERVICE} + } + + if err := c.Run(); err != nil { + var ee *exec.ExitError + if errors.As(err, &ee) { + return ee.ExitCode() // propagate CMD's own exit status (e.g. 3) + } + fmt.Fprintf(os.Stderr, "drop: %v\n", err) + return 1 + } + return 0 +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage:") + fmt.Fprintln(os.Stderr, " pmon drop --user [--keep-cap net_bind_service] -- CMD [args...]") + fmt.Fprintln(os.Stderr, " pmon bindprobe [--port 80]") +} + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + var code int + switch os.Args[1] { + case "drop": + code = cmdDrop(os.Args[2:]) + case "bindprobe": + code = cmdBindprobe(os.Args[2:]) + default: + usage() + code = 2 + } + os.Exit(code) +} diff --git a/examples/14-identity-and-privilege/rust/Cargo.lock b/examples/14-identity-and-privilege/rust/Cargo.lock new file mode 100644 index 0000000..b3fc4e8 --- /dev/null +++ b/examples/14-identity-and-privilege/rust/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "pmon" +version = "3.0.0" +dependencies = [ + "libc", +] diff --git a/examples/14-identity-and-privilege/rust/Cargo.toml b/examples/14-identity-and-privilege/rust/Cargo.toml new file mode 100644 index 0000000..f487059 --- /dev/null +++ b/examples/14-identity-and-privilege/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "pmon" +version = "3.0.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +libc = "0.2.186" diff --git a/examples/14-identity-and-privilege/rust/demo.sh b/examples/14-identity-and-privilege/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/14-identity-and-privilege/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/14-identity-and-privilege/rust/rust-toolchain.toml b/examples/14-identity-and-privilege/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/14-identity-and-privilege/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/14-identity-and-privilege/rust/src/main.rs b/examples/14-identity-and-privilege/rust/src/main.rs new file mode 100644 index 0000000..b11d46b --- /dev/null +++ b/examples/14-identity-and-privilege/rust/src/main.rs @@ -0,0 +1,329 @@ +//! pmon v3 — identity & privilege (chapter 14). +//! +//! Subcommands: +//! pmon drop --user [--keep-cap net_bind_service] -- CMD [args...] +//! pmon bindprobe [--port 80] +//! +//! `drop` runs as root and hands CMD to an unprivileged user, optionally +//! carrying exactly one capability (CAP_NET_BIND_SERVICE) across the identity +//! change and the following execvp(3) via the ambient-capability mechanism. +//! The ordering is the lesson (same as the C++ raw path): +//! +//! 1. prctl(PR_SET_KEEPCAPS, 1) before the uid change, so the permitted set +//! survives setresuid(2). +//! 2. setgroups / setresgid / setresuid become the target user. +//! 3. capset(2) pins CAP_NET_BIND_SERVICE into permitted + inheritable +//! (ambient requires both). +//! 4. prctl(PR_CAP_AMBIENT_RAISE) raises it into the ambient set — the only +//! set a non-root, no-file-cap execve keeps. +//! 5. execvp(3): CMD runs as , keeping :80 iff the cap was raised. +//! +//! Raw libc sequence, mirroring the C++ build; std OwnedFd gives the socket +//! RAII; errors flow through Result/?. + +use std::ffi::{CStr, CString, c_char, c_void}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + +// Not exported by the libc crate; both are stable kernel ABI constants. +const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522; +const CAP_NET_BIND_SERVICE: u32 = 10; + +/// A syscall failure: what we attempted, plus the captured errno. +struct SysErr { + what: String, + errno: i32, +} + +/// libc strerror(3), so the text matches the C++/Go builds byte-for-byte. +fn strerror(errno: i32) -> String { + // SAFETY: strerror returns a valid NUL-terminated static string. + unsafe { CStr::from_ptr(libc::strerror(errno)) } + .to_string_lossy() + .into_owned() +} + +fn last_errno() -> i32 { + std::io::Error::last_os_error().raw_os_error().unwrap_or(0) +} + +/// Turn a negative syscall return into an error carrying errno. +fn check(rc: i64, what: &str) -> Result<(), SysErr> { + if rc < 0 { + Err(SysErr { + what: what.to_string(), + errno: last_errno(), + }) + } else { + Ok(()) + } +} + +// ---- bindprobe ----------------------------------------------------------- + +fn cmd_bindprobe(args: &[String]) -> i32 { + let mut port: u16 = 80; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--port" => { + let Some(v) = args.get(i + 1) else { + eprintln!("bindprobe: --port needs a value"); + return 2; + }; + match v.parse::() { + Ok(p) => port = p, + Err(_) => { + eprintln!("bindprobe: bad port: {v}"); + return 2; + } + } + i += 1; + } + other => { + eprintln!("bindprobe: unexpected argument: {other}"); + return 2; + } + } + i += 1; + } + + // SAFETY: raw socket then immediately owned; all pointers below are to + // locals whose lifetime covers each call. + let raw = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) }; + if raw < 0 { + eprintln!("bindprobe: socket: {}", strerror(last_errno())); + return 1; + } + let sock = unsafe { OwnedFd::from_raw_fd(raw) }; + + let one: i32 = 1; + unsafe { + libc::setsockopt( + sock.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_REUSEADDR, + &one as *const i32 as *const c_void, + std::mem::size_of::() as libc::socklen_t, + ); + } + + let mut addr: libc::sockaddr_in = unsafe { std::mem::zeroed() }; + addr.sin_family = libc::AF_INET as libc::sa_family_t; + addr.sin_addr.s_addr = libc::INADDR_ANY.to_be(); + addr.sin_port = port.to_be(); + + let rc = unsafe { + libc::bind( + sock.as_raw_fd(), + &addr as *const libc::sockaddr_in as *const libc::sockaddr, + std::mem::size_of::() as libc::socklen_t, + ) + }; + if rc != 0 { + eprintln!("bindprobe: bind :{port}: {}", strerror(last_errno())); + return 3; + } + let uid = unsafe { libc::getuid() }; + println!("bindprobe: uid={uid} bound :{port}"); + 0 +} + +// ---- drop ---------------------------------------------------------------- + +#[repr(C)] +struct CapHeader { + version: u32, + pid: i32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct CapData { + effective: u32, + permitted: u32, + inheritable: u32, +} + +/// Drop to `uid`/`gid`, optionally retaining CAP_NET_BIND_SERVICE, then exec. +/// Returns only on failure; on success control never comes back. +fn arrange_and_exec( + uid: libc::uid_t, + gid: libc::gid_t, + keep_cap: bool, + cmd: &[String], +) -> Result<(), SysErr> { + // (1) Retain the permitted set across the uid change (must precede it). + if keep_cap { + check( + unsafe { libc::prctl(libc::PR_SET_KEEPCAPS, 1, 0, 0, 0) } as i64, + "PR_SET_KEEPCAPS", + )?; + } + + // (2) Become the target user: groups, then gid, then uid. + check(unsafe { libc::setgroups(1, &gid) } as i64, "setgroups")?; + check( + unsafe { libc::setresgid(gid, gid, gid) } as i64, + "setresgid", + )?; + check( + unsafe { libc::setresuid(uid, uid, uid) } as i64, + "setresuid", + )?; + + // (3) Pin exactly CAP_NET_BIND_SERVICE into permitted+inheritable. + if keep_cap { + let hdr = CapHeader { + version: LINUX_CAPABILITY_VERSION_3, + pid: 0, + }; + let bit = 1u32 << CAP_NET_BIND_SERVICE; // cap 10, word 0 + let data = [ + CapData { + effective: bit, + permitted: bit, + inheritable: bit, + }, + CapData { + effective: 0, + permitted: 0, + inheritable: 0, + }, + ]; + check( + unsafe { libc::syscall(libc::SYS_capset, &hdr as *const CapHeader, data.as_ptr()) }, + "capset", + )?; + + // (4) Raise it into the ambient set. + check( + unsafe { + libc::prctl( + libc::PR_CAP_AMBIENT, + libc::PR_CAP_AMBIENT_RAISE as libc::c_ulong, + CAP_NET_BIND_SERVICE as libc::c_ulong, + 0, + 0, + ) + } as i64, + "PR_CAP_AMBIENT_RAISE", + )?; + } + + // (5) Hand off to CMD. + let prog = CString::new(cmd[0].as_str()).map_err(|_| SysErr { + what: "bad CMD".into(), + errno: libc::EINVAL, + })?; + let cargs: Vec = cmd + .iter() + .map(|s| CString::new(s.as_str()).unwrap_or_default()) + .collect(); + let mut ptrs: Vec<*const c_char> = cargs.iter().map(|c| c.as_ptr()).collect(); + ptrs.push(std::ptr::null()); + unsafe { libc::execvp(prog.as_ptr(), ptrs.as_ptr()) }; + Err(SysErr { + what: format!("execvp {}", cmd[0]), + errno: last_errno(), + }) +} + +fn cmd_drop(args: &[String]) -> i32 { + let mut user: Option<&str> = None; + let mut keep_cap = false; + let mut cmd: Vec = Vec::new(); + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--user" => { + let Some(v) = args.get(i + 1) else { + eprintln!("drop: --user needs a value"); + return 2; + }; + user = Some(v); + i += 1; + } + "--keep-cap" => { + let Some(v) = args.get(i + 1) else { + eprintln!("drop: --keep-cap needs a value"); + return 2; + }; + if v != "net_bind_service" { + eprintln!("drop: unsupported --keep-cap: {v}"); + return 2; + } + keep_cap = true; + i += 1; + } + "--" => { + cmd = args[i + 1..].to_vec(); + break; + } + other => { + eprintln!("drop: unexpected argument: {other}"); + return 2; + } + } + i += 1; + } + + let Some(user) = user else { + eprintln!("drop: --user is required"); + return 2; + }; + if cmd.is_empty() { + eprintln!("drop: missing -- CMD"); + return 2; + } + if unsafe { libc::getuid() } != 0 { + eprintln!("drop: must run as root"); + return 1; + } + + let cname = match CString::new(user) { + Ok(c) => c, + Err(_) => { + eprintln!("drop: unknown user: {user}"); + return 1; + } + }; + // SAFETY: getpwnam returns a pointer into a static buffer valid until the + // next call; we read it immediately. + let pw = unsafe { libc::getpwnam(cname.as_ptr()) }; + if pw.is_null() { + eprintln!("drop: unknown user: {user}"); + return 1; + } + let (uid, gid) = unsafe { ((*pw).pw_uid, (*pw).pw_gid) }; + + match arrange_and_exec(uid, gid, keep_cap, &cmd) { + Ok(()) => 0, // unreachable: exec never returns on success + Err(e) => { + eprintln!("drop: {}: {}", e.what, strerror(e.errno)); + 1 + } + } +} + +fn usage() { + eprintln!("usage:"); + eprintln!(" pmon drop --user [--keep-cap net_bind_service] -- CMD [args...]"); + eprintln!(" pmon bindprobe [--port 80]"); +} + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 2 { + usage(); + std::process::exit(2); + } + let code = match args[1].as_str() { + "drop" => cmd_drop(&args[2..]), + "bindprobe" => cmd_bindprobe(&args[2..]), + _ => { + usage(); + 2 + } + }; + std::process::exit(code); +} diff --git a/examples/14-identity-and-privilege/verify.lua b/examples/14-identity-and-privilege/verify.lua new file mode 100644 index 0000000..e1113ac --- /dev/null +++ b/examples/14-identity-and-privilege/verify.lua @@ -0,0 +1,88 @@ +-- Verify pmon v3 (identity & privilege) — a VM example. Behavioral assertions: +-- +-- 1. `drop --user nobody --keep-cap net_bind_service -- CMD bindprobe :80` +-- must run CMD as an UNPRIVILEGED uid (65534, not 0) yet still bind :80. +-- That pairing is impossible without the ambient CAP_NET_BIND_SERVICE +-- surviving the setuid + execve — so it is the proof the cap was carried. +-- +-- 2. NEGATIVE CONTROL — the same drop WITHOUT --keep-cap must fail the bind +-- with "Permission denied" and exit 3. Same uid, cap withheld, port +-- denied: this is what makes assertion 1 meaningful rather than trivially +-- true (a root process, or a lowered ip_unprivileged_port_start, would +-- pass assertion 1 while proving nothing). +-- +-- Runs under the runner's vm mode (TARGET=systems-target, deploy via +-- deploy-to-vm.sh with SUDO=1). Because /home/fedora is mode 700, the dropped +-- user cannot reach a binary staged there, so we stage a world-readable copy +-- of the exec target in /tmp and point CMD at it. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local target = os.getenv("TARGET") +if not target or target == "" then + checks.skip("no TARGET set — this is a vm example (run with --mode vm)") +end + +local example_dir = os.getenv("EXAMPLE_DIR") or "." +local home = os.getenv("HOME") or "/root" + +-- Local path of the freshly built binary for this language. +local bin_rel = ({ + cpp = "cpp/build/release/app", + go = "go/bin/app", + rust = "rust/target/release/app", +})[lang] +local local_bin = example_dir .. "/" .. bin_rel + +-- Resolve the guest IP (same helper deploy-to-vm.sh uses). +local ipf = io.popen(repo_root .. "/scripts/lab/vm-ip.sh '" .. target .. "' 2>/dev/null") +local ip = ipf and ipf:read("*l") or nil +if ipf then ipf:close() end +if not ip or ip == "" then + checks.skip("cannot resolve IP for VM '" .. target .. "' (lab down?)") +end + +-- Stage a world-readable copy of the exec target on the guest (/tmp is 1777; +-- /home/fedora is 700 and unreachable by the dropped user). +local ssh_opts = + "-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=" .. home .. "/.ssh/known_hosts" +local staged = "/tmp/pmon-verify" +local stage_cmd = string.format( + "scp %s '%s' 'fedora@%s:%s' && ssh %s 'fedora@%s' 'chmod 755 %s'", + ssh_opts, local_bin, ip, staged, ssh_opts, ip, staged) +local st = checks.run(stage_cmd) +checks.expect_exit(st, 0, lang .. ": stage exec target on guest") + +-- Positive: drop to nobody, KEEP the cap, bind :80. +local pos = checks.run(string.format( + "SUDO=1 ./demo.sh %s run drop --user nobody --keep-cap net_bind_service -- %s bindprobe --port 80", + lang, staged)) +checks.expect_exit(pos, 0, lang .. ": drop+keep-cap bindprobe :80 exits 0") +checks.expect_match(pos.out, "bindprobe: uid=%d+ bound :80", + lang .. ": prints bound-:80 proof line") +local uid = pos.out:match("bindprobe: uid=(%d+) bound :80") +if uid and tonumber(uid) ~= 0 then + checks.expect_match(pos.out, "uid=65534 bound :80", + lang .. ": bound :80 as unprivileged nobody (uid=65534, not 0)") +else + checks.expect_match("uid=" .. tostring(uid), "never-matches", + lang .. ": uid must be non-root but was " .. tostring(uid)) +end + +-- Negative control: same drop, NO cap — bind :80 must be denied, exit 3. +local neg = checks.run(string.format( + "SUDO=1 ./demo.sh %s run drop --user nobody -- %s bindprobe --port 80", + lang, staged)) +checks.expect_exit(neg, 3, lang .. ": drop WITHOUT keep-cap exits 3") +checks.expect_match(neg.out, "bindprobe: bind :80: Permission denied", + lang .. ": :80 denied without the ambient cap") + +checks.finish() diff --git a/examples/15-virtual-memory/README.md b/examples/15-virtual-memory/README.md new file mode 100644 index 0000000..891186c --- /dev/null +++ b/examples/15-virtual-memory/README.md @@ -0,0 +1,108 @@ +# 15-virtual-memory — memmap + +A self-inspecting virtual-memory tool, implemented three times (C++23, Go, +Rust) with identical CLI, output shapes, and exit codes. `memmap` performs one +kind of allocation, touches its pages, and then reports what the kernel now +says about *its own* address space: + +``` +memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N] +``` + +| mode | allocation | touch | +|------|-----------|-------| +| `stack` | large local buffer on the call stack (clamped to 4 MiB) | write 1 byte/page | +| `heap` | allocator memory (`std::vector` / `make([]byte,…)` / `Vec`) | write 1 byte/page | +| `mmap-anon` | private anonymous `mmap(2)` | write 1 byte/page | +| `mmap-file FILE` | read-only private `mmap(2)` of FILE (sized by the file) | read 1 byte/page | +| `fault-walk` | temp file of N MiB, mapped read-only, touched in 8 steps | read 1 byte/page | + +`--mb N` sizes the allocation (default 64, range 1..1024; `stack` clamps to 4 +so the buffer stays inside the default 8 MiB stack rlimit). + +## Output contract + +``` +memmap: mode= bytes= pages=

+memmap: walk file= steps=8 (fault-walk only) +memmap: step=/8 pages=

minor= major= (fault-walk only, ×8) +memmap: maps excerpt +memmap: <-- target (mode=) +memmap: vmrss_before=KB vmrss_after=KB +memmap: faults minor= major= +``` + +- The maps excerpt echoes every `/proc/self/maps` line whose range overlaps + the allocation, annotated with `<-- target`. For `mmap-file` the line names + the mapped file and shows `r--p`; for the writable modes it shows `rw-p`. +- `vmrss_before` is sampled just before the allocation, `vmrss_after` after + the touch loop, both from `VmRSS:` in `/proc/self/status`. +- The fault counts are `getrusage(RUSAGE_SELF)` deltas over the same window. +- Usage errors (missing/unknown `--mode`, bad `--mb`, stray arguments) print + the usage line on stderr and exit 2. Runtime errors (missing or empty FILE) + print `memmap: error: …` on stderr and exit 1; the exact message text is + each language's native errno rendering. + +Example (heap, defaults — 64 MiB touched means ~64 MiB of RSS growth and one +minor fault per touched 4 KiB page): + +``` +memmap: mode=heap bytes=67108864 pages=16384 +memmap: maps excerpt +memmap: 7fe0badd9000-7fe0bedda000 rw-p 00000000 00:00 0 <-- target (mode=heap) +memmap: vmrss_before=3908KB vmrss_after=69448KB +memmap: faults minor=16385 major=0 +``` + +## What it teaches + +- **Demand paging.** Every mode allocates address space first (VSZ) and only + pays for physical pages when they are touched: the RSS delta and the minor + fault delta line up with the touched page count, not the allocation call. +- **Fault-around.** In `fault-walk` and `mmap-file`, 512 touched file pages + produce ~32 minor faults per step, not 512 — the kernel maps a batch of + surrounding page-cache pages per fault. Anonymous writes fault one page at + a time, so `heap`/`mmap-anon` report ~1 fault per page. +- **Minor vs major.** The walk file is written immediately before it is + mapped, so every page is in the page cache and `major=0`; a major fault + would mean a real disk read. +- **Where allocations live.** A 64 MiB `malloc`/`Vec`/`make` does not extend + `[heap]` — the allocator satisfies it with a private anonymous mapping, and + the excerpt shows exactly that region. + +## Per-language notes + +- **C++** (`cpp/src/main.cpp`): RAII wrappers (`Fd`, `Mapping`, `TempFile`), + `std::expected` error plumbing, `std::println`. The stack buffer lives in a + `noinline` worker whose caller takes the fault baseline first. +- **Go** (`go/main.go`): wrapped errors, `golang.org/x/sys/unix` for + `Mmap`/`Getrusage`. The stack touch runs on a dedicated goroutine with the + result returned over a channel. Goroutine stacks are runtime-managed heap + memory, so the stack-mode excerpt shows an anonymous region, not `[stack]`. +- **Rust** (`rust/src/main.rs`): `Result` + `?` with `anyhow`, `rustix::mm` + for the mappings (RAII `Mapping` unmaps on drop), `libc::getrusage`. The + baseline is taken before entering the big-frame worker because LLVM's stack + probing touches every frame page at function entry. + +## Run it + +``` +./demo.sh # build + run all three (usage exits 2 without a mode) +./demo.sh cpp run --mode heap +./demo.sh go run --mode fault-walk --mb 16 +./demo.sh rust run --mode mmap-file /etc/os-release +``` + +Each language directory follows the book-wide demo contract: `build`, `run +[args]`, and `TARGET= … run` to deploy via `scripts/lab/deploy-to-vm.sh` +(this chapter's verification runs entirely on the local host). + +## Verification + +`verify.lua` (run per language with `LSP_LANG` set) asserts behavior, not +just exit codes: usage/error exit codes, exact header lines (`bytes`/`pages` +arithmetic, stack clamping, file-sized `mmap-file`), the annotated maps +excerpt including permissions and the mapped file name, heap and `mmap-anon` +RSS deltas of at least half the touched size, nonzero minor-fault deltas, the +8-step fault-walk table shape with accumulating counts, and that the walk's +temp file is unlinked afterwards. diff --git a/examples/15-virtual-memory/cpp/.clang-format b/examples/15-virtual-memory/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/15-virtual-memory/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/15-virtual-memory/cpp/.clang-tidy b/examples/15-virtual-memory/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/15-virtual-memory/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/15-virtual-memory/cpp/CMakeLists.txt b/examples/15-virtual-memory/cpp/CMakeLists.txt new file mode 100644 index 0000000..8cf1a3d --- /dev/null +++ b/examples/15-virtual-memory/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(memmap CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/15-virtual-memory/cpp/CMakePresets.json b/examples/15-virtual-memory/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/15-virtual-memory/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/15-virtual-memory/cpp/demo.sh b/examples/15-virtual-memory/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/15-virtual-memory/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/15-virtual-memory/cpp/src/main.cpp b/examples/15-virtual-memory/cpp/src/main.cpp new file mode 100644 index 0000000..2a07c16 --- /dev/null +++ b/examples/15-virtual-memory/cpp/src/main.cpp @@ -0,0 +1,524 @@ +// memmap: self-inspecting virtual-memory tool — perform one kind of +// allocation, touch its pages, then report what the kernel now shows for +// this very process: the /proc/self/maps region backing the allocation, +// the VmRSS growth, and the getrusage(2) page-fault deltas. +// +// memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N] +// +// stack touch a local buffer on the call stack (clamped to 4 MiB) +// heap allocator memory (operator new via std::vector) +// mmap-anon private anonymous mmap(2), written one byte per page +// mmap-file read-only private mmap of FILE, read one byte per page +// fault-walk create a temp file of N MiB, map it, touch it in 8 steps, +// printing the minor/major fault growth after every step +// +// Output contract (identical across C++, Go, Rust): +// memmap: mode= bytes= pages=

+// [fault-walk only] memmap: walk file= steps=8 +// [fault-walk only] memmap: step=/8 pages=

minor= major= +// memmap: maps excerpt +// memmap: <-- target (mode=) +// memmap: vmrss_before=KB vmrss_after=KB +// memmap: faults minor= major= + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr std::size_t kMiB = 1024 * 1024; +constexpr std::size_t kStackCapMb = 4; // stays well inside the 8 MiB rlimit +constexpr int kWalkSteps = 8; + +enum class Mode { stack, heap, mmap_anon, mmap_file, fault_walk }; + +[[nodiscard]] constexpr std::string_view mode_name(Mode m) { + switch (m) { + case Mode::stack: return "stack"; + case Mode::heap: return "heap"; + case Mode::mmap_anon: return "mmap-anon"; + case Mode::mmap_file: return "mmap-file"; + case Mode::fault_walk: return "fault-walk"; + } + return "?"; +} + +[[nodiscard]] std::string errno_message(int err) { + return std::error_code(err, std::system_category()).message(); +} + +// Keep the optimizer from proving the touched buffer unread. +inline void opt_barrier(const void* p) { + __asm__ volatile("" : : "r"(p) : "memory"); +} + +// --------------------------------------------------------------------------- +// RAII wrappers +// --------------------------------------------------------------------------- + +class Fd { +public: + explicit Fd(int fd = -1) noexcept : fd_{fd} {} + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& o) noexcept : fd_{std::exchange(o.fd_, -1)} {} + Fd& operator=(Fd&& o) noexcept { + if (this != &o) { + reset(); + fd_ = std::exchange(o.fd_, -1); + } + return *this; + } + ~Fd() { reset(); } + [[nodiscard]] int get() const noexcept { return fd_; } + [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; } + +private: + void reset() noexcept { + if (fd_ >= 0) { + ::close(fd_); + fd_ = -1; + } + } + int fd_ = -1; +}; + +class Mapping { +public: + Mapping(void* p, std::size_t len) noexcept : p_{p}, len_{len} {} + Mapping(const Mapping&) = delete; + Mapping& operator=(const Mapping&) = delete; + Mapping(Mapping&& o) noexcept + : p_{std::exchange(o.p_, nullptr)}, len_{std::exchange(o.len_, 0)} {} + ~Mapping() { + if (p_ != nullptr) { + ::munmap(p_, len_); + } + } + [[nodiscard]] std::byte* data() const noexcept { + return static_cast(p_); + } + [[nodiscard]] std::size_t size() const noexcept { return len_; } + +private: + void* p_ = nullptr; + std::size_t len_ = 0; +}; + +// Unlinks its path on scope exit. +class TempFile { +public: + explicit TempFile(std::string path) : path_{std::move(path)} {} + TempFile(const TempFile&) = delete; + TempFile& operator=(const TempFile&) = delete; + ~TempFile() { + if (!path_.empty()) { + ::unlink(path_.c_str()); + } + } + [[nodiscard]] const std::string& path() const noexcept { return path_; } + +private: + std::string path_; +}; + +// --------------------------------------------------------------------------- +// /proc and getrusage probes +// --------------------------------------------------------------------------- + +[[nodiscard]] std::expected vmrss_kb() { + std::ifstream f{"/proc/self/status"}; + if (!f) { + return std::unexpected{"cannot open /proc/self/status"}; + } + std::string line; + while (std::getline(f, line)) { + if (line.starts_with("VmRSS:")) { + std::string_view rest{line}; + rest.remove_prefix(6); + std::size_t i = 0; + while (i < rest.size() && (rest[i] == ' ' || rest[i] == '\t')) ++i; + long kb = 0; + auto [ptr, ec] = + std::from_chars(rest.data() + i, rest.data() + rest.size(), kb); + if (ec != std::errc{}) { + return std::unexpected{"cannot parse VmRSS"}; + } + return kb; + } + } + return std::unexpected{"VmRSS not found in /proc/self/status"}; +} + +struct Faults { + long minor = 0; + long major = 0; +}; + +[[nodiscard]] std::expected fault_counts() { + rusage ru{}; + if (::getrusage(RUSAGE_SELF, &ru) != 0) { + return std::unexpected{"getrusage: " + errno_message(errno)}; + } + return Faults{ru.ru_minflt, ru.ru_majflt}; +} + +// Print every /proc/self/maps line whose range overlaps [addr, addr+len). +[[nodiscard]] std::expected +print_maps_excerpt(std::uintptr_t addr, std::size_t len, Mode mode) { + std::ifstream f{"/proc/self/maps"}; + if (!f) { + return std::unexpected{"cannot open /proc/self/maps"}; + } + std::println("memmap: maps excerpt"); + const std::uintptr_t lo = addr; + const std::uintptr_t hi = addr + len; + std::string line; + while (std::getline(f, line)) { + const auto dash = line.find('-'); + const auto sp = line.find(' '); + if (dash == std::string::npos || sp == std::string::npos || sp < dash) { + continue; + } + std::uintptr_t start = 0; + std::uintptr_t end = 0; + auto r1 = std::from_chars(line.data(), line.data() + dash, start, 16); + auto r2 = + std::from_chars(line.data() + dash + 1, line.data() + sp, end, 16); + if (r1.ec != std::errc{} || r2.ec != std::errc{}) { + continue; + } + if (start < hi && end > lo) { + std::println("memmap: {} <-- target (mode={})", line, + mode_name(mode)); + } + } + return {}; +} + +struct Baseline { + long rss_kb = 0; + Faults faults; +}; + +[[nodiscard]] std::expected take_baseline() { + auto rss = vmrss_kb(); + if (!rss) return std::unexpected{rss.error()}; + auto fl = fault_counts(); + if (!fl) return std::unexpected{fl.error()}; + return Baseline{*rss, *fl}; +} + +// Common tail: maps excerpt, RSS growth, fault deltas. +[[nodiscard]] std::expected +report(std::uintptr_t addr, std::size_t len, Mode mode, const Baseline& base) { + if (auto r = print_maps_excerpt(addr, len, mode); !r) return r; + auto rss_after = vmrss_kb(); + if (!rss_after) return std::unexpected{rss_after.error()}; + auto fl = fault_counts(); + if (!fl) return std::unexpected{fl.error()}; + std::println("memmap: vmrss_before={}KB vmrss_after={}KB", base.rss_kb, + *rss_after); + std::println("memmap: faults minor={} major={}", + fl->minor - base.faults.minor, fl->major - base.faults.major); + return {}; +} + +// --------------------------------------------------------------------------- +// Modes +// --------------------------------------------------------------------------- + +void touch_writable(std::span mem, std::size_t page) { + for (std::size_t i = 0; i < mem.size(); i += page) { + mem[i] = std::byte{1}; + } + opt_barrier(mem.data()); +} + +// Read one byte per page; the sum flows into the barrier so it stays live. +void touch_readable(std::span mem, std::size_t page) { + unsigned long sum = 0; + for (std::size_t i = 0; i < mem.size(); i += page) { + sum += std::to_underlying(mem[i]); + } + opt_barrier(&sum); +} + +// Baseline is captured by the caller so that any page the compiler touches +// while setting up this 4 MiB frame still lands inside the measured window. +[[gnu::noinline]] std::expected +stack_worker(std::size_t bytes, std::size_t page, const Baseline& base) { + std::array buf{}; + touch_writable(std::span{buf.data(), bytes}, page); + return report(reinterpret_cast(buf.data()), bytes, + Mode::stack, base); +} + +[[nodiscard]] std::expected +run_stack(std::size_t bytes, std::size_t page) { + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + return stack_worker(bytes, page, *base); +} + +[[nodiscard]] std::expected +run_heap(std::size_t bytes, std::size_t page) { + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + std::vector buf(bytes); + touch_writable(std::span{buf}, page); + return report(reinterpret_cast(buf.data()), bytes, + Mode::heap, *base); +} + +[[nodiscard]] std::expected +run_mmap_anon(std::size_t bytes, std::size_t page) { + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + void* p = ::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p == MAP_FAILED) { + return std::unexpected{"mmap: " + errno_message(errno)}; + } + Mapping map{p, bytes}; + touch_writable(std::span{map.data(), map.size()}, page); + return report(reinterpret_cast(map.data()), map.size(), + Mode::mmap_anon, *base); +} + +[[nodiscard]] std::expected, std::string> +open_for_map(const std::string& path) { + Fd fd{::open(path.c_str(), O_RDONLY | O_CLOEXEC)}; + if (!fd.valid()) { + return std::unexpected{path + ": " + errno_message(errno)}; + } + struct stat st{}; + if (::fstat(fd.get(), &st) != 0) { + return std::unexpected{path + ": fstat: " + errno_message(errno)}; + } + if (st.st_size <= 0) { + return std::unexpected{path + ": file is empty"}; + } + return std::pair{std::move(fd), static_cast(st.st_size)}; +} + +[[nodiscard]] std::expected map_readonly(int fd, + std::size_t len) { + void* p = ::mmap(nullptr, len, PROT_READ, MAP_PRIVATE, fd, 0); + if (p == MAP_FAILED) { + return std::unexpected{"mmap: " + errno_message(errno)}; + } + return Mapping{p, len}; +} + +[[nodiscard]] std::expected +run_mmap_file(const std::string& path, std::size_t page) { + auto opened = open_for_map(path); + if (!opened) return std::unexpected{opened.error()}; + auto& [fd, len] = *opened; + const std::size_t pages = (len + page - 1) / page; + std::println("memmap: mode=mmap-file bytes={} pages={}", len, pages); + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + auto map = map_readonly(fd.get(), len); + if (!map) return std::unexpected{map.error()}; + touch_readable(std::span{map->data(), map->size()}, page); + return report(reinterpret_cast(map->data()), map->size(), + Mode::mmap_file, *base); +} + +[[nodiscard]] std::expected +write_walk_file(std::size_t bytes) { + const char* tmpdir = std::getenv("TMPDIR"); + std::string path = std::string{tmpdir != nullptr ? tmpdir : "/tmp"} + + "/memmap-walk-" + std::to_string(::getpid()) + ".bin"; + Fd fd{::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644)}; + if (!fd.valid()) { + return std::unexpected{path + ": " + errno_message(errno)}; + } + std::vector chunk(kMiB, std::byte{0xA5}); + std::size_t left = bytes; + while (left > 0) { + const std::size_t n = std::min(left, chunk.size()); + std::size_t off = 0; + while (off < n) { + const ssize_t w = ::write(fd.get(), chunk.data() + off, n - off); + if (w < 0) { + if (errno == EINTR) continue; + return std::unexpected{path + ": write: " + + errno_message(errno)}; + } + off += static_cast(w); + } + left -= n; + } + return path; +} + +[[nodiscard]] std::expected +run_fault_walk(std::size_t bytes, std::size_t page) { + auto path = write_walk_file(bytes); + if (!path) return std::unexpected{path.error()}; + TempFile tmp{*path}; + auto opened = open_for_map(tmp.path()); + if (!opened) return std::unexpected{opened.error()}; + auto& [fd, len] = *opened; + auto base = take_baseline(); + if (!base) return std::unexpected{base.error()}; + auto map = map_readonly(fd.get(), len); + if (!map) return std::unexpected{map.error()}; + + std::println("memmap: walk file={} steps={}", tmp.path(), kWalkSteps); + const std::size_t pages = len / page; + Faults prev = base->faults; + std::size_t done = 0; + for (int step = 1; step <= kWalkSteps; ++step) { + std::size_t quota = pages / kWalkSteps; + if (step == kWalkSteps) quota = pages - done; + touch_readable(std::span{map->data() + done * page, quota * page}, + page); + auto now = fault_counts(); + if (!now) return std::unexpected{now.error()}; + std::println("memmap: step={}/{} pages={} minor={} major={}", step, + kWalkSteps, quota, now->minor - prev.minor, + now->major - prev.major); + prev = *now; + done += quota; + } + return report(reinterpret_cast(map->data()), map->size(), + Mode::fault_walk, *base); +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +struct Options { + Mode mode = Mode::heap; + std::size_t mb = 64; + std::string file; +}; + +void usage(std::FILE* to) { + std::println(to, + "usage: memmap --mode stack|heap|mmap-anon|mmap-file |" + "fault-walk [--mb N]"); +} + +[[nodiscard]] std::expected +parse_args(std::span args) { + Options opts; + bool mode_set = false; + for (std::size_t i = 0; i < args.size(); ++i) { + const std::string_view a{args[i]}; + if (a == "--mode") { + if (++i >= args.size()) { + return std::unexpected{"--mode needs a value"}; + } + const std::string_view m{args[i]}; + if (m == "stack") opts.mode = Mode::stack; + else if (m == "heap") opts.mode = Mode::heap; + else if (m == "mmap-anon") opts.mode = Mode::mmap_anon; + else if (m == "mmap-file") opts.mode = Mode::mmap_file; + else if (m == "fault-walk") opts.mode = Mode::fault_walk; + else return std::unexpected{"unknown mode: " + std::string{m}}; + mode_set = true; + } else if (a == "--mb") { + if (++i >= args.size()) { + return std::unexpected{"--mb needs a value"}; + } + const std::string_view v{args[i]}; + std::size_t mb = 0; + auto [p, ec] = std::from_chars(v.data(), v.data() + v.size(), mb); + if (ec != std::errc{} || p != v.data() + v.size() || mb < 1 || + mb > 1024) { + return std::unexpected{"--mb must be 1..1024"}; + } + opts.mb = mb; + } else if (!a.starts_with("--") && opts.file.empty()) { + opts.file = a; + } else { + return std::unexpected{"unexpected argument: " + std::string{a}}; + } + } + if (!mode_set) return std::unexpected{"--mode is required"}; + if (opts.mode == Mode::mmap_file && opts.file.empty()) { + return std::unexpected{"mmap-file needs a FILE argument"}; + } + if (opts.mode != Mode::mmap_file && !opts.file.empty()) { + return std::unexpected{"only mmap-file takes a FILE argument"}; + } + return opts; +} + +} // namespace + +int main(int argc, char** argv) { + const auto opts = + parse_args(std::span{argv + 1, static_cast(argc - 1)}); + if (!opts) { + usage(stderr); + return 2; + } + const auto page = static_cast(::sysconf(_SC_PAGESIZE)); + + std::expected r; + switch (opts->mode) { + case Mode::stack: { + const std::size_t bytes = std::min(opts->mb, kStackCapMb) * kMiB; + std::println("memmap: mode=stack bytes={} pages={}", bytes, + bytes / page); + r = run_stack(bytes, page); + break; + } + case Mode::heap: { + const std::size_t bytes = opts->mb * kMiB; + std::println("memmap: mode=heap bytes={} pages={}", bytes, + bytes / page); + r = run_heap(bytes, page); + break; + } + case Mode::mmap_anon: { + const std::size_t bytes = opts->mb * kMiB; + std::println("memmap: mode=mmap-anon bytes={} pages={}", bytes, + bytes / page); + r = run_mmap_anon(bytes, page); + break; + } + case Mode::mmap_file: + r = run_mmap_file(opts->file, page); // prints its own header line + break; + case Mode::fault_walk: { + const std::size_t bytes = opts->mb * kMiB; + std::println("memmap: mode=fault-walk bytes={} pages={}", bytes, + bytes / page); + r = run_fault_walk(bytes, page); + break; + } + } + if (!r) { + std::println(stderr, "memmap: error: {}", r.error()); + return 1; + } + return 0; +} diff --git a/examples/15-virtual-memory/demo.sh b/examples/15-virtual-memory/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/15-virtual-memory/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/15-virtual-memory/go/demo.sh b/examples/15-virtual-memory/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/15-virtual-memory/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/15-virtual-memory/go/go.mod b/examples/15-virtual-memory/go/go.mod new file mode 100644 index 0000000..b92a068 --- /dev/null +++ b/examples/15-virtual-memory/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/15-virtual-memory/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/15-virtual-memory/go/go.sum b/examples/15-virtual-memory/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/15-virtual-memory/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/15-virtual-memory/go/main.go b/examples/15-virtual-memory/go/main.go new file mode 100644 index 0000000..32b284f --- /dev/null +++ b/examples/15-virtual-memory/go/main.go @@ -0,0 +1,419 @@ +// memmap: self-inspecting virtual-memory tool — perform one kind of +// allocation, touch its pages, then report what the kernel now shows for +// this very process: the /proc/self/maps region backing the allocation, +// the VmRSS growth, and the getrusage(2) page-fault deltas. +// +// memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N] +// +// Output contract (identical across C++, Go, Rust): +// +// memmap: mode= bytes= pages=

+// [fault-walk only] memmap: walk file= steps=8 +// [fault-walk only] memmap: step=/8 pages=

minor= major= +// memmap: maps excerpt +// memmap: <-- target (mode=) +// memmap: vmrss_before=KB vmrss_after=KB +// memmap: faults minor= major= +// +// Go note: goroutine stacks are runtime-managed memory, not the kernel's +// [stack] mapping, so the stack-mode excerpt shows an anonymous region. +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + mib = 1 << 20 + stackCapMb = 4 // stays well inside typical stack expectations + walkSteps = 8 +) + +// sink defeats dead-store elimination of the touch loops. +var sink byte + +type options struct { + mode string + mb int + file string +} + +type baseline struct { + rssKB int64 + minor int64 + major int64 +} + +// --------------------------------------------------------------------------- +// /proc and getrusage probes +// --------------------------------------------------------------------------- + +func vmrssKB() (int64, error) { + f, err := os.Open("/proc/self/status") + if err != nil { + return 0, fmt.Errorf("open /proc/self/status: %w", err) + } + defer f.Close() + sc := bufio.NewScanner(f) + for sc.Scan() { + line := sc.Text() + if !strings.HasPrefix(line, "VmRSS:") { + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + return 0, fmt.Errorf("cannot parse VmRSS: %q", line) + } + kb, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + return 0, fmt.Errorf("cannot parse VmRSS: %w", err) + } + return kb, nil + } + if err := sc.Err(); err != nil { + return 0, fmt.Errorf("read /proc/self/status: %w", err) + } + return 0, fmt.Errorf("VmRSS not found in /proc/self/status") +} + +func faultCounts() (minor, major int64, err error) { + var ru unix.Rusage + if err := unix.Getrusage(unix.RUSAGE_SELF, &ru); err != nil { + return 0, 0, fmt.Errorf("getrusage: %w", err) + } + return ru.Minflt, ru.Majflt, nil +} + +func takeBaseline() (baseline, error) { + rss, err := vmrssKB() + if err != nil { + return baseline{}, err + } + minor, major, err := faultCounts() + if err != nil { + return baseline{}, err + } + return baseline{rssKB: rss, minor: minor, major: major}, nil +} + +// printMapsExcerpt prints every /proc/self/maps line overlapping +// [addr, addr+length). +func printMapsExcerpt(addr, length uintptr, mode string) error { + f, err := os.Open("/proc/self/maps") + if err != nil { + return fmt.Errorf("open /proc/self/maps: %w", err) + } + defer f.Close() + fmt.Println("memmap: maps excerpt") + lo, hi := uint64(addr), uint64(addr+length) + sc := bufio.NewScanner(f) + sc.Buffer(make([]byte, 64*1024), 64*1024) + for sc.Scan() { + line := sc.Text() + rangePart, _, ok := strings.Cut(line, " ") + if !ok { + continue + } + startStr, endStr, ok := strings.Cut(rangePart, "-") + if !ok { + continue + } + start, err1 := strconv.ParseUint(startStr, 16, 64) + end, err2 := strconv.ParseUint(endStr, 16, 64) + if err1 != nil || err2 != nil { + continue + } + if start < hi && end > lo { + fmt.Printf("memmap: %s <-- target (mode=%s)\n", line, mode) + } + } + return sc.Err() +} + +// report prints the common tail: maps excerpt, RSS growth, fault deltas. +func report(addr, length uintptr, mode string, base baseline) error { + if err := printMapsExcerpt(addr, length, mode); err != nil { + return err + } + rssAfter, err := vmrssKB() + if err != nil { + return err + } + minor, major, err := faultCounts() + if err != nil { + return err + } + fmt.Printf("memmap: vmrss_before=%dKB vmrss_after=%dKB\n", base.rssKB, rssAfter) + fmt.Printf("memmap: faults minor=%d major=%d\n", minor-base.minor, major-base.major) + return nil +} + +// --------------------------------------------------------------------------- +// Modes +// --------------------------------------------------------------------------- + +func touchWritable(b []byte, page int) { + for i := 0; i < len(b); i += page { + b[i] = 1 + } + if len(b) > 0 { + sink = b[0] + } +} + +func touchReadable(b []byte, page int) { + var sum byte + for i := 0; i < len(b); i += page { + sum += b[i] + } + sink = sum +} + +// runStack touches a large local buffer on a dedicated goroutine so the +// growth happens on a fresh stack; the result comes back over a channel. +// The baseline is captured before the goroutine starts so the runtime work +// of growing (or relocating) that stack is part of the measured window. +func runStack(bytes, page int) error { + base, err := takeBaseline() + if err != nil { + return err + } + errc := make(chan error, 1) + go func() { + errc <- stackWorker(bytes, page, base) + }() + return <-errc +} + +//go:noinline +func stackWorker(n, page int, base baseline) error { + var buf [stackCapMb * mib]byte + touchWritable(buf[:n], page) + return report(uintptr(unsafe.Pointer(&buf[0])), uintptr(n), "stack", base) +} + +func runHeap(n, page int) error { + base, err := takeBaseline() + if err != nil { + return err + } + buf := make([]byte, n) + touchWritable(buf, page) + return report(uintptr(unsafe.Pointer(&buf[0])), uintptr(n), "heap", base) +} + +func runMmapAnon(n, page int) error { + base, err := takeBaseline() + if err != nil { + return err + } + m, err := unix.Mmap(-1, 0, n, unix.PROT_READ|unix.PROT_WRITE, + unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer unix.Munmap(m) + touchWritable(m, page) + return report(uintptr(unsafe.Pointer(&m[0])), uintptr(n), "mmap-anon", base) +} + +func openForMap(path string) (*os.File, int64, error) { + f, err := os.Open(path) + if err != nil { + return nil, 0, err + } + st, err := f.Stat() + if err != nil { + f.Close() + return nil, 0, fmt.Errorf("%s: stat: %w", path, err) + } + if st.Size() <= 0 { + f.Close() + return nil, 0, fmt.Errorf("%s: file is empty", path) + } + return f, st.Size(), nil +} + +func runMmapFile(path string, page int) error { + f, size, err := openForMap(path) + if err != nil { + return err + } + defer f.Close() + pages := (size + int64(page) - 1) / int64(page) + fmt.Printf("memmap: mode=mmap-file bytes=%d pages=%d\n", size, pages) + base, err := takeBaseline() + if err != nil { + return err + } + m, err := unix.Mmap(int(f.Fd()), 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer unix.Munmap(m) + touchReadable(m, page) + return report(uintptr(unsafe.Pointer(&m[0])), uintptr(size), "mmap-file", base) +} + +func writeWalkFile(n int) (string, error) { + path := filepath.Join(os.TempDir(), + fmt.Sprintf("memmap-walk-%d.bin", os.Getpid())) + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return "", err + } + defer f.Close() + chunk := make([]byte, mib) + for i := range chunk { + chunk[i] = 0xA5 + } + for left := n; left > 0; left -= len(chunk) { + c := chunk + if left < len(c) { + c = c[:left] + } + if _, err := f.Write(c); err != nil { + return "", fmt.Errorf("%s: write: %w", path, err) + } + } + return path, nil +} + +func runFaultWalk(n, page int) error { + path, err := writeWalkFile(n) + if err != nil { + return err + } + defer os.Remove(path) + f, size, err := openForMap(path) + if err != nil { + return err + } + defer f.Close() + base, err := takeBaseline() + if err != nil { + return err + } + m, err := unix.Mmap(int(f.Fd()), 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer unix.Munmap(m) + + fmt.Printf("memmap: walk file=%s steps=%d\n", path, walkSteps) + pages := int(size) / page + prevMinor, prevMajor := base.minor, base.major + done := 0 + for step := 1; step <= walkSteps; step++ { + quota := pages / walkSteps + if step == walkSteps { + quota = pages - done + } + touchReadable(m[done*page:(done+quota)*page], page) + minor, major, err := faultCounts() + if err != nil { + return err + } + fmt.Printf("memmap: step=%d/%d pages=%d minor=%d major=%d\n", + step, walkSteps, quota, minor-prevMinor, major-prevMajor) + prevMinor, prevMajor = minor, major + done += quota + } + return report(uintptr(unsafe.Pointer(&m[0])), uintptr(size), "fault-walk", base) +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +func usage() { + fmt.Fprintln(os.Stderr, + "usage: memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N]") +} + +func parseArgs(args []string) (options, error) { + opts := options{mb: 64} + modeSet := false + for i := 0; i < len(args); i++ { + switch a := args[i]; { + case a == "--mode": + i++ + if i >= len(args) { + return opts, fmt.Errorf("--mode needs a value") + } + switch args[i] { + case "stack", "heap", "mmap-anon", "mmap-file", "fault-walk": + opts.mode = args[i] + modeSet = true + default: + return opts, fmt.Errorf("unknown mode: %s", args[i]) + } + case a == "--mb": + i++ + if i >= len(args) { + return opts, fmt.Errorf("--mb needs a value") + } + mb, err := strconv.Atoi(args[i]) + if err != nil || mb < 1 || mb > 1024 { + return opts, fmt.Errorf("--mb must be 1..1024") + } + opts.mb = mb + case !strings.HasPrefix(a, "--") && opts.file == "": + opts.file = a + default: + return opts, fmt.Errorf("unexpected argument: %s", a) + } + } + if !modeSet { + return opts, fmt.Errorf("--mode is required") + } + if opts.mode == "mmap-file" && opts.file == "" { + return opts, fmt.Errorf("mmap-file needs a FILE argument") + } + if opts.mode != "mmap-file" && opts.file != "" { + return opts, fmt.Errorf("only mmap-file takes a FILE argument") + } + return opts, nil +} + +func main() { + opts, err := parseArgs(os.Args[1:]) + if err != nil { + usage() + os.Exit(2) + } + page := unix.Getpagesize() + + switch opts.mode { + case "stack": + n := min(opts.mb, stackCapMb) * mib + fmt.Printf("memmap: mode=stack bytes=%d pages=%d\n", n, n/page) + err = runStack(n, page) + case "heap": + n := opts.mb * mib + fmt.Printf("memmap: mode=heap bytes=%d pages=%d\n", n, n/page) + err = runHeap(n, page) + case "mmap-anon": + n := opts.mb * mib + fmt.Printf("memmap: mode=mmap-anon bytes=%d pages=%d\n", n, n/page) + err = runMmapAnon(n, page) + case "mmap-file": + err = runMmapFile(opts.file, page) // prints its own header line + case "fault-walk": + n := opts.mb * mib + fmt.Printf("memmap: mode=fault-walk bytes=%d pages=%d\n", n, n/page) + err = runFaultWalk(n, page) + } + if err != nil { + fmt.Fprintf(os.Stderr, "memmap: error: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/15-virtual-memory/rust/Cargo.lock b/examples/15-virtual-memory/rust/Cargo.lock new file mode 100644 index 0000000..d98f8b6 --- /dev/null +++ b/examples/15-virtual-memory/rust/Cargo.lock @@ -0,0 +1,74 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memmap-tool" +version = "0.1.0" +dependencies = [ + "anyhow", + "libc", + "rustix", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/examples/15-virtual-memory/rust/Cargo.toml b/examples/15-virtual-memory/rust/Cargo.toml new file mode 100644 index 0000000..421efb5 --- /dev/null +++ b/examples/15-virtual-memory/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "memmap-tool" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.100" +libc = "0.2" +rustix = { version = "1", features = ["mm", "param"] } diff --git a/examples/15-virtual-memory/rust/demo.sh b/examples/15-virtual-memory/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/15-virtual-memory/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/15-virtual-memory/rust/rust-toolchain.toml b/examples/15-virtual-memory/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/15-virtual-memory/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/15-virtual-memory/rust/src/main.rs b/examples/15-virtual-memory/rust/src/main.rs new file mode 100644 index 0000000..adc9de3 --- /dev/null +++ b/examples/15-virtual-memory/rust/src/main.rs @@ -0,0 +1,435 @@ +// memmap: self-inspecting virtual-memory tool — perform one kind of +// allocation, touch its pages, then report what the kernel now shows for +// this very process: the /proc/self/maps region backing the allocation, +// the VmRSS growth, and the getrusage(2) page-fault deltas. +// +// memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N] +// +// Output contract (identical across C++, Go, Rust): +// memmap: mode= bytes= pages=

+// [fault-walk only] memmap: walk file= steps=8 +// [fault-walk only] memmap: step=/8 pages=

minor= major= +// memmap: maps excerpt +// memmap: <-- target (mode=) +// memmap: vmrss_before=KB vmrss_after=KB +// memmap: faults minor= major= + +use std::ffi::c_void; +use std::fs::File; +use std::hint::black_box; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, anyhow, bail}; +use rustix::mm::{MapFlags, ProtFlags, mmap, mmap_anonymous, munmap}; + +const MIB: usize = 1 << 20; +const STACK_CAP_MB: usize = 4; // stays well inside the 8 MiB rlimit +const WALK_STEPS: usize = 8; + +// --------------------------------------------------------------------------- +// RAII wrappers +// --------------------------------------------------------------------------- + +/// Owned mmap region; unmapped on drop. +struct Mapping { + ptr: *mut c_void, + len: usize, +} + +impl Mapping { + fn anon_rw(len: usize) -> Result { + // SAFETY: NULL hint, fresh private mapping; kernel picks the address. + let ptr = unsafe { + mmap_anonymous( + std::ptr::null_mut(), + len, + ProtFlags::READ | ProtFlags::WRITE, + MapFlags::PRIVATE, + ) + } + .context("mmap")?; + Ok(Self { ptr, len }) + } + + fn file_ro(file: &File, len: usize) -> Result { + // SAFETY: NULL hint, read-only private file mapping. + let ptr = unsafe { + mmap( + std::ptr::null_mut(), + len, + ProtFlags::READ, + MapFlags::PRIVATE, + file, + 0, + ) + } + .context("mmap")?; + Ok(Self { ptr, len }) + } + + fn addr(&self) -> usize { + self.ptr as usize + } + + fn as_slice(&self) -> &[u8] { + // SAFETY: the mapping is live for the lifetime of &self. + unsafe { std::slice::from_raw_parts(self.ptr as *const u8, self.len) } + } + + fn as_mut_slice(&mut self) -> &mut [u8] { + // SAFETY: the mapping is live and writable (only built via anon_rw). + unsafe { std::slice::from_raw_parts_mut(self.ptr as *mut u8, self.len) } + } +} + +impl Drop for Mapping { + fn drop(&mut self) { + // SAFETY: ptr/len came from a successful mmap and are unmapped once. + unsafe { + let _ = munmap(self.ptr, self.len); + } + } +} + +/// Unlinks its path on drop. +struct TempFile { + path: PathBuf, +} + +impl Drop for TempFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.path); + } +} + +// --------------------------------------------------------------------------- +// /proc and getrusage probes +// --------------------------------------------------------------------------- + +struct Baseline { + rss_kb: i64, + minor: i64, + major: i64, +} + +fn vmrss_kb() -> Result { + let status = std::fs::read_to_string("/proc/self/status") + .context("open /proc/self/status")?; + for line in status.lines() { + if let Some(rest) = line.strip_prefix("VmRSS:") { + let kb = rest + .split_whitespace() + .next() + .ok_or_else(|| anyhow!("cannot parse VmRSS"))? + .parse::() + .context("cannot parse VmRSS")?; + return Ok(kb); + } + } + bail!("VmRSS not found in /proc/self/status") +} + +fn fault_counts() -> Result<(i64, i64)> { + // SAFETY: rusage is plain data; getrusage fills it or fails. + let mut ru: libc::rusage = unsafe { std::mem::zeroed() }; + let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }; + if rc != 0 { + bail!("getrusage: {}", std::io::Error::last_os_error()); + } + Ok((ru.ru_minflt, ru.ru_majflt)) +} + +fn take_baseline() -> Result { + let rss_kb = vmrss_kb()?; + let (minor, major) = fault_counts()?; + Ok(Baseline { rss_kb, minor, major }) +} + +/// Print every /proc/self/maps line whose range overlaps [addr, addr+len). +fn print_maps_excerpt(addr: usize, len: usize, mode: &str) -> Result<()> { + let maps = std::fs::read_to_string("/proc/self/maps") + .context("open /proc/self/maps")?; + println!("memmap: maps excerpt"); + let (lo, hi) = (addr, addr + len); + for line in maps.lines() { + let Some((range, _)) = line.split_once(' ') else { + continue; + }; + let Some((start_s, end_s)) = range.split_once('-') else { + continue; + }; + let (Ok(start), Ok(end)) = ( + usize::from_str_radix(start_s, 16), + usize::from_str_radix(end_s, 16), + ) else { + continue; + }; + if start < hi && end > lo { + println!("memmap: {line} <-- target (mode={mode})"); + } + } + Ok(()) +} + +/// Common tail: maps excerpt, RSS growth, fault deltas. +fn report(addr: usize, len: usize, mode: &str, base: &Baseline) -> Result<()> { + print_maps_excerpt(addr, len, mode)?; + let rss_after = vmrss_kb()?; + let (minor, major) = fault_counts()?; + println!( + "memmap: vmrss_before={}KB vmrss_after={}KB", + base.rss_kb, rss_after + ); + println!( + "memmap: faults minor={} major={}", + minor - base.minor, + major - base.major + ); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Modes +// --------------------------------------------------------------------------- + +fn touch_writable(mem: &mut [u8], page: usize) { + let mut i = 0; + while i < mem.len() { + mem[i] = 1; + i += page; + } + black_box(mem); +} + +fn touch_readable(mem: &[u8], page: usize) { + let mut sum = 0u64; + let mut i = 0; + while i < mem.len() { + sum += u64::from(mem[i]); + i += page; + } + black_box(sum); +} + +// Baseline is captured by the caller: the compiler's stack probing touches +// every page of this 4 MiB frame at function entry, so faults counted from +// inside the frame would miss the allocation itself. +#[inline(never)] +fn stack_worker(bytes: usize, page: usize, base: &Baseline) -> Result<()> { + let mut buf = [0u8; STACK_CAP_MB * MIB]; + touch_writable(&mut buf[..bytes], page); + report(buf.as_ptr() as usize, bytes, "stack", base) +} + +fn run_stack(bytes: usize, page: usize) -> Result<()> { + let base = take_baseline()?; + stack_worker(bytes, page, &base) +} + +fn run_heap(bytes: usize, page: usize) -> Result<()> { + let base = take_baseline()?; + let mut buf = vec![0u8; bytes]; + touch_writable(&mut buf, page); + report(buf.as_ptr() as usize, bytes, "heap", &base) +} + +fn run_mmap_anon(bytes: usize, page: usize) -> Result<()> { + let base = take_baseline()?; + let mut map = Mapping::anon_rw(bytes)?; + touch_writable(map.as_mut_slice(), page); + report(map.addr(), map.len, "mmap-anon", &base) +} + +fn open_for_map(path: &Path) -> Result<(File, usize)> { + let file = + File::open(path).with_context(|| path.display().to_string())?; + let len = file + .metadata() + .with_context(|| format!("{}: stat", path.display()))? + .len() as usize; + if len == 0 { + bail!("{}: file is empty", path.display()); + } + Ok((file, len)) +} + +fn run_mmap_file(path: &Path, page: usize) -> Result<()> { + let (file, len) = open_for_map(path)?; + println!( + "memmap: mode=mmap-file bytes={} pages={}", + len, + len.div_ceil(page) + ); + let base = take_baseline()?; + let map = Mapping::file_ro(&file, len)?; + touch_readable(map.as_slice(), page); + report(map.addr(), map.len, "mmap-file", &base) +} + +fn write_walk_file(bytes: usize) -> Result { + let path = std::env::temp_dir() + .join(format!("memmap-walk-{}.bin", std::process::id())); + let mut file = + File::create(&path).with_context(|| path.display().to_string())?; + let chunk = vec![0xA5u8; MIB]; + let mut left = bytes; + while left > 0 { + let n = left.min(chunk.len()); + file.write_all(&chunk[..n]) + .with_context(|| format!("{}: write", path.display()))?; + left -= n; + } + Ok(path) +} + +fn run_fault_walk(bytes: usize, page: usize) -> Result<()> { + let tmp = TempFile { + path: write_walk_file(bytes)?, + }; + let (file, len) = open_for_map(&tmp.path)?; + let base = take_baseline()?; + let map = Mapping::file_ro(&file, len)?; + + println!("memmap: walk file={} steps={}", tmp.path.display(), WALK_STEPS); + let pages = len / page; + let (mut prev_minor, mut prev_major) = (base.minor, base.major); + let mut done = 0usize; + for step in 1..=WALK_STEPS { + let quota = if step == WALK_STEPS { + pages - done + } else { + pages / WALK_STEPS + }; + touch_readable( + &map.as_slice()[done * page..(done + quota) * page], + page, + ); + let (minor, major) = fault_counts()?; + println!( + "memmap: step={}/{} pages={} minor={} major={}", + step, + WALK_STEPS, + quota, + minor - prev_minor, + major - prev_major + ); + (prev_minor, prev_major) = (minor, major); + done += quota; + } + report(map.addr(), map.len, "fault-walk", &base) +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +enum Mode { + Stack, + Heap, + MmapAnon, + MmapFile, + FaultWalk, +} + +struct Options { + mode: Mode, + mb: usize, + file: Option, +} + +fn usage() { + eprintln!( + "usage: memmap --mode stack|heap|mmap-anon|mmap-file |fault-walk [--mb N]" + ); +} + +fn parse_args(args: &[String]) -> Result { + let mut mode = None; + let mut mb = 64usize; + let mut file: Option = None; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--mode" => { + i += 1; + let m = args.get(i).ok_or_else(|| anyhow!("--mode needs a value"))?; + mode = Some(match m.as_str() { + "stack" => Mode::Stack, + "heap" => Mode::Heap, + "mmap-anon" => Mode::MmapAnon, + "mmap-file" => Mode::MmapFile, + "fault-walk" => Mode::FaultWalk, + other => bail!("unknown mode: {other}"), + }); + } + "--mb" => { + i += 1; + let v = args.get(i).ok_or_else(|| anyhow!("--mb needs a value"))?; + mb = match v.parse::() { + Ok(n) if (1..=1024).contains(&n) => n, + _ => bail!("--mb must be 1..1024"), + }; + } + a if !a.starts_with("--") && file.is_none() => { + file = Some(PathBuf::from(a)); + } + a => bail!("unexpected argument: {a}"), + } + i += 1; + } + let mode = mode.ok_or_else(|| anyhow!("--mode is required"))?; + if matches!(mode, Mode::MmapFile) && file.is_none() { + bail!("mmap-file needs a FILE argument"); + } + if !matches!(mode, Mode::MmapFile) && file.is_some() { + bail!("only mmap-file takes a FILE argument"); + } + Ok(Options { mode, mb, file }) +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let Ok(opts) = parse_args(&args) else { + usage(); + std::process::exit(2); + }; + let page = rustix::param::page_size(); + + let result = match opts.mode { + Mode::Stack => { + let bytes = opts.mb.min(STACK_CAP_MB) * MIB; + println!("memmap: mode=stack bytes={} pages={}", bytes, bytes / page); + run_stack(bytes, page) + } + Mode::Heap => { + let bytes = opts.mb * MIB; + println!("memmap: mode=heap bytes={} pages={}", bytes, bytes / page); + run_heap(bytes, page) + } + Mode::MmapAnon => { + let bytes = opts.mb * MIB; + println!( + "memmap: mode=mmap-anon bytes={} pages={}", + bytes, + bytes / page + ); + run_mmap_anon(bytes, page) + } + // Prints its own header line (size comes from the file). + Mode::MmapFile => run_mmap_file(opts.file.as_deref().unwrap(), page), + Mode::FaultWalk => { + let bytes = opts.mb * MIB; + println!( + "memmap: mode=fault-walk bytes={} pages={}", + bytes, + bytes / page + ); + run_fault_walk(bytes, page) + } + }; + if let Err(err) = result { + eprintln!("memmap: error: {err:#}"); + std::process::exit(1); + } +} diff --git a/examples/15-virtual-memory/verify.lua b/examples/15-virtual-memory/verify.lua new file mode 100644 index 0000000..0337a9b --- /dev/null +++ b/examples/15-virtual-memory/verify.lua @@ -0,0 +1,158 @@ +-- Verify 15-virtual-memory: run memmap in every mode and assert the +-- observable behavior — header shapes, the annotated /proc/self/maps +-- excerpt (region range, permissions, backing path), real VmRSS growth +-- (heap/mmap-anon must gain at least half the touched allocation), nonzero +-- minor-fault deltas, the per-step fault-walk table shape, exit codes for +-- usage/runtime errors, and that the fault-walk temp file is unlinked. +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" +-- Absolute path: each language's demo.sh runs the binary from its own +-- subdirectory, so a relative FILE would land there instead of here. +local example_dir = os.getenv("EXAMPLE_DIR") or "." +local map_file = example_dir .. "/verify-" .. lang .. "-map.bin" +local empty_file = example_dir .. "/verify-" .. lang .. "-empty.bin" + +local function num(out, pat) + return tonumber(string.match(out, pat) or "") +end + +-- Asserts on the common report tail; returns rss delta and minor faults. +local function check_report(out, mode_label, label_prefix) + checks.expect_match(out, "memmap: maps excerpt", + label_prefix .. ": maps excerpt header") + checks.expect_match(out, + "memmap:%s+%x+%-%x+ [rw%-][w%-][x%-][ps][^\n]*<%-%- target %(mode=" .. + mode_label .. "%)", + label_prefix .. ": annotated target region in maps excerpt") + checks.expect_match(out, "memmap: vmrss_before=%d+KB vmrss_after=%d+KB", + label_prefix .. ": vmrss line") + checks.expect_match(out, "memmap: faults minor=%d+ major=%d+", + label_prefix .. ": faults line") + local before = num(out, "vmrss_before=(%d+)KB") + local after = num(out, "vmrss_after=(%d+)KB") + local minor = num(out, "faults minor=(%d+) major=") + local delta = (before and after) and (after - before) or nil + return delta, minor +end + +-- 1. No arguments: usage on stderr, exit 2. +local r = checks.run(demo) +checks.expect_exit(r, 2, lang .. ": no args exits 2") +checks.expect_match(r.out, + "usage: memmap %-%-mode stack|heap|mmap%-anon|mmap%-file |fault%-walk %[%-%-mb N%]", + lang .. ": no args prints usage") + +-- 2. Unknown mode: usage, exit 2. +r = checks.run(demo .. " --mode sideways") +checks.expect_exit(r, 2, lang .. ": bad mode exits 2") + +-- 3. Heap, default 64 MiB: header, labeled rw-p region, RSS grows by at +-- least half the allocation, nonzero minor fault delta. +r = checks.run(demo .. " --mode heap") +checks.expect_exit(r, 0, lang .. ": heap exits 0") +checks.expect_match(r.out, "memmap: mode=heap bytes=67108864 pages=16384", + lang .. ": heap header line") +checks.expect_match(r.out, + "memmap:%s+%x+%-%x+ rw%-p[^\n]*<%-%- target %(mode=heap%)", + lang .. ": heap target region is rw-p") +local delta, minor = check_report(r.out, "heap", lang .. ": heap") +checks.expect_match(tostring(delta ~= nil and delta >= 32768), "true", + lang .. ": heap RSS delta >= 32768 KB (got " .. tostring(delta) .. ")") +checks.expect_match(tostring(minor ~= nil and minor > 0), "true", + lang .. ": heap minor faults > 0 (got " .. tostring(minor) .. ")") + +-- 4. Stack: clamped to 4 MiB, labeled region, nonzero minor faults. +r = checks.run(demo .. " --mode stack") +checks.expect_exit(r, 0, lang .. ": stack exits 0") +checks.expect_match(r.out, "memmap: mode=stack bytes=4194304 pages=1024", + lang .. ": stack header line (clamped to 4 MiB)") +delta, minor = check_report(r.out, "stack", lang .. ": stack") +checks.expect_match(tostring(minor ~= nil and minor > 0), "true", + lang .. ": stack minor faults > 0 (got " .. tostring(minor) .. ")") + +-- 5. mmap-anon, 32 MiB: rw-p region, RSS grows by at least half. +r = checks.run(demo .. " --mode mmap-anon --mb 32") +checks.expect_exit(r, 0, lang .. ": mmap-anon exits 0") +checks.expect_match(r.out, "memmap: mode=mmap%-anon bytes=33554432 pages=8192", + lang .. ": mmap-anon header line") +checks.expect_match(r.out, + "memmap:%s+%x+%-%x+ rw%-p[^\n]*<%-%- target %(mode=mmap%-anon%)", + lang .. ": mmap-anon target region is rw-p") +delta, minor = check_report(r.out, "mmap%-anon", lang .. ": mmap-anon") +checks.expect_match(tostring(delta ~= nil and delta >= 16384), "true", + lang .. ": mmap-anon RSS delta >= 16384 KB (got " .. tostring(delta) .. ")") +checks.expect_match(tostring(minor ~= nil and minor > 0), "true", + lang .. ": mmap-anon minor faults > 0 (got " .. tostring(minor) .. ")") + +-- 6. mmap-file on a real 2 MiB file: size from the file, r--p region whose +-- maps line names the file, nonzero minor faults. +checks.run("dd if=/dev/zero of=" .. map_file .. " bs=1M count=2 status=none") +r = checks.run(demo .. " --mode mmap-file " .. map_file) +checks.expect_exit(r, 0, lang .. ": mmap-file exits 0") +checks.expect_match(r.out, "memmap: mode=mmap%-file bytes=2097152 pages=512", + lang .. ": mmap-file header sized from the file") +checks.expect_match(r.out, + "memmap:%s+%x+%-%x+ r%-%-p[^\n]*verify%-" .. lang .. + "%-map%.bin[^\n]*<%-%- target %(mode=mmap%-file%)", + lang .. ": mmap-file target line is r--p and names the file") +delta, minor = check_report(r.out, "mmap%-file", lang .. ": mmap-file") +checks.expect_match(tostring(minor ~= nil and minor > 0), "true", + lang .. ": mmap-file minor faults > 0 (got " .. tostring(minor) .. ")") +os.remove(map_file) + +-- 7. mmap-file runtime errors: missing file and empty file both exit 1. +r = checks.run(demo .. " --mode mmap-file /nonexistent/memmap-missing.bin") +checks.expect_exit(r, 1, lang .. ": missing file exits 1") +checks.expect_match(r.out, "memmap: error:", lang .. ": missing file error line") +checks.run(": > " .. empty_file) +r = checks.run(demo .. " --mode mmap-file " .. empty_file) +checks.expect_exit(r, 1, lang .. ": empty file exits 1") +checks.expect_match(r.out, "memmap: error: [^\n]*file is empty", + lang .. ": empty file error line") +os.remove(empty_file) + +-- 8. fault-walk, 16 MiB: 8 step lines of 512 pages each with growing fault +-- counts (fault-around batches them, so per-step minors are small but +-- must be nonzero), then the summary tail, then the temp file is gone. +r = checks.run(demo .. " --mode fault-walk --mb 16") +checks.expect_exit(r, 0, lang .. ": fault-walk exits 0") +checks.expect_match(r.out, "memmap: mode=fault%-walk bytes=16777216 pages=4096", + lang .. ": fault-walk header line") +checks.expect_match(r.out, "memmap: walk file=[^\n]*memmap%-walk%-%d+%.bin steps=8", + lang .. ": fault-walk announces file and steps") +local step_minor_total = 0 +for i = 1, 8 do + checks.expect_match(r.out, + "memmap: step=" .. i .. "/8 pages=512 minor=%d+ major=%d+", + lang .. ": fault-walk step " .. i .. "/8 line") + local m = num(r.out, "step=" .. i .. "/8 pages=512 minor=(%d+) major=") + step_minor_total = step_minor_total + (m or 0) +end +checks.expect_match(tostring(step_minor_total >= 8), "true", + lang .. ": fault-walk steps accumulate minor faults (got " .. + step_minor_total .. ")") +delta, minor = check_report(r.out, "fault%-walk", lang .. ": fault-walk") +checks.expect_match(tostring(minor ~= nil and minor > 0), "true", + lang .. ": fault-walk total minor faults > 0 (got " .. tostring(minor) .. ")") +local walk_file = string.match(r.out, "walk file=(%S+) steps=8") +if walk_file then + local probe = checks.run("test -e " .. walk_file) + checks.expect_match(tostring(probe.exit ~= 0), "true", + lang .. ": fault-walk temp file was unlinked") +else + checks.expect_match("missing", "present", lang .. ": walk file path parsed") +end + +checks.finish() diff --git a/examples/16-mmap-and-shared-mappings/README.md b/examples/16-mmap-and-shared-mappings/README.md new file mode 100644 index 0000000..5d5c589 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/README.md @@ -0,0 +1,95 @@ +# 16-mmap-and-shared-mappings — shmkv v0 + +A tiny key/value store whose entire database is one `mmap`'d file, implemented +three times — C++23, Go, and Rust — against **one on-disk format**. That format +is the whole point: the Rust binary can create a store, the Go binary can write +into it, and the C++ binary can read it back, because all three agree on every +byte. `verify.lua` proves this with `cmp(1)`, not by trusting exit codes. + +Every process maps the file `MAP_SHARED`, so stores land in the shared page +cache and are visible to any other process mapping the same file; `msync(MS_SYNC)` +pushes dirty pages to disk before a write reports success. + +## The on-disk format (byte-exact) + +``` +offset 0 6 bytes magic "SHKV1\0" 53 48 4b 56 31 00 +offset 6 4 bytes slot_count, u32 little-endian +offset 10 slot_count slots, 256 bytes each: + [ 0.. 64) key, NUL-padded (max 63 bytes; key[0] == 0 means empty) + [ 64..256) value, NUL-padded (max 191 bytes) +``` + +File size is always exactly `10 + slot_count * 256` bytes — `create --slots 8` +produces a 2058-byte file, and any file whose size disagrees with its header is +rejected as `not a shmkv v0 store`. `create` truncates to zero and extends with +`ftruncate`, so every slot starts as guaranteed-zero bytes: "empty" falls out +of the file format for free. + +## CLI + +``` +shmkv create FILE --slots N # ftruncate + mmap + init header + # -> "created FILE: N slots, SIZE bytes" +shmkv set FILE KEY VALUE # linear probe: overwrite KEY's slot, else + # first empty; msync -> "set KEY" +shmkv get FILE KEY # value on stdout, or exit 4 +shmkv dump FILE # "key=value" per pair, key-sorted (stdout); + # "shmkv: n/N slots used" (stderr) +``` + +Exit codes, identical in all three languages: + +| code | meaning | +|------|----------------------------------------------------------------| +| 0 | success | +| 1 | cannot open / not a shmkv v0 store / mmap-msync failure | +| 2 | usage error, key > 63 bytes, value > 191 bytes, empty key | +| 4 | `get`: key not found (`shmkv: key not found` on stderr) | +| 5 | `set`: store full (`shmkv: store full (N slots)` on stderr) | + +## What each language shows + +- **C++** (`cpp/src/main.cpp`) — RAII throughout: an `Fd` class owns the + descriptor, a `Mapping` class owns the `mmap`/`munmap` pair and exposes + `msync` as `sync()`; every fallible path returns `std::expected`, and output + goes through `std::println`. +- **Go** (`go/main.go`) — `unix.Mmap` returns the mapping as a `[]byte`, and the + discipline is never touching that slice after `unix.Munmap`; errors wrap + causes with `%w` while a `cliError` type pins the exact stderr line and exit + code; `errors.Is`/`errors.As` route them in `main`. +- **Rust** (`rust/src/main.rs`) — the `memmap2` crate, with the program's single + `unsafe` block confined to one function (`map_shared`) behind an explicit + boundary comment spelling out why the call is sound; the `File` owns the fd + as an `OwnedFd`, and drop order (map before file) mirrors the C++ RAII types. + +The dump ordering is bytewise key sort in all three, and `set` clears the whole +256-byte slot before writing, so a shorter value never leaves a longer one's +tail behind — both details matter for the byte-identical guarantee. + +## Run it + +``` +./demo.sh build # build all three +./demo.sh cpp run create /tmp/db.kv --slots 8 +./demo.sh go run set /tmp/db.kv answer 42 +./demo.sh rust run dump /tmp/db.kv +``` + +Each language directory keeps the book-wide demo contract: `./demo.sh` builds +then runs, `./demo.sh build` builds only, `./demo.sh run [args]` runs the built +binary, and with `TARGET` set `run` deploys to that lab VM instead. + +## Verification + +`verify.lua` (one pass per `LSP_LANG`) asserts behavior, not exit-0: + +- header bytes via `od` (`53 48 4b 56 31 00` + LE slot count), file size via + `stat` (2058 bytes for 8 slots); +- CLI shapes: `set`/`get`/`dump` output, overwrite reusing its slot (2/8 used), + exit 4 on missing key, exit 5 on a full store, exit 2 on oversized keys or + values, exit 1 on a non-store file; +- **cross-language interop**: Rust creates and writes a store, Go writes into + it, C++ and Go read it back; the three `dump` outputs are compared with + `cmp`; then C++ creates a second store replaying the same operations and + Rust reads it — and `cmp` shows the two store files are byte-identical. diff --git a/examples/16-mmap-and-shared-mappings/cpp/.clang-format b/examples/16-mmap-and-shared-mappings/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/16-mmap-and-shared-mappings/cpp/.clang-tidy b/examples/16-mmap-and-shared-mappings/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/16-mmap-and-shared-mappings/cpp/CMakeLists.txt b/examples/16-mmap-and-shared-mappings/cpp/CMakeLists.txt new file mode 100644 index 0000000..4fbf3e0 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(shmkv CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/16-mmap-and-shared-mappings/cpp/CMakePresets.json b/examples/16-mmap-and-shared-mappings/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/16-mmap-and-shared-mappings/cpp/demo.sh b/examples/16-mmap-and-shared-mappings/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/16-mmap-and-shared-mappings/cpp/src/main.cpp b/examples/16-mmap-and-shared-mappings/cpp/src/main.cpp new file mode 100644 index 0000000..edd293d --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/cpp/src/main.cpp @@ -0,0 +1,333 @@ +// shmkv v0 — a tiny mmap-backed key/value store (C++23). +// +// On-disk format, byte-exact and shared with the Go and Rust implementations: +// +// offset 0 6 bytes magic "SHKV1\0" (53 48 4b 56 31 00) +// offset 6 4 bytes u32 little-endian slot_count +// offset 10 slot_count x 256-byte slots: +// [ 0.. 64) key, NUL-padded (max 63 bytes, key[0]==0 => empty) +// [ 64..256) value, NUL-padded (max 191 bytes) +// +// Every process maps the file MAP_SHARED, so a `set` from one binary is +// immediately visible to a `get` from another; msync(MS_SYNC) pushes the +// dirty pages to disk before we report success. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr std::array kMagic{'S', 'H', 'K', 'V', '1', '\0'}; +constexpr std::size_t kHeaderSize = 10; +constexpr std::size_t kKeyField = 64; +constexpr std::size_t kValueField = 192; +constexpr std::size_t kSlotSize = kKeyField + kValueField; +constexpr std::size_t kKeyMax = kKeyField - 1; // 63: room for one NUL +constexpr std::size_t kValueMax = kValueField - 1; // 191 + +// A command failure: fixed message for stderr plus the process exit code. +struct CliError { + int code; + std::string msg; +}; + +template +using Result = std::expected; + +[[nodiscard]] CliError fail(int code, std::string msg) { return {code, std::move(msg)}; } + +// RAII file descriptor. +class Fd { + public: + explicit Fd(int fd) noexcept : fd_(fd) {} + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {} + Fd& operator=(Fd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = std::exchange(other.fd_, -1); + } + return *this; + } + ~Fd() { reset(); } + [[nodiscard]] int get() const noexcept { return fd_; } + + private: + void reset() noexcept { + if (fd_ >= 0) ::close(fd_); + fd_ = -1; + } + int fd_ = -1; +}; + +// RAII shared mapping: mmap on construction, munmap in the destructor, +// msync(MS_SYNC) on demand. MAP_SHARED means stores are visible to every +// other process mapping the same file, and reach the page cache (and disk, +// after msync) rather than staying private to this address space. +class Mapping { + public: + static Result map_shared(const Fd& fd, std::size_t len) { + void* p = ::mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0); + if (p == MAP_FAILED) { + return std::unexpected(fail(1, "shmkv: mmap failed")); + } + return Mapping{static_cast(p), len}; + } + Mapping(const Mapping&) = delete; + Mapping& operator=(const Mapping&) = delete; + Mapping(Mapping&& other) noexcept + : data_(std::exchange(other.data_, nullptr)), len_(std::exchange(other.len_, 0)) {} + Mapping& operator=(Mapping&& other) noexcept { + if (this != &other) { + reset(); + data_ = std::exchange(other.data_, nullptr); + len_ = std::exchange(other.len_, 0); + } + return *this; + } + ~Mapping() { reset(); } + + [[nodiscard]] unsigned char* data() const noexcept { return data_; } + [[nodiscard]] std::size_t size() const noexcept { return len_; } + + [[nodiscard]] Result<> sync() const { + if (::msync(data_, len_, MS_SYNC) != 0) { + return std::unexpected(fail(1, "shmkv: msync failed")); + } + return {}; + } + + private: + Mapping(unsigned char* data, std::size_t len) noexcept : data_(data), len_(len) {} + void reset() noexcept { + if (data_ != nullptr) ::munmap(data_, len_); + data_ = nullptr; + len_ = 0; + } + unsigned char* data_ = nullptr; + std::size_t len_ = 0; +}; + +// An open store: owns the fd and the mapping, decodes the header once. +class Store { + public: + static Result create(const std::string& path, std::uint32_t slots) { + // O_TRUNC first so a reused path starts from zero bytes; the + // following ftruncate then extends with guaranteed-zero pages, + // which is what makes every slot "empty" (key[0] == 0) for free. + Fd fd{::open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644)}; + if (fd.get() < 0) { + return std::unexpected(fail(1, "shmkv: cannot open " + path)); + } + const std::size_t len = kHeaderSize + static_cast(slots) * kSlotSize; + if (::ftruncate(fd.get(), static_cast(len)) != 0) { + return std::unexpected(fail(1, "shmkv: ftruncate failed on " + path)); + } + auto map = Mapping::map_shared(fd, len); + if (!map) return std::unexpected(map.error()); + std::memcpy(map->data(), kMagic.data(), kMagic.size()); + const std::array le{ + static_cast(slots & 0xff), + static_cast((slots >> 8) & 0xff), + static_cast((slots >> 16) & 0xff), + static_cast((slots >> 24) & 0xff), + }; + std::memcpy(map->data() + kMagic.size(), le.data(), le.size()); + if (auto s = map->sync(); !s) return std::unexpected(s.error()); + return Store{std::move(fd), std::move(*map), slots}; + } + + static Result open(const std::string& path) { + Fd fd{::open(path.c_str(), O_RDWR)}; + if (fd.get() < 0) { + return std::unexpected(fail(1, "shmkv: cannot open " + path)); + } + struct stat st{}; + if (::fstat(fd.get(), &st) != 0) { + return std::unexpected(fail(1, "shmkv: cannot open " + path)); + } + const auto len = static_cast(st.st_size); + CliError bad = fail(1, "shmkv: " + path + ": not a shmkv v0 store"); + if (len < kHeaderSize) return std::unexpected(std::move(bad)); + auto map = Mapping::map_shared(fd, len); + if (!map) return std::unexpected(map.error()); + if (std::memcmp(map->data(), kMagic.data(), kMagic.size()) != 0) { + return std::unexpected(std::move(bad)); + } + std::uint32_t slots = 0; + for (std::size_t i = 0; i < 4; ++i) { + slots |= static_cast(map->data()[kMagic.size() + i]) << (8 * i); + } + if (len != kHeaderSize + static_cast(slots) * kSlotSize) { + return std::unexpected(std::move(bad)); + } + return Store{std::move(fd), std::move(*map), slots}; + } + + [[nodiscard]] std::uint32_t slot_count() const noexcept { return slots_; } + + [[nodiscard]] std::string_view slot_key(std::uint32_t i) const { + const unsigned char* s = map_.data() + kHeaderSize + std::size_t{i} * kSlotSize; + return field(s, kKeyField); + } + + [[nodiscard]] std::string_view slot_value(std::uint32_t i) const { + const unsigned char* s = map_.data() + kHeaderSize + std::size_t{i} * kSlotSize; + return field(s + kKeyField, kValueField); + } + + // Linear probe: overwrite the slot holding `key`, else claim the first + // empty slot. Exit-5 failure when every slot is taken. + [[nodiscard]] Result<> set(std::string_view key, std::string_view value) { + std::uint32_t target = slots_; + bool overwrite = false; + std::uint32_t first_empty = slots_; + for (std::uint32_t i = 0; i < slots_; ++i) { + const auto k = slot_key(i); + if (k == key) { + target = i; + overwrite = true; + break; + } + if (k.empty() && first_empty == slots_) first_empty = i; + } + if (!overwrite) target = first_empty; + if (target == slots_) { + return std::unexpected(fail(5, std::format("shmkv: store full ({} slots)", slots_))); + } + unsigned char* s = map_.data() + kHeaderSize + std::size_t{target} * kSlotSize; + std::memset(s, 0, kSlotSize); // clear any longer previous value + std::memcpy(s, key.data(), key.size()); + std::memcpy(s + kKeyField, value.data(), value.size()); + return map_.sync(); + } + + private: + Store(Fd fd, Mapping map, std::uint32_t slots) noexcept + : fd_(std::move(fd)), map_(std::move(map)), slots_(slots) {} + + [[nodiscard]] static std::string_view field(const unsigned char* p, std::size_t max) { + const auto* nul = static_cast(std::memchr(p, 0, max)); + const std::size_t n = (nul != nullptr) ? static_cast(nul - p) : max; + return {reinterpret_cast(p), n}; + } + + Fd fd_; + Mapping map_; + std::uint32_t slots_; +}; + +[[nodiscard]] Result parse_slots(std::string_view text) { + // Digits only, in range [1, u32 max] — identical rules in all three + // languages so the CLIs reject exactly the same inputs. + if (text.empty() || text.size() > 10 || + !std::ranges::all_of(text, [](char c) { return c >= '0' && c <= '9'; })) { + return std::unexpected(fail(2, "")); + } + std::uint64_t v = 0; + for (const char c : text) v = v * 10 + static_cast(c - '0'); + if (v == 0 || v > 0xffffffffULL) return std::unexpected(fail(2, "")); + return static_cast(v); +} + +[[nodiscard]] Result<> cmd_create(const std::string& file, std::string_view slots_text) { + const auto slots = parse_slots(slots_text); + if (!slots) return std::unexpected(slots.error()); + auto store = Store::create(file, *slots); + if (!store) return std::unexpected(store.error()); + const std::size_t bytes = kHeaderSize + std::size_t{*slots} * kSlotSize; + std::println("created {}: {} slots, {} bytes", file, *slots, bytes); + return {}; +} + +[[nodiscard]] Result<> cmd_set(const std::string& file, std::string_view key, + std::string_view value) { + if (key.empty()) return std::unexpected(fail(2, "shmkv: empty key")); + if (key.size() > kKeyMax) { + return std::unexpected(fail(2, "shmkv: key too long (max 63 bytes)")); + } + if (value.size() > kValueMax) { + return std::unexpected(fail(2, "shmkv: value too long (max 191 bytes)")); + } + auto store = Store::open(file); + if (!store) return std::unexpected(store.error()); + if (auto r = store->set(key, value); !r) return std::unexpected(r.error()); + std::println("set {}", key); + return {}; +} + +[[nodiscard]] Result<> cmd_get(const std::string& file, std::string_view key) { + auto store = Store::open(file); + if (!store) return std::unexpected(store.error()); + for (std::uint32_t i = 0; i < store->slot_count(); ++i) { + if (store->slot_key(i) == key) { + std::println("{}", store->slot_value(i)); + return {}; + } + } + return std::unexpected(fail(4, "shmkv: key not found")); +} + +[[nodiscard]] Result<> cmd_dump(const std::string& file) { + auto store = Store::open(file); + if (!store) return std::unexpected(store.error()); + std::vector> pairs; + for (std::uint32_t i = 0; i < store->slot_count(); ++i) { + const auto k = store->slot_key(i); + if (!k.empty()) pairs.emplace_back(std::string{k}, std::string{store->slot_value(i)}); + } + std::ranges::sort(pairs); // bytewise by key, matching Go/Rust + for (const auto& [k, v] : pairs) std::println("{}={}", k, v); + // Flush before the stderr summary: stdout is fully buffered on a pipe, + // and Go/Rust emit the pairs first — keep the interleaving identical. + std::fflush(stdout); + std::println(stderr, "shmkv: {}/{} slots used", pairs.size(), store->slot_count()); + return {}; +} + +int usage() { + std::println(stderr, "usage: shmkv create FILE --slots N | set FILE KEY VALUE | " + "get FILE KEY | dump FILE"); + return 2; +} + +} // namespace + +int main(int argc, char** argv) { + const std::vector args(argv + 1, argv + argc); + Result<> r; + if (args.size() == 4 && args[0] == "create" && args[2] == "--slots") { + r = cmd_create(args[1], args[3]); + } else if (args.size() == 4 && args[0] == "set") { + r = cmd_set(args[1], args[2], args[3]); + } else if (args.size() == 3 && args[0] == "get") { + r = cmd_get(args[1], args[2]); + } else if (args.size() == 2 && args[0] == "dump") { + r = cmd_dump(args[1]); + } else { + return usage(); + } + if (!r) { + if (r.error().msg.empty()) return usage(); + std::println(stderr, "{}", r.error().msg); + return r.error().code; + } + return 0; +} diff --git a/examples/16-mmap-and-shared-mappings/demo.sh b/examples/16-mmap-and-shared-mappings/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/16-mmap-and-shared-mappings/go/demo.sh b/examples/16-mmap-and-shared-mappings/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/16-mmap-and-shared-mappings/go/go.mod b/examples/16-mmap-and-shared-mappings/go/go.mod new file mode 100644 index 0000000..5ced5c2 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/16-mmap-and-shared-mappings/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/16-mmap-and-shared-mappings/go/go.sum b/examples/16-mmap-and-shared-mappings/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/16-mmap-and-shared-mappings/go/main.go b/examples/16-mmap-and-shared-mappings/go/main.go new file mode 100644 index 0000000..7fad70b --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/go/main.go @@ -0,0 +1,329 @@ +// shmkv v0 — a tiny mmap-backed key/value store (Go). +// +// On-disk format, byte-exact and shared with the C++ and Rust implementations: +// +// offset 0 6 bytes magic "SHKV1\0" (53 48 4b 56 31 00) +// offset 6 4 bytes u32 little-endian slot_count +// offset 10 slot_count x 256-byte slots: +// [ 0.. 64) key, NUL-padded (max 63 bytes, key[0]==0 => empty) +// [ 64..256) value, NUL-padded (max 191 bytes) +// +// The mapping is unix.Mmap with MAP_SHARED: the returned []byte aliases the +// page cache, so writes are visible to every other process mapping the same +// file, and unix.Msync(MS_SYNC) pushes them to disk before we report success. +package main + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "os" + "sort" + + "golang.org/x/sys/unix" +) + +const ( + headerSize = 10 + keyField = 64 + valueField = 192 + slotSize = keyField + valueField + keyMax = keyField - 1 // 63: room for one NUL + valueMax = valueField - 1 // 191 +) + +var magic = []byte{'S', 'H', 'K', 'V', '1', 0} + +// errUsage makes main print the usage line and exit 2. +var errUsage = errors.New("usage") + +// cliError pins the exact stderr line and exit code (byte-identical with the +// C++ and Rust binaries) while still wrapping the underlying cause for %w / +// errors.Is chains. +type cliError struct { + code int + msg string + err error +} + +func (e *cliError) Error() string { + if e.err != nil { + return fmt.Sprintf("%s: %v", e.msg, e.err) + } + return e.msg +} + +func (e *cliError) Unwrap() error { return e.err } + +func failf(code int, cause error, format string, a ...any) error { + return &cliError{code: code, msg: fmt.Sprintf(format, a...), err: cause} +} + +// store owns the fd and the shared mapping; close() unmaps then closes, +// mirroring the RAII types in the C++ and Rust versions. +type store struct { + fd int + data []byte + slots uint32 +} + +func (s *store) close() { + if s.data != nil { + _ = unix.Munmap(s.data) // []byte discipline: never touch s.data after this + s.data = nil + } + if s.fd >= 0 { + _ = unix.Close(s.fd) + s.fd = -1 + } +} + +func mapShared(fd int, size int) ([]byte, error) { + data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) + if err != nil { + return nil, failf(1, fmt.Errorf("mmap: %w", err), "shmkv: mmap failed") + } + return data, nil +} + +func createStore(path string, slots uint32) (*store, error) { + // O_TRUNC first so a reused path starts from zero bytes; the following + // Ftruncate then extends with guaranteed-zero pages, which makes every + // slot "empty" (key[0] == 0) for free. + fd, err := unix.Open(path, unix.O_RDWR|unix.O_CREAT|unix.O_TRUNC, 0o644) + if err != nil { + return nil, failf(1, fmt.Errorf("open: %w", err), "shmkv: cannot open %s", path) + } + size := headerSize + int(slots)*slotSize + if err := unix.Ftruncate(fd, int64(size)); err != nil { + unix.Close(fd) + return nil, failf(1, fmt.Errorf("ftruncate: %w", err), "shmkv: ftruncate failed on %s", path) + } + data, err := mapShared(fd, size) + if err != nil { + unix.Close(fd) + return nil, err + } + s := &store{fd: fd, data: data, slots: slots} + copy(s.data, magic) + binary.LittleEndian.PutUint32(s.data[len(magic):headerSize], slots) + if err := unix.Msync(s.data, unix.MS_SYNC); err != nil { + s.close() + return nil, failf(1, fmt.Errorf("msync: %w", err), "shmkv: msync failed") + } + return s, nil +} + +func openStore(path string) (*store, error) { + fd, err := unix.Open(path, unix.O_RDWR, 0) + if err != nil { + return nil, failf(1, fmt.Errorf("open: %w", err), "shmkv: cannot open %s", path) + } + var st unix.Stat_t + if err := unix.Fstat(fd, &st); err != nil { + unix.Close(fd) + return nil, failf(1, fmt.Errorf("fstat: %w", err), "shmkv: cannot open %s", path) + } + size := int(st.Size) + badStore := func() error { + return failf(1, nil, "shmkv: %s: not a shmkv v0 store", path) + } + if size < headerSize { + unix.Close(fd) + return nil, badStore() + } + data, err := mapShared(fd, size) + if err != nil { + unix.Close(fd) + return nil, err + } + s := &store{fd: fd, data: data, slots: 0} + if !bytes.Equal(s.data[:len(magic)], magic) { + s.close() + return nil, badStore() + } + s.slots = binary.LittleEndian.Uint32(s.data[len(magic):headerSize]) + if size != headerSize+int(s.slots)*slotSize { + s.close() + return nil, badStore() + } + return s, nil +} + +func field(b []byte) string { + if i := bytes.IndexByte(b, 0); i >= 0 { + return string(b[:i]) + } + return string(b) +} + +func (s *store) slot(i uint32) []byte { + off := headerSize + int(i)*slotSize + return s.data[off : off+slotSize] +} + +func (s *store) slotKey(i uint32) string { return field(s.slot(i)[:keyField]) } +func (s *store) slotValue(i uint32) string { return field(s.slot(i)[keyField:]) } + +// set: linear probe — overwrite the slot holding key, else claim the first +// empty slot; exit-5 failure when every slot is taken. +func (s *store) set(key, value string) error { + target := s.slots + overwrite := false + firstEmpty := s.slots + for i := uint32(0); i < s.slots; i++ { + k := s.slotKey(i) + if k == key { + target = i + overwrite = true + break + } + if k == "" && firstEmpty == s.slots { + firstEmpty = i + } + } + if !overwrite { + target = firstEmpty + } + if target == s.slots { + return failf(5, nil, "shmkv: store full (%d slots)", s.slots) + } + slot := s.slot(target) + for i := range slot { // clear any longer previous value + slot[i] = 0 + } + copy(slot[:keyField], key) + copy(slot[keyField:], value) + if err := unix.Msync(s.data, unix.MS_SYNC); err != nil { + return failf(1, fmt.Errorf("msync: %w", err), "shmkv: msync failed") + } + return nil +} + +// parseSlots: digits only, in range [1, u32 max] — identical rules in all +// three languages so the CLIs reject exactly the same inputs. +func parseSlots(text string) (uint32, error) { + if text == "" || len(text) > 10 { + return 0, errUsage + } + var v uint64 + for _, c := range []byte(text) { + if c < '0' || c > '9' { + return 0, errUsage + } + v = v*10 + uint64(c-'0') + } + if v == 0 || v > 0xffffffff { + return 0, errUsage + } + return uint32(v), nil +} + +func cmdCreate(file, slotsText string) error { + slots, err := parseSlots(slotsText) + if err != nil { + return err + } + s, err := createStore(file, slots) + if err != nil { + return err + } + defer s.close() + fmt.Printf("created %s: %d slots, %d bytes\n", file, slots, headerSize+int(slots)*slotSize) + return nil +} + +func cmdSet(file, key, value string) error { + if key == "" { + return failf(2, nil, "shmkv: empty key") + } + if len(key) > keyMax { + return failf(2, nil, "shmkv: key too long (max 63 bytes)") + } + if len(value) > valueMax { + return failf(2, nil, "shmkv: value too long (max 191 bytes)") + } + s, err := openStore(file) + if err != nil { + return err + } + defer s.close() + if err := s.set(key, value); err != nil { + return err + } + fmt.Printf("set %s\n", key) + return nil +} + +func cmdGet(file, key string) error { + s, err := openStore(file) + if err != nil { + return err + } + defer s.close() + for i := uint32(0); i < s.slots; i++ { + if s.slotKey(i) == key { + fmt.Println(s.slotValue(i)) + return nil + } + } + return failf(4, nil, "shmkv: key not found") +} + +func cmdDump(file string) error { + s, err := openStore(file) + if err != nil { + return err + } + defer s.close() + type pair struct{ k, v string } + var pairs []pair + for i := uint32(0); i < s.slots; i++ { + if k := s.slotKey(i); k != "" { + pairs = append(pairs, pair{k, s.slotValue(i)}) + } + } + sort.Slice(pairs, func(a, b int) bool { return pairs[a].k < pairs[b].k }) // bytewise + for _, p := range pairs { + fmt.Printf("%s=%s\n", p.k, p.v) + } + fmt.Fprintf(os.Stderr, "shmkv: %d/%d slots used\n", len(pairs), s.slots) + return nil +} + +func usage() { + fmt.Fprintln(os.Stderr, + "usage: shmkv create FILE --slots N | set FILE KEY VALUE | get FILE KEY | dump FILE") + os.Exit(2) +} + +func main() { + args := os.Args[1:] + var err error + switch { + case len(args) == 4 && args[0] == "create" && args[2] == "--slots": + err = cmdCreate(args[1], args[3]) + case len(args) == 4 && args[0] == "set": + err = cmdSet(args[1], args[2], args[3]) + case len(args) == 3 && args[0] == "get": + err = cmdGet(args[1], args[2]) + case len(args) == 2 && args[0] == "dump": + err = cmdDump(args[1]) + default: + usage() + } + if err == nil { + return + } + if errors.Is(err, errUsage) { + usage() + } + var ce *cliError + if errors.As(err, &ce) { + fmt.Fprintln(os.Stderr, ce.msg) + os.Exit(ce.code) + } + fmt.Fprintf(os.Stderr, "shmkv: %v\n", err) + os.Exit(1) +} diff --git a/examples/16-mmap-and-shared-mappings/rust/Cargo.lock b/examples/16-mmap-and-shared-mappings/rust/Cargo.lock new file mode 100644 index 0000000..7364815 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/rust/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "shmkv" +version = "0.1.0" +dependencies = [ + "memmap2", +] diff --git a/examples/16-mmap-and-shared-mappings/rust/Cargo.toml b/examples/16-mmap-and-shared-mappings/rust/Cargo.toml new file mode 100644 index 0000000..1023304 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "shmkv" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +memmap2 = "0.9" diff --git a/examples/16-mmap-and-shared-mappings/rust/demo.sh b/examples/16-mmap-and-shared-mappings/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/16-mmap-and-shared-mappings/rust/rust-toolchain.toml b/examples/16-mmap-and-shared-mappings/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/16-mmap-and-shared-mappings/rust/src/main.rs b/examples/16-mmap-and-shared-mappings/rust/src/main.rs new file mode 100644 index 0000000..2a75c22 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/rust/src/main.rs @@ -0,0 +1,261 @@ +// shmkv v0 — a tiny mmap-backed key/value store (Rust, edition 2024). +// +// On-disk format, byte-exact and shared with the C++ and Go implementations: +// +// offset 0 6 bytes magic "SHKV1\0" (53 48 4b 56 31 00) +// offset 6 4 bytes u32 little-endian slot_count +// offset 10 slot_count x 256-byte slots: +// [ 0.. 64) key, NUL-padded (max 63 bytes, key[0]==0 => empty) +// [ 64..256) value, NUL-padded (max 191 bytes) +// +// The mapping is memmap2::MmapMut over a MAP_SHARED mapping: writes are +// visible to every other process mapping the same file, and flush() (msync +// with MS_SYNC) pushes them to disk before we report success. + +use std::fs::OpenOptions; +use std::process::ExitCode; + +use memmap2::MmapMut; + +const HEADER_SIZE: usize = 10; +const KEY_FIELD: usize = 64; +const VALUE_FIELD: usize = 192; +const SLOT_SIZE: usize = KEY_FIELD + VALUE_FIELD; +const KEY_MAX: usize = KEY_FIELD - 1; // 63: room for one NUL +const VALUE_MAX: usize = VALUE_FIELD - 1; // 191 +const MAGIC: [u8; 6] = *b"SHKV1\0"; + +/// A command failure: fixed stderr line plus the process exit code. +/// An empty message means "print the usage line" (exit 2). +struct Fail { + code: u8, + msg: String, +} + +impl Fail { + fn new(code: u8, msg: impl Into) -> Self { + Self { code, msg: msg.into() } + } + fn usage() -> Self { + Self { code: 2, msg: String::new() } + } +} + +type Result = std::result::Result; + +/// An open store. `file` is a std::fs::File, which owns the descriptor as an +/// OwnedFd — dropping the Store unmaps (MmapMut's Drop runs munmap) and then +/// closes the fd, in that order (fields drop in declaration order). +struct Store { + map: MmapMut, + slots: u32, + _file: std::fs::File, +} + +/// Map `file` MAP_SHARED, read/write. +/// +/// ---- unsafe boundary ------------------------------------------------- +/// `MmapMut::map_mut` is `unsafe` because the compiler cannot see other +/// writers of the underlying file: another process truncating it would +/// turn our in-bounds loads into SIGBUS, and concurrent mutation would be +/// a data race the borrow checker never observed. Why the call is sound +/// here: +/// * every access goes through `&self.map[..]` slices bounded by the +/// length we mapped, never raw pointers; +/// * we size the mapping from ftruncate/fstat in the same process and +/// no shmkv command ever shrinks an existing store file; +/// * the demo runs shmkv commands sequentially, so cross-process writes +/// are ordered by process exit (and each writer msyncs before exit) — +/// the concurrent-writer race the type system cannot rule out is +/// excluded by the tool's usage model, not by the compiler. +/// Everything outside this function stays in safe Rust. +/// ---------------------------------------------------------------------- +fn map_shared(file: &std::fs::File) -> Result { + unsafe { MmapMut::map_mut(file) }.map_err(|_| Fail::new(1, "shmkv: mmap failed")) +} + +impl Store { + fn create(path: &str, slots: u32) -> Result { + // truncate(true) first so a reused path starts from zero bytes; the + // following set_len (ftruncate) then extends with guaranteed-zero + // pages, which makes every slot "empty" (key[0] == 0) for free. + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(path) + .map_err(|_| Fail::new(1, format!("shmkv: cannot open {path}")))?; + let len = HEADER_SIZE as u64 + u64::from(slots) * SLOT_SIZE as u64; + file.set_len(len) + .map_err(|_| Fail::new(1, format!("shmkv: ftruncate failed on {path}")))?; + let mut map = map_shared(&file)?; + map[..MAGIC.len()].copy_from_slice(&MAGIC); + map[MAGIC.len()..HEADER_SIZE].copy_from_slice(&slots.to_le_bytes()); + map.flush().map_err(|_| Fail::new(1, "shmkv: msync failed"))?; + Ok(Store { map, slots, _file: file }) + } + + fn open(path: &str) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .open(path) + .map_err(|_| Fail::new(1, format!("shmkv: cannot open {path}")))?; + let len = file + .metadata() + .map_err(|_| Fail::new(1, format!("shmkv: cannot open {path}")))? + .len() as usize; + let bad = || Fail::new(1, format!("shmkv: {path}: not a shmkv v0 store")); + if len < HEADER_SIZE { + return Err(bad()); + } + let map = map_shared(&file)?; + if map[..MAGIC.len()] != MAGIC { + return Err(bad()); + } + let slots = u32::from_le_bytes(map[MAGIC.len()..HEADER_SIZE].try_into().unwrap()); + if len != HEADER_SIZE + slots as usize * SLOT_SIZE { + return Err(bad()); + } + Ok(Store { map, slots, _file: file }) + } + + fn slot(&self, i: u32) -> &[u8] { + let off = HEADER_SIZE + i as usize * SLOT_SIZE; + &self.map[off..off + SLOT_SIZE] + } + + fn slot_key(&self, i: u32) -> &[u8] { + field(&self.slot(i)[..KEY_FIELD]) + } + + fn slot_value(&self, i: u32) -> &[u8] { + field(&self.slot(i)[KEY_FIELD..]) + } + + /// Linear probe: overwrite the slot holding `key`, else claim the first + /// empty slot. Exit-5 failure when every slot is taken. + fn set(&mut self, key: &[u8], value: &[u8]) -> Result<()> { + let mut target = self.slots; + let mut overwrite = false; + let mut first_empty = self.slots; + for i in 0..self.slots { + let k = self.slot_key(i); + if k == key { + target = i; + overwrite = true; + break; + } + if k.is_empty() && first_empty == self.slots { + first_empty = i; + } + } + if !overwrite { + target = first_empty; + } + if target == self.slots { + return Err(Fail::new(5, format!("shmkv: store full ({} slots)", self.slots))); + } + let off = HEADER_SIZE + target as usize * SLOT_SIZE; + let slot = &mut self.map[off..off + SLOT_SIZE]; + slot.fill(0); // clear any longer previous value + slot[..key.len()].copy_from_slice(key); + slot[KEY_FIELD..KEY_FIELD + value.len()].copy_from_slice(value); + self.map.flush().map_err(|_| Fail::new(1, "shmkv: msync failed")) + } +} + +fn field(bytes: &[u8]) -> &[u8] { + match bytes.iter().position(|&b| b == 0) { + Some(n) => &bytes[..n], + None => bytes, + } +} + +/// Digits only, in range [1, u32 max] — identical rules in all three +/// languages so the CLIs reject exactly the same inputs. +fn parse_slots(text: &str) -> Result { + if text.is_empty() || text.len() > 10 || !text.bytes().all(|b| b.is_ascii_digit()) { + return Err(Fail::usage()); + } + let v: u64 = text.bytes().fold(0, |acc, b| acc * 10 + u64::from(b - b'0')); + if v == 0 || v > u64::from(u32::MAX) { + return Err(Fail::usage()); + } + Ok(v as u32) +} + +fn cmd_create(file: &str, slots_text: &str) -> Result<()> { + let slots = parse_slots(slots_text)?; + let _store = Store::create(file, slots)?; + let bytes = HEADER_SIZE as u64 + u64::from(slots) * SLOT_SIZE as u64; + println!("created {file}: {slots} slots, {bytes} bytes"); + Ok(()) +} + +fn cmd_set(file: &str, key: &str, value: &str) -> Result<()> { + if key.is_empty() { + return Err(Fail::new(2, "shmkv: empty key")); + } + if key.len() > KEY_MAX { + return Err(Fail::new(2, "shmkv: key too long (max 63 bytes)")); + } + if value.len() > VALUE_MAX { + return Err(Fail::new(2, "shmkv: value too long (max 191 bytes)")); + } + let mut store = Store::open(file)?; + store.set(key.as_bytes(), value.as_bytes())?; + println!("set {key}"); + Ok(()) +} + +fn cmd_get(file: &str, key: &str) -> Result<()> { + let store = Store::open(file)?; + for i in 0..store.slots { + if store.slot_key(i) == key.as_bytes() { + println!("{}", String::from_utf8_lossy(store.slot_value(i))); + return Ok(()); + } + } + Err(Fail::new(4, "shmkv: key not found")) +} + +fn cmd_dump(file: &str) -> Result<()> { + let store = Store::open(file)?; + let mut pairs: Vec<(Vec, Vec)> = (0..store.slots) + .filter(|&i| !store.slot_key(i).is_empty()) + .map(|i| (store.slot_key(i).to_vec(), store.slot_value(i).to_vec())) + .collect(); + pairs.sort(); // bytewise by key, matching C++/Go + for (k, v) in &pairs { + println!("{}={}", String::from_utf8_lossy(k), String::from_utf8_lossy(v)); + } + eprintln!("shmkv: {}/{} slots used", pairs.len(), store.slots); + Ok(()) +} + +fn usage() -> ExitCode { + eprintln!("usage: shmkv create FILE --slots N | set FILE KEY VALUE | get FILE KEY | dump FILE"); + ExitCode::from(2) +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let strs: Vec<&str> = args.iter().map(String::as_str).collect(); + let result = match strs.as_slice() { + ["create", file, "--slots", n] => cmd_create(file, n), + ["set", file, key, value] => cmd_set(file, key, value), + ["get", file, key] => cmd_get(file, key), + ["dump", file] => cmd_dump(file), + _ => return usage(), + }; + match result { + Ok(()) => ExitCode::SUCCESS, + Err(f) if f.msg.is_empty() => usage(), + Err(f) => { + eprintln!("{}", f.msg); + ExitCode::from(f.code) + } + } +} diff --git a/examples/16-mmap-and-shared-mappings/verify.lua b/examples/16-mmap-and-shared-mappings/verify.lua new file mode 100644 index 0000000..558b9e4 --- /dev/null +++ b/examples/16-mmap-and-shared-mappings/verify.lua @@ -0,0 +1,189 @@ +-- Verify 16-mmap-and-shared-mappings: shmkv v0, an mmap-backed KV store with +-- ONE on-disk format shared by all three languages. Asserts behavior: exact +-- header bytes (od), file sizes (stat), CLI stdout/stderr shapes and exit +-- codes — and, the point of the example, CROSS-LANGUAGE INTEROP: a store +-- created and written by the Rust binary is read by the Go and C++ binaries +-- (byte-identical dumps), then the reverse (created by C++, read by Rust), +-- with cmp(1) proving the store files themselves are byte-identical. +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and EXAMPLE_DIR set. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +-- Absolute example dir: each language's demo.sh runs the binary from its own +-- subdirectory, so relative FILE arguments would land there instead of here. +local dir = os.getenv("EXAMPLE_DIR") +if not dir or dir == "" or dir == "." then + dir = checks.run("pwd").out:gsub("%s+$", "") +end + +local demo = { + cpp = "./demo.sh cpp run", + go = "./demo.sh go run", + rust = "./demo.sh rust run", +} +local bins = { + cpp = "cpp/build/release/app", + go = "go/bin/app", + rust = "rust/target/release/app", +} + +-- The interop checks drive all three binaries no matter which LSP_LANG this +-- pass is for; build any that the orchestrator has not built yet. +for l, bin in pairs(bins) do + local f = io.open(dir .. "/" .. bin, "r") + if f then f:close() else checks.run("./" .. l .. "/demo.sh build") end +end + +local D = demo[lang] +local store = dir .. "/verify-" .. lang .. ".kv" +local scratch = { + store, dir .. "/verify-" .. lang .. "-full.kv", dir .. "/verify-" .. lang .. "-bad.kv", + dir .. "/verify-x.kv", dir .. "/verify-y.kv", + dir .. "/verify-dump-cpp.txt", dir .. "/verify-dump-go.txt", + dir .. "/verify-dump-rust.txt", dir .. "/verify-dump-y.txt", +} +for _, f in ipairs(scratch) do os.remove(f) end + +-- ---------------------------------------------------------------- CLI shape +local r = checks.run(D) +checks.expect_exit(r, 2, lang .. ": no args exits 2") +checks.expect_match(r.out, "usage: shmkv create FILE %-%-slots N | set FILE KEY VALUE", + lang .. ": no args prints usage") + +r = checks.run(D .. " frobnicate " .. store) +checks.expect_exit(r, 2, lang .. ": unknown subcommand exits 2") + +-- ------------------------------------------------- create: format on disk +r = checks.run(D .. " create " .. store .. " --slots 8") +checks.expect_exit(r, 0, lang .. ": create exits 0") +checks.expect_match(r.out, "created .*verify%-" .. lang .. "%.kv: 8 slots, 2058 bytes", + lang .. ": create reports slots and bytes") + +r = checks.run("stat -c %s " .. store) +checks.expect_match(r.out, "^2058%s*$", lang .. ": file is exactly 10 + 8*256 bytes") + +-- Header, byte for byte: magic "SHKV1\0" then u32 LE slot_count == 8. +r = checks.run("od -An -tx1 -N10 " .. store) +checks.expect_match(r.out, "53 48 4b 56 31 00 08 00 00 00", + lang .. ": header bytes are SHKV1\\0 + u32le 8") + +-- ------------------------------------------------------- set / get / dump +r = checks.run(D .. " set " .. store .. " alpha one") +checks.expect_exit(r, 0, lang .. ": set exits 0") +checks.expect_match(r.out, "set alpha", lang .. ": set echoes the key") +checks.run(D .. " set " .. store .. " beta two") +r = checks.run(D .. " set " .. store .. " alpha uno") -- overwrite in place +checks.expect_exit(r, 0, lang .. ": overwrite set exits 0") + +r = checks.run(D .. " get " .. store .. " alpha") +checks.expect_exit(r, 0, lang .. ": get exits 0") +checks.expect_match(r.out, "^uno\n$", lang .. ": get returns the overwritten value") + +r = checks.run(D .. " get " .. store .. " gamma") +checks.expect_exit(r, 4, lang .. ": missing key exits 4") +checks.expect_match(r.out, "shmkv: key not found", lang .. ": missing key message") + +r = checks.run(D .. " dump " .. store) +checks.expect_exit(r, 0, lang .. ": dump exits 0") +checks.expect_match(r.out, "alpha=uno\nbeta=two\n", lang .. ": dump sorted pairs") +checks.expect_match(r.out, "shmkv: 2/8 slots used", + lang .. ": overwrite burned no extra slot (2/8 used)") + +-- ---------------------------------------------------------- failure modes +local full = dir .. "/verify-" .. lang .. "-full.kv" +checks.run(D .. " create " .. full .. " --slots 2") +checks.run(D .. " set " .. full .. " k1 v1") +checks.run(D .. " set " .. full .. " k2 v2") +r = checks.run(D .. " set " .. full .. " k3 v3") +checks.expect_exit(r, 5, lang .. ": set on a full store exits 5") +checks.expect_match(r.out, "shmkv: store full %(2 slots%)", lang .. ": store-full message") + +r = checks.run(D .. " set " .. full .. " " .. string.rep("k", 64) .. " v") +checks.expect_exit(r, 2, lang .. ": 64-byte key exits 2") +checks.expect_match(r.out, "shmkv: key too long %(max 63 bytes%)", lang .. ": key-too-long message") + +r = checks.run(D .. " set " .. full .. " k " .. string.rep("v", 192)) +checks.expect_exit(r, 2, lang .. ": 192-byte value exits 2") +checks.expect_match(r.out, "shmkv: value too long %(max 191 bytes%)", + lang .. ": value-too-long message") + +local badf = dir .. "/verify-" .. lang .. "-bad.kv" +checks.run("printf 'this is not a shmkv store, not even close' > " .. badf) +r = checks.run(D .. " get " .. badf .. " k") +checks.expect_exit(r, 1, lang .. ": non-store file exits 1") +checks.expect_match(r.out, "not a shmkv v0 store", lang .. ": bad-magic message") + +r = checks.run(D .. " get " .. dir .. "/verify-does-not-exist.kv k") +checks.expect_exit(r, 1, lang .. ": missing file exits 1") +checks.expect_match(r.out, "shmkv: cannot open", lang .. ": cannot-open message") + +r = checks.run(D .. " create " .. badf .. " --slots 0") +checks.expect_exit(r, 2, lang .. ": --slots 0 exits 2 with usage") + +-- ------------------------------------------------- CROSS-LANGUAGE INTEROP +-- One store, written by Rust AND Go, read by everyone. This only works if +-- all three binaries agree on every byte of the format. +local x = dir .. "/verify-x.kv" +checks.run(demo.rust .. " create " .. x .. " --slots 6") +checks.run(demo.rust .. " set " .. x .. " zeta z1") +checks.run(demo.rust .. " set " .. x .. " alpha a1") +checks.run(demo.rust .. " set " .. x .. " mid m1") + +r = checks.run(demo.go .. " get " .. x .. " alpha") +checks.expect_exit(r, 0, "interop: go reads rust-created store") +checks.expect_match(r.out, "^a1\n$", "interop: go gets rust-written value") + +r = checks.run(demo.cpp .. " get " .. x .. " zeta") +checks.expect_exit(r, 0, "interop: cpp reads rust-created store") +checks.expect_match(r.out, "^z1\n$", "interop: cpp gets rust-written value") + +r = checks.run(demo.go .. " set " .. x .. " gadd g1") +checks.expect_exit(r, 0, "interop: go writes into rust-created store") +r = checks.run(demo.cpp .. " get " .. x .. " gadd") +checks.expect_match(r.out, "^g1\n$", "interop: cpp sees the go-written key") + +for l, d in pairs(demo) do + r = checks.run(d .. " dump " .. x .. " > " .. dir .. "/verify-dump-" .. l .. ".txt") + checks.expect_exit(r, 0, "interop: " .. l .. " dump exits 0") + checks.expect_match(r.out, "shmkv: 4/6 slots used", "interop: " .. l .. " counts 4/6 used") +end +r = checks.run("cmp -s " .. dir .. "/verify-dump-cpp.txt " .. dir .. "/verify-dump-go.txt") +checks.expect_exit(r, 0, "interop: cpp and go dumps are byte-identical") +r = checks.run("cmp -s " .. dir .. "/verify-dump-cpp.txt " .. dir .. "/verify-dump-rust.txt") +checks.expect_exit(r, 0, "interop: cpp and rust dumps are byte-identical") +r = checks.run("cat " .. dir .. "/verify-dump-rust.txt") +checks.expect_match(r.out, "^alpha=a1\ngadd=g1\nmid=m1\nzeta=z1\n$", + "interop: dump is the four pairs, key-sorted") + +-- Reverse direction: C++ creates and replays the identical operations; Rust +-- reads it back; and the two store FILES must be byte-identical, since the +-- format (zero padding included) is fully deterministic. +local y = dir .. "/verify-y.kv" +checks.run(demo.cpp .. " create " .. y .. " --slots 6") +checks.run(demo.cpp .. " set " .. y .. " zeta z1") +checks.run(demo.cpp .. " set " .. y .. " alpha a1") +checks.run(demo.cpp .. " set " .. y .. " mid m1") +checks.run(demo.cpp .. " set " .. y .. " gadd g1") + +r = checks.run(demo.rust .. " get " .. y .. " mid") +checks.expect_exit(r, 0, "interop: rust reads cpp-created store") +checks.expect_match(r.out, "^m1\n$", "interop: rust gets cpp-written value") + +r = checks.run(demo.rust .. " dump " .. y .. " > " .. dir .. "/verify-dump-y.txt") +checks.expect_exit(r, 0, "interop: rust dump of cpp store exits 0") +r = checks.run("cmp -s " .. dir .. "/verify-dump-y.txt " .. dir .. "/verify-dump-rust.txt") +checks.expect_exit(r, 0, "interop: rust dump of cpp store matches its dump of the rust store") + +r = checks.run("cmp -s " .. x .. " " .. y) +checks.expect_exit(r, 0, "interop: rust-built and cpp-built store files are byte-identical") + +for _, f in ipairs(scratch) do os.remove(f) end +checks.finish() diff --git a/examples/17-allocators-and-gc-runtimes/README.md b/examples/17-allocators-and-gc-runtimes/README.md new file mode 100644 index 0000000..68de019 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/README.md @@ -0,0 +1,126 @@ +# 17-allocators-and-gc-runtimes — allocbench + +Chapter 17 puts the three languages' memory stories side by side with one +allocation-heavy workload: build a string→string index (1000 distinct keys, +overwritten round-robin) out of `N` iterations of short-lived intermediate +strings, then query every key back. + +``` +allocbench [--allocs N] [--variant default|arena] +``` + +- `--allocs N` — iterations of churn (default 200000; must be a positive + integer). +- `--variant default` — every intermediate goes through the language's + general-purpose allocation path (glibc malloc, the Go heap + GC, the Rust + global allocator). +- `--variant arena` — the same logical work, but the short-lived + intermediates are batch-allocated and reclaimed wholesale (or recycled) + instead of being freed one by one. + +## Behavior (identical across C++, Go, and Rust) + +- On success, exactly one stdout line: + `allocbench: variant= allocs= peak_rss=KB ms=` + where `peak_rss` is `getrusage(2)` `ru_maxrss` (the process high-water + mark, `VmHWM`, in KB) and `ms` is the wall time of the build+query phase. +- **Go only** appends ` gc_cycles=`: the delta of + `runtime.MemStats.NumGC` across the workload, read via + `runtime.ReadMemStats`. C++ and Rust have no collector, so the field would + be a lie there — its absence is part of the lesson. +- Bad usage (unknown flag, non-positive `--allocs`, unknown variant) prints + a diagnostic plus `usage: allocbench [--allocs N] [--variant default|arena]` + on stderr and exits 2. Runtime failures print `allocbench: ...` on stderr + and exit 1. +- The query phase is a real check: if any of the distinct keys is missing + from the index or the summed value bytes are zero, the run fails — the + benchmark cannot "pass" by optimizing the work away. + +## The two variants, per language + +| | default | arena | +|---|---|---| +| **C++** | `std::string` intermediates from the global heap, freed one by one when each batch's scratch `std::vector` is destroyed | per-batch `std::pmr::monotonic_buffer_resource` over one reusable 256 KiB slab; intermediates are `std::pmr::string`s held in a `std::pmr::vector`, released wholesale when the resource is destroyed | +| **Go** | fresh strings every iteration (`strconv.Itoa`, `+` concatenation) — all of it garbage for the collector | `sync.Pool` of scratch objects (three `bytes.Buffer`s plus a `strconv.AppendInt` byte slice): churn buffers are recycled, not collected | +| **Rust** | `String`/`format!` intermediates from the global allocator, dropped individually | `bumpalo::Bump` arena: `bumpalo::collections::String`/`Vec` intermediates, reclaimed in O(1) by `bump.reset()` after each 1000-iteration batch | + +The long-lived index itself is an ordinary hash map in all six builds; only +the final key/value pairs are copied into it. That is the arena discipline +in one sentence: bulk-free the churn, copy out what survives. + +Go deliberately is not "arena" in the C++/Rust sense — the language offers +no stable arena API (the `arena` experiment stalled), so the idiomatic +answer to allocation churn is object reuse via `sync.Pool`, which is what +the variant demonstrates. + +## Knobs worth playing with (Go) + +The Go runtime exposes its memory policy as environment variables — no +rebuild required: + +```console +$ ./demo.sh go run +allocbench: variant=default allocs=200000 peak_rss=8196KB ms=52 gc_cycles=9 +$ GOGC=25 ./demo.sh go run # GC 4x as eager +allocbench: variant=default allocs=200000 peak_rss=5908KB ms=73 gc_cycles=33 +$ GOGC=off GOMEMLIMIT=8MiB ./demo.sh go run # pacing off; the soft memory + # limit alone drives collection +allocbench: variant=default allocs=200000 peak_rss=5724KB ms=61 gc_cycles=34 +$ GOGC=off GOMEMLIMIT=64MiB ./demo.sh go run # limit never approached... +allocbench: variant=default allocs=200000 peak_rss=37292KB ms=51 gc_cycles=0 +``` + +`GOMEMLIMIT` (Go 1.19+) is a soft cap on the runtime's total memory: as the +heap approaches it, the collector runs more aggressively to stay under — +and if the limit is never approached (last run above), the collector simply +never fires and the garbage piles up as RSS. Watch `gc_cycles` and +`peak_rss` move in opposite directions as you trade one for the other. +`GOGC` scales how much new garbage may accumulate between cycles (default +100 = the heap may double). + +## Try it + +```console +$ ./demo.sh cpp run # default variant, 200000 allocs +allocbench: variant=default allocs=200000 peak_rss=3908KB ms=21 +$ ./demo.sh cpp run --variant arena +allocbench: variant=arena allocs=200000 peak_rss=4296KB ms=22 +$ ./demo.sh go run --allocs 500000 +allocbench: variant=default allocs=500000 peak_rss=8132KB ms=124 gc_cycles=24 +$ ./demo.sh rust run +allocbench: variant=default allocs=200000 peak_rss=2536KB ms=57 +$ ./demo.sh rust run --variant arena +allocbench: variant=arena allocs=200000 peak_rss=2576KB ms=31 +``` + +(Numbers vary by host and run; only the line shape is stable.) Note what +the C++ pair shows: at this workload size the arena's peak RSS is +*comparable*, not lower — glibc malloc recycles the churn through the same +pages, while the arena's always-touched 256 KiB slab is a fixed cost. The +arena's win here is deterministic batch reclamation (no per-object frees), +not a smaller high-water mark; crank `--allocs` or the value sizes and the +curves separate. + +## Demo contract + +Each language directory has a `demo.sh` with the book-wide interface: + +- `./demo.sh build` — build only +- `./demo.sh run [args]` — run the built binary (builds first if needed) +- With env `TARGET` set, `run` deploys the binary to that lab VM via + `scripts/lab/deploy-to-vm.sh` instead of running locally. + +The top-level `demo.sh` dispatches: `./demo.sh go run --variant arena` execs +`go/demo.sh run --variant arena`. The binary is always named `app`. + +## Verification + +`verify.lua` (run per language with `LSP_LANG` set) asserts behavior, not +just exit codes: bad usage exits 2 with the usage line; both variants emit +the exact single-line report; `variant`/`allocs` echo the request; +`peak_rss`/`ms` (and `gc_cycles` for Go — which must be absent for C++ and +Rust) parse to sane numbers; the bare run defaults to +`variant=default allocs=200000`; and a 7-alloc run still answers its +queries. The default-vs-arena RSS delta is printed to the log but not +asserted — ordering can flake on a loaded host, and the chapter text, not +CI, owns that claim. diff --git a/examples/17-allocators-and-gc-runtimes/cpp/.clang-format b/examples/17-allocators-and-gc-runtimes/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/17-allocators-and-gc-runtimes/cpp/.clang-tidy b/examples/17-allocators-and-gc-runtimes/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/17-allocators-and-gc-runtimes/cpp/CMakeLists.txt b/examples/17-allocators-and-gc-runtimes/cpp/CMakeLists.txt new file mode 100644 index 0000000..3f0a368 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(allocbench CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/17-allocators-and-gc-runtimes/cpp/CMakePresets.json b/examples/17-allocators-and-gc-runtimes/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/17-allocators-and-gc-runtimes/cpp/demo.sh b/examples/17-allocators-and-gc-runtimes/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/17-allocators-and-gc-runtimes/cpp/src/main.cpp b/examples/17-allocators-and-gc-runtimes/cpp/src/main.cpp new file mode 100644 index 0000000..7e8321a --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/cpp/src/main.cpp @@ -0,0 +1,226 @@ +// allocbench — allocation-heavy index build/query (chapter 17: allocators +// and GC runtimes). +// +// allocbench [--allocs N] [--variant default|arena] +// +// Both variants build the same string->string index (1000 distinct keys, +// overwritten round-robin) from N iterations of short-lived intermediate +// strings, then query every distinct key back. What differs is where the +// intermediates live: +// +// default: every key/frag/value is a plain std::string — each a trip to +// the global heap (malloc), each freed individually when its +// batch scratch vector is destroyed. +// arena: per batch of 1000 iterations, a std::pmr::monotonic_buffer_- +// resource carves the intermediates (std::pmr::string in a +// std::pmr::vector) out of one reusable slab. Nothing is freed +// per-object; the whole batch is released at once when the +// resource is destroyed, and the slab is reused for the next +// batch. Only the final key/value pairs are copied out into the +// long-lived index. +// +// Reports: "allocbench: variant= allocs= peak_rss=KB ms=" +// (peak_rss is getrusage(2) ru_maxrss — the VmHWM high-water mark — in KB). + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +using std::expected; +using std::unexpected; + +constexpr long kDefaultAllocs = 200'000; +constexpr long kKeyspace = 1'000; // distinct keys; the rest is churn +constexpr long kBatch = 1'000; // arena reset granularity (iterations) +constexpr int kRepeat = 4; // value = frag repeated kRepeat times +constexpr std::size_t kSlabBytes = 256 * 1024; // reusable arena slab + +using Index = std::unordered_map; + +struct Config { + long allocs = kDefaultAllocs; + bool arena = false; +}; + +[[nodiscard]] std::error_code errno_ec() { + return std::error_code{errno, std::system_category()}; +} + +[[nodiscard]] expected parse_positive(std::string_view s) { + long v = 0; + const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v); + if (ec != std::errc{} || ptr != s.data() + s.size() || v <= 0) { + return unexpected("not a positive integer: " + std::string{s}); + } + return v; +} + +[[nodiscard]] expected parse_args(int argc, char** argv) { + Config cfg; + for (int i = 1; i < argc; ++i) { + const std::string_view arg = argv[i]; + if (arg == "--allocs" && i + 1 < argc) { + const auto v = parse_positive(argv[++i]); + if (!v) { + return unexpected(v.error()); + } + cfg.allocs = *v; + } else if (arg == "--variant" && i + 1 < argc) { + const std::string_view v = argv[++i]; + if (v == "default") { + cfg.arena = false; + } else if (v == "arena") { + cfg.arena = true; + } else { + return unexpected("unknown variant: " + std::string{v}); + } + } else { + return unexpected("unknown argument: " + std::string{arg}); + } + } + return cfg; +} + +// Append the decimal form of v without allocating (unlike std::to_string, +// which would hand the arena variant a heap std::string behind our back). +template +void append_num(Str& out, long v) { + std::array buf; + const auto [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), v); + out.append(buf.data(), static_cast(ptr - buf.data())); +} + +// One iteration of churn: key "key-", frag "value--/", +// value = frag x kRepeat. Works on std::string and std::pmr::string alike. +template +void make_kv(long i, Str& key, Str& frag, Str& value) { + const long idx = i % kKeyspace; + key.append("key-", 4); + append_num(key, idx); + frag.append("value-", 6); + append_num(frag, idx); + frag.push_back('-'); + append_num(frag, i % 97); + frag.push_back('/'); + for (int r = 0; r < kRepeat; ++r) { + value.append(frag.data(), frag.size()); + } +} + +// default variant: plain heap strings, freed one by one at batch end. +[[nodiscard]] Index build_default(long allocs) { + Index index; + index.reserve(kKeyspace); + for (long start = 0; start < allocs; start += kBatch) { + const long end = std::min(start + kBatch, allocs); + std::vector scratch; // keeps the churn alive per batch + scratch.reserve(static_cast(end - start)); + for (long i = start; i < end; ++i) { + std::string key; + std::string frag; + std::string value; + make_kv(i, key, frag, value); + index.insert_or_assign(std::move(key), std::move(value)); + scratch.push_back(std::move(frag)); + } + } // ~scratch: one deallocation per surviving string + return index; +} + +// arena variant: same shape, but the intermediates come from a monotonic +// buffer over one reusable slab and are released wholesale per batch. +[[nodiscard]] Index build_arena(long allocs) { + std::vector slab(kSlabBytes); // reused by every batch + Index index; + index.reserve(kKeyspace); + for (long start = 0; start < allocs; start += kBatch) { + const long end = std::min(start + kBatch, allocs); + std::pmr::monotonic_buffer_resource arena{slab.data(), slab.size()}; + std::pmr::vector scratch{&arena}; + scratch.reserve(static_cast(end - start)); + for (long i = start; i < end; ++i) { + std::pmr::string key{&arena}; + std::pmr::string frag{&arena}; + std::pmr::string value{&arena}; + make_kv(i, key, frag, value); + index.insert_or_assign(std::string{key}, std::string{value}); + scratch.push_back(std::move(frag)); + } + } // ~arena: the whole batch vanishes at once; no per-object frees + return index; +} + +[[nodiscard]] expected query(const Index& index, long allocs) { + const long distinct = std::min(allocs, kKeyspace); + std::uint64_t total = 0; + for (long idx = 0; idx < distinct; ++idx) { + std::string key{"key-"}; + append_num(key, idx); + const auto it = index.find(key); + if (it == index.end()) { + return unexpected("missing key: " + key); + } + total += it->second.size(); + } + if (total == 0) { + return unexpected("query summed zero bytes"); + } + return total; +} + +[[nodiscard]] expected peak_rss_kb() { + rusage ru{}; + if (::getrusage(RUSAGE_SELF, &ru) != 0) { + return unexpected(errno_ec()); + } + return ru.ru_maxrss; // kilobytes on Linux +} + +} // namespace + +int main(int argc, char** argv) { + const auto cfg = parse_args(argc, argv); + if (!cfg) { + std::println(stderr, "allocbench: {}", cfg.error()); + std::println(stderr, "usage: allocbench [--allocs N] [--variant default|arena]"); + return 2; + } + + const auto t0 = std::chrono::steady_clock::now(); + const Index index = cfg->arena ? build_arena(cfg->allocs) : build_default(cfg->allocs); + const auto total = query(index, cfg->allocs); + const auto t1 = std::chrono::steady_clock::now(); + if (!total) { + std::println(stderr, "allocbench: {}", total.error()); + return 1; + } + + const auto rss = peak_rss_kb(); + if (!rss) { + std::println(stderr, "allocbench: getrusage: {}", rss.error().message()); + return 1; + } + + const auto ms = std::chrono::duration_cast(t1 - t0).count(); + std::println("allocbench: variant={} allocs={} peak_rss={}KB ms={}", + cfg->arena ? "arena" : "default", cfg->allocs, *rss, ms); + return 0; +} diff --git a/examples/17-allocators-and-gc-runtimes/demo.sh b/examples/17-allocators-and-gc-runtimes/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/17-allocators-and-gc-runtimes/go/demo.sh b/examples/17-allocators-and-gc-runtimes/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/17-allocators-and-gc-runtimes/go/go.mod b/examples/17-allocators-and-gc-runtimes/go/go.mod new file mode 100644 index 0000000..feaa5ea --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/17-allocators-and-gc-runtimes/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/17-allocators-and-gc-runtimes/go/go.sum b/examples/17-allocators-and-gc-runtimes/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/17-allocators-and-gc-runtimes/go/main.go b/examples/17-allocators-and-gc-runtimes/go/main.go new file mode 100644 index 0000000..d423b59 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/go/main.go @@ -0,0 +1,214 @@ +// allocbench — allocation-heavy index build/query (chapter 17: allocators +// and GC runtimes). +// +// allocbench [--allocs N] [--variant default|arena] +// +// Both variants build the same string->string index (1000 distinct keys, +// overwritten round-robin) from N iterations of short-lived intermediate +// strings, then query every distinct key back. What differs is who pays for +// the intermediates: +// +// - default: every key/frag/value is a freshly allocated string. All of +// that garbage is the GC's problem — watch gc_cycles climb with N. +// - arena: Go has no std::pmr / bumpalo equivalent, so the idiom is +// sync.Pool: each iteration borrows a *scratch (three bytes.Buffers and +// a number-formatting []byte) from the pool, builds the strings in +// place with strconv.AppendInt (no per-digit garbage), copies only the +// final key/value out, and returns the scratch. The churn objects are +// recycled instead of collected. +// +// Reports: "allocbench: variant= allocs= peak_rss=KB ms= gc_cycles=" +// where gc_cycles is the delta of runtime.MemStats.NumGC across the +// build+query phase (the Go-only field — C++ and Rust have no collector), +// and peak_rss comes from unix.Getrusage (ru_maxrss, KB). +// +// The GC's appetite is tunable without touching this code: GOGC scales how +// much garbage may pile up between cycles, and GOMEMLIMIT puts a soft cap +// on the runtime's total memory (see the README). +package main + +import ( + "bytes" + "fmt" + "os" + "runtime" + "strconv" + "sync" + "time" + + "golang.org/x/sys/unix" +) + +const ( + defaultAllocs = 200_000 + keyspace = 1_000 // distinct keys; the rest is churn + repeat = 4 // value = frag repeated `repeat` times +) + +type config struct { + allocs int + arena bool +} + +func usage() { + fmt.Fprintln(os.Stderr, "usage: allocbench [--allocs N] [--variant default|arena]") +} + +func parseArgs(args []string) (config, error) { + cfg := config{allocs: defaultAllocs} + for i := 0; i < len(args); i++ { + switch { + case args[i] == "--allocs" && i+1 < len(args): + i++ + n, err := strconv.Atoi(args[i]) + if err != nil || n <= 0 { + return cfg, fmt.Errorf("not a positive integer: %s", args[i]) + } + cfg.allocs = n + case args[i] == "--variant" && i+1 < len(args): + i++ + switch args[i] { + case "default": + cfg.arena = false + case "arena": + cfg.arena = true + default: + return cfg, fmt.Errorf("unknown variant: %s", args[i]) + } + default: + return cfg, fmt.Errorf("unknown argument: %s", args[i]) + } + } + return cfg, nil +} + +// buildDefault: every intermediate is a fresh allocation; the collector +// sweeps up behind each iteration. +func buildDefault(allocs int) map[string]string { + index := make(map[string]string, keyspace) + for i := 0; i < allocs; i++ { + idx := i % keyspace + key := "key-" + strconv.Itoa(idx) + frag := "value-" + strconv.Itoa(idx) + "-" + strconv.Itoa(i%97) + "/" + value := "" + for r := 0; r < repeat; r++ { + value += frag // deliberate churn: each += is a new string + } + index[key] = value + } + return index +} + +// scratch is the pooled churn object: reusable buffers for key, frag, and +// value plus a []byte for strconv.AppendInt so digits never allocate. +type scratch struct { + key, frag, value bytes.Buffer + num []byte +} + +var scratchPool = sync.Pool{New: func() any { return &scratch{num: make([]byte, 0, 20)} }} + +func (s *scratch) reset() { + s.key.Reset() + s.frag.Reset() + s.value.Reset() +} + +func (s *scratch) writeNum(b *bytes.Buffer, v int) { + s.num = strconv.AppendInt(s.num[:0], int64(v), 10) + b.Write(s.num) +} + +// buildArena: same logical work, but the intermediates live in pooled +// buffers that are recycled instead of collected. Only the final key/value +// strings are copied out into the long-lived index. +func buildArena(allocs int) map[string]string { + index := make(map[string]string, keyspace) + for i := 0; i < allocs; i++ { + s := scratchPool.Get().(*scratch) + s.reset() + idx := i % keyspace + s.key.WriteString("key-") + s.writeNum(&s.key, idx) + s.frag.WriteString("value-") + s.writeNum(&s.frag, idx) + s.frag.WriteByte('-') + s.writeNum(&s.frag, i%97) + s.frag.WriteByte('/') + for r := 0; r < repeat; r++ { + s.value.Write(s.frag.Bytes()) + } + index[s.key.String()] = s.value.String() + scratchPool.Put(s) + } + return index +} + +func query(index map[string]string, allocs int) (uint64, error) { + distinct := min(allocs, keyspace) + var total uint64 + for idx := 0; idx < distinct; idx++ { + key := "key-" + strconv.Itoa(idx) + value, ok := index[key] + if !ok { + return 0, fmt.Errorf("missing key: %s", key) + } + total += uint64(len(value)) + } + if total == 0 { + return 0, fmt.Errorf("query summed zero bytes") + } + return total, nil +} + +func peakRSSKB() (int64, error) { + var ru unix.Rusage + if err := unix.Getrusage(unix.RUSAGE_SELF, &ru); err != nil { + return 0, fmt.Errorf("getrusage: %w", err) + } + return ru.Maxrss, nil // kilobytes on Linux +} + +func run(cfg config) error { + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + + t0 := time.Now() + var index map[string]string + if cfg.arena { + index = buildArena(cfg.allocs) + } else { + index = buildDefault(cfg.allocs) + } + if _, err := query(index, cfg.allocs); err != nil { + return err + } + ms := time.Since(t0).Milliseconds() + + runtime.ReadMemStats(&after) + peak, err := peakRSSKB() + if err != nil { + return err + } + + variant := "default" + if cfg.arena { + variant = "arena" + } + fmt.Printf("allocbench: variant=%s allocs=%d peak_rss=%dKB ms=%d gc_cycles=%d\n", + variant, cfg.allocs, peak, ms, after.NumGC-before.NumGC) + return nil +} + +func main() { + cfg, err := parseArgs(os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "allocbench: %v\n", err) + usage() + os.Exit(2) + } + if err := run(cfg); err != nil { + fmt.Fprintf(os.Stderr, "allocbench: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/17-allocators-and-gc-runtimes/rust/Cargo.lock b/examples/17-allocators-and-gc-runtimes/rust/Cargo.lock new file mode 100644 index 0000000..c8a6c6a --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/rust/Cargo.lock @@ -0,0 +1,30 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocbench" +version = "0.1.0" +dependencies = [ + "anyhow", + "bumpalo", + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" diff --git a/examples/17-allocators-and-gc-runtimes/rust/Cargo.toml b/examples/17-allocators-and-gc-runtimes/rust/Cargo.toml new file mode 100644 index 0000000..42fdc27 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "allocbench" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.104" +bumpalo = { version = "3.20.3", features = ["collections"] } +libc = "0.2" diff --git a/examples/17-allocators-and-gc-runtimes/rust/demo.sh b/examples/17-allocators-and-gc-runtimes/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/17-allocators-and-gc-runtimes/rust/rust-toolchain.toml b/examples/17-allocators-and-gc-runtimes/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/17-allocators-and-gc-runtimes/rust/src/main.rs b/examples/17-allocators-and-gc-runtimes/rust/src/main.rs new file mode 100644 index 0000000..3af6036 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/rust/src/main.rs @@ -0,0 +1,195 @@ +// allocbench — allocation-heavy index build/query (chapter 17: allocators +// and GC runtimes). +// +// allocbench [--allocs N] [--variant default|arena] +// +// Both variants build the same string->string index (1000 distinct keys, +// overwritten round-robin) from N iterations of short-lived intermediate +// strings, then query every distinct key back. What differs is where the +// intermediates live: +// +// default: every key/frag/value is a plain String — each a trip to the +// global allocator, each freed individually when its batch +// scratch Vec is dropped. +// arena: a bumpalo::Bump arena. Per batch of 1000 iterations the +// intermediates are bumpalo::collections::String values carved +// out of the bump by pointer arithmetic; nothing is freed +// per-object. After each batch, `bump.reset()` reclaims the +// whole region in O(1) and the next batch reuses the same +// chunks. Only the final key/value pairs are copied out into +// the long-lived index. +// +// Reports: "allocbench: variant= allocs= peak_rss=KB ms=" +// (peak_rss is getrusage(2) ru_maxrss — the VmHWM high-water mark — in KB). + +use std::collections::HashMap; +use std::fmt::Write as _; +use std::process::ExitCode; +use std::time::Instant; + +use anyhow::{Context, Result, bail}; +use bumpalo::Bump; +use bumpalo::collections::String as BumpString; +use bumpalo::collections::Vec as BumpVec; + +const DEFAULT_ALLOCS: usize = 200_000; +const KEYSPACE: usize = 1_000; // distinct keys; the rest is churn +const BATCH: usize = 1_000; // arena reset granularity (iterations) +const REPEAT: usize = 4; // value = frag repeated REPEAT times +const SLAB_BYTES: usize = 512 * 1024; // initial bump capacity + +struct Config { + allocs: usize, + arena: bool, +} + +fn usage() { + eprintln!("usage: allocbench [--allocs N] [--variant default|arena]"); +} + +fn parse_args() -> Result { + let args: Vec = std::env::args().skip(1).collect(); + let mut cfg = Config { + allocs: DEFAULT_ALLOCS, + arena: false, + }; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--allocs" if i + 1 < args.len() => { + i += 1; + cfg.allocs = match args[i].parse::() { + Ok(n) if n > 0 => n, + _ => return Err(format!("not a positive integer: {}", args[i])), + }; + } + "--variant" if i + 1 < args.len() => { + i += 1; + match args[i].as_str() { + "default" => cfg.arena = false, + "arena" => cfg.arena = true, + other => return Err(format!("unknown variant: {other}")), + } + } + other => return Err(format!("unknown argument: {other}")), + } + i += 1; + } + Ok(cfg) +} + +/// default variant: plain heap strings, freed one by one at batch end. +fn build_default(allocs: usize) -> HashMap { + let mut index = HashMap::with_capacity(KEYSPACE); + let mut start = 0; + while start < allocs { + let end = (start + BATCH).min(allocs); + let mut scratch: Vec = Vec::with_capacity(end - start); + for i in start..end { + let idx = i % KEYSPACE; + let key = format!("key-{idx}"); + let frag = format!("value-{idx}-{}/", i % 97); + let mut value = String::new(); + for _ in 0..REPEAT { + value.push_str(&frag); + } + index.insert(key, value); + scratch.push(frag); // keeps the churn alive per batch + } + start = end; + } // drop(scratch): one deallocation per surviving string + index +} + +/// arena variant: same shape, but the intermediates are bump-allocated and +/// the whole batch is reclaimed at once by `bump.reset()`. +fn build_arena(allocs: usize) -> HashMap { + let mut bump = Bump::with_capacity(SLAB_BYTES); + let mut index = HashMap::with_capacity(KEYSPACE); + let mut start = 0; + while start < allocs { + let end = (start + BATCH).min(allocs); + { + let mut scratch: BumpVec = BumpVec::with_capacity_in(end - start, &bump); + for i in start..end { + let idx = i % KEYSPACE; + let mut key = BumpString::new_in(&bump); + write!(key, "key-{idx}").expect("bump string write cannot fail"); + let mut frag = BumpString::new_in(&bump); + write!(frag, "value-{idx}-{}/", i % 97).expect("bump string write cannot fail"); + let mut value = BumpString::new_in(&bump); + for _ in 0..REPEAT { + value.push_str(frag.as_str()); + } + index.insert(key.as_str().to_owned(), value.as_str().to_owned()); + scratch.push(frag); // keeps the churn alive per batch + } + } // borrows on the bump end here... + bump.reset(); // ...so the whole batch vanishes in O(1) + start = end; + } + index +} + +fn query(index: &HashMap, allocs: usize) -> Result { + let distinct = allocs.min(KEYSPACE); + let mut total: u64 = 0; + for idx in 0..distinct { + let key = format!("key-{idx}"); + let value = index + .get(&key) + .with_context(|| format!("missing key: {key}"))?; + total += value.len() as u64; + } + if total == 0 { + bail!("query summed zero bytes"); + } + Ok(total) +} + +fn peak_rss_kb() -> Result { + let mut ru = std::mem::MaybeUninit::::zeroed(); + // SAFETY: RUSAGE_SELF with a properly sized, writable rusage out-param. + let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, ru.as_mut_ptr()) }; + if rc != 0 { + return Err(std::io::Error::last_os_error()).context("getrusage"); + } + // SAFETY: getrusage returned 0, so the struct is fully initialized. + let ru = unsafe { ru.assume_init() }; + Ok(ru.ru_maxrss) // kilobytes on Linux +} + +fn run(cfg: &Config) -> Result<()> { + let t0 = Instant::now(); + let index = if cfg.arena { + build_arena(cfg.allocs) + } else { + build_default(cfg.allocs) + }; + query(&index, cfg.allocs)?; + let ms = t0.elapsed().as_millis(); + + let peak = peak_rss_kb()?; + let variant = if cfg.arena { "arena" } else { "default" }; + println!( + "allocbench: variant={variant} allocs={} peak_rss={peak}KB ms={ms}", + cfg.allocs + ); + Ok(()) +} + +fn main() -> ExitCode { + let cfg = match parse_args() { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("allocbench: {err}"); + usage(); + return ExitCode::from(2); + } + }; + if let Err(err) = run(&cfg) { + eprintln!("allocbench: {err:#}"); + return ExitCode::from(1); + } + ExitCode::SUCCESS +} diff --git a/examples/17-allocators-and-gc-runtimes/verify.lua b/examples/17-allocators-and-gc-runtimes/verify.lua new file mode 100644 index 0000000..d5f3652 --- /dev/null +++ b/examples/17-allocators-and-gc-runtimes/verify.lua @@ -0,0 +1,105 @@ +-- Verify allocbench: both variants must produce the exact report line with +-- numbers that parse to sane values, bad usage must exit 2, and the default +-- --allocs must be 200000. Go alone must additionally report gc_cycles= +-- (its collector is the chapter's point; C++/Rust must NOT print it). +-- Timing/RSS ordering between variants is measured and printed for the log +-- but deliberately not asserted — it can flake on a loaded host. +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and EXAMPLE_DIR set. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local function demo(args) + return checks.run("./demo.sh " .. lang .. " run " .. args) +end + +-- Bad usage: unknown flag / bad N / bad variant -> usage on stderr, exit 2. +local r = demo("--bogus") +checks.expect_exit(r, 2, lang .. ": unknown flag exits 2") +checks.expect_match(r.out, "usage: allocbench %[%-%-allocs N%] %[%-%-variant default|arena%]", + lang .. ": unknown flag prints usage") + +r = demo("--allocs zero") +checks.expect_exit(r, 2, lang .. ": non-numeric --allocs exits 2") +checks.expect_match(r.out, "not a positive integer", lang .. ": non-numeric --allocs diagnostic") + +r = demo("--variant marble") +checks.expect_exit(r, 2, lang .. ": unknown variant exits 2") +checks.expect_match(r.out, "unknown variant: marble", lang .. ": unknown variant diagnostic") + +-- The report line, parsed strictly. Go carries the extra gc_cycles field; +-- C++ and Rust must not (no collector to count). +local base = "allocbench: variant=(%a+) allocs=(%d+) peak_rss=(%d+)KB ms=(%d+)" +local line_pat +if lang == "go" then + line_pat = "^" .. base .. " gc_cycles=(%d+)\n$" +else + line_pat = "^" .. base .. "\n$" +end + +-- Run one variant and return its parsed peak_rss (KB) after asserting the +-- full line shape and that every number parses to a sane value. +local function check_variant(variant, allocs) + local args = "--allocs " .. allocs + if variant ~= "default" then + args = args .. " --variant " .. variant + end + r = demo(args) + checks.expect_exit(r, 0, lang .. ": " .. variant .. " run exits 0") + checks.expect_match(r.out, line_pat, lang .. ": " .. variant .. " report is the exact single line") + local v, n, rss, ms, gc = r.out:match(base .. (lang == "go" and " gc_cycles=(%d+)" or "")) + if not v then + checks.expect_match(r.out, base, lang .. ": " .. variant .. " line parses") -- records the FAIL + return nil + end + checks.expect_match(v, "^" .. variant .. "$", lang .. ": reports variant=" .. variant) + checks.expect_match(n, "^" .. allocs .. "$", lang .. ": echoes allocs=" .. allocs) + local rss_n, ms_n = tonumber(rss), tonumber(ms) + if rss_n and rss_n >= 1000 then + checks.expect_match("ok", "ok", lang .. ": " .. variant .. " peak_rss parses and is >= 1000 KB (" .. rss .. ")") + else + checks.expect_match(rss or "", "impossible-peak-rss", lang .. ": " .. variant .. " peak_rss sane") + end + if ms_n and ms_n >= 0 and ms_n < 300000 then + checks.expect_match("ok", "ok", lang .. ": " .. variant .. " ms parses (" .. ms .. ")") + else + checks.expect_match(ms or "", "impossible-ms", lang .. ": " .. variant .. " ms sane") + end + if lang == "go" then + if tonumber(gc) then + checks.expect_match("ok", "ok", lang .. ": " .. variant .. " gc_cycles parses (" .. gc .. ")") + else + checks.expect_match(gc or "", "impossible-gc", lang .. ": " .. variant .. " gc_cycles sane") + end + end + return rss_n +end + +local rss_default = check_variant("default", 200000) +local rss_arena = check_variant("arena", 200000) +if rss_default and rss_arena then + -- Informational only: ordering is workload truth, not an assertion. + print(string.format("info: %s peak_rss default=%dKB arena=%dKB (delta %+dKB)", + lang, rss_default, rss_arena, rss_arena - rss_default)) +end + +-- Defaults: bare run must use allocs=200000 and variant=default. +r = demo("") +checks.expect_exit(r, 0, lang .. ": bare run exits 0") +checks.expect_match(r.out, "^allocbench: variant=default allocs=200000 peak_rss=%d+KB ms=%d+", + lang .. ": bare run defaults to variant=default allocs=200000") + +-- A tiny run still works (fewer allocs than the 1000-key keyspace). +r = demo("--allocs 7 --variant arena") +checks.expect_exit(r, 0, lang .. ": --allocs 7 arena run exits 0") +checks.expect_match(r.out, "^allocbench: variant=arena allocs=7 peak_rss=%d+KB ms=%d+", + lang .. ": --allocs 7 arena report line") + +checks.finish() diff --git a/examples/18-pipes-fifos-splice/README.md b/examples/18-pipes-fifos-splice/README.md new file mode 100644 index 0000000..185c2d3 --- /dev/null +++ b/examples/18-pipes-fifos-splice/README.md @@ -0,0 +1,128 @@ +# 18 — Pipes, FIFOs, splice (`pmon` v4) + +`pmon` is the process supervisor that grows across the book: v0 `run`, +v1 the sigchld supervise engine, v2 the pidfd engine (chapter 13), v3 the +privilege-drop chapter. v4 keeps both supervise engines exactly as they were +and turns the supervisor into a log pipeline built from kernel pipe +machinery: + +- **`supervise` captures the child** — the child's stdout and stderr each + flow through their own `pipe(2)` back into the supervisor, which relays + them line-buffered into a log file as `[out] ...` / `[err] ...` lines. + The two pipe read ends simply join the fd set each engine already + multiplexes; nothing about the v1/v2 restart/timeout/stop behavior + changes. +- **`tail` republishes the log** — it creates a FIFO (`mkfifo(3)`), follows + the log file, and relays appended bytes into whatever reader opens the + FIFO. C++ and Rust move the bytes kernel-side with `splice(2)` + (file → pipe, no userspace copy; automatic `read`/`write` fallback on + filesystems without splice support). A reader that disappears is not + fatal: SIGPIPE is ignored, the resulting `EPIPE` prints + `pmon: tail reader detached`, and the relay waits for the next reader — + losing nothing, because the log offset only advances for bytes that + actually reached the pipe. + +| Language | Architecture | +|---|---| +| **C++23** | one `poll(2)` loop per child over four fds — the two capture pipes, the signalfd, and (pidfd engine) the pidfd from raw `SYS_pidfd_open`; `waitid(P_PIDFD)` reaps race-free. RAII `Fd`/`SpawnAttrs`/`FileActions` owners, `std::expected` error paths, `posix_spawnp` wiring the pipes via `adddup2` and resetting the child's signal mask. `tail` uses the `splice(2)` fast path with an offset-preserving `read`/`write` fallback. | +| **Rust (edition 2024)** | the same poll set via `rustix::event::poll` + a nix `SignalFd`, `pidfd_open` returning an `OwnedFd`, `waitid(WaitId::PidFd)`; `rustix::pipe::splice` for the tail fast path; `anyhow` + `?` throughout, every fd `OwnedFd`-backed. | +| **Go 1.26** | the idiomatic inversion: one goroutine per capture pipe feeds a channel of prefixed lines into the same `select` that already multiplexes the pidfd poller (or `signal.Notify(SIGCHLD)`), the stop signals, and the deadline. `tail` relays with `io.Copy` per reader session, rewinding the log to `start+written` on `EPIPE`; `mkfifo`/nonblocking FIFO open via `golang.org/x/sys/unix`. | + +All three expose identical observable behavior, so one `verify.lua` covers +them. + +## CLI + +``` +usage: pmon + run -- CMD [ARGS...] spawn CMD, wait, mirror its exit + supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T] + [--log FILE] -- CMD [ARGS...] restart CMD on abnormal exit; + capture stdout/stderr into FILE + (defaults: pidfd, N=3, T=10000, + FILE=pmon.log) + tail --log FILE --fifo PATH relay appended log lines into a + FIFO created at PATH +``` + +The v2 output contract is unchanged: spawn lines +(`pmon: engine=pidfd child= pidfd=` / `pmon: engine=sigchld +child=` / `pmon: run child=`) on stderr; observations +(`pmon: child= exited status=`, `pmon: child= killed +signal=`, `pmon: restart /`, `pmon: giving up after restarts`, +`pmon: exiting (signal|timeout)`) on stdout. `run` mirrors the child's exit +(fatal signal → `128+signo`); clean `status=0` ends supervision with exit 0; +an exhausted restart budget exits 1; both stop paths exit 0. + +New in v4: + +- `supervise` appends each complete child line to the log immediately as + `[out] line` / `[err] line`; a trailing partial line is flushed with a + newline when the stream closes, and the log is fully drained before the + exit observation is printed. +- `tail --log FILE --fifo PATH` — creates PATH as a FIFO (an existing FIFO + is reused; anything else is an error), prints + `pmon: tail ready (fifo PATH)` to stderr, then relays FILE from the + beginning into each successive FIFO reader, polling for appended bytes. + Reader detach (`EPIPE`) prints `pmon: tail reader detached` (stdout) and + the relay keeps running; undelivered bytes are re-sent to the next + reader. SIGINT/SIGTERM → `pmon: exiting (signal)`, exit 0. +- Exit codes everywhere: `0` success, `1` runtime error, `2` usage error. + +## Demo contract + +Each language directory has the standard `demo.sh`: + +- `./demo.sh build` — build only +- `./demo.sh run [args]` — run the built binary (`app`); the local path + `exec`s the binary so a backgrounded `run tail ...` can be signalled + directly +- `./demo.sh` — build, then a one-shot self-demo (`run -- sh -c ...`) +- With env `TARGET` set, `run` deploys to that lab VM via + `scripts/lab/deploy-to-vm.sh` + +The top-level `./demo.sh [cpp|go|rust|all|build]` dispatches per language. + +## Try it + +```sh +./demo.sh cpp build +cd "$(mktemp -d)" +APP=/path/to/examples/18-pipes-fifos-splice/cpp/build/release/app + +# two-pipe capture with a restart (pidfd engine, unchanged from v2) +$APP supervise --max-restarts 2 --log pmon.log -- sh -c 'echo hello; echo oops >&2; exit 1' +cat pmon.log # [out] hello / [err] oops, repeated per attempt + +# republish the log through a FIFO +$APP tail --log pmon.log --fifo pmon.fifo & +cat pmon.fifo & # receives the backlog +echo '[out] more' >> pmon.log # ...and anything appended +kill %2 # kill the cat: tail prints "pmon: tail reader detached" +cat pmon.fifo # reattach: nothing was lost +``` + +Watch the fast path: `strace -e trace=splice` on the C++ or Rust tail shows +`splice(3, NULL, 4, NULL, 65536, 0) = ` moving log bytes straight into +the pipe, and `= -1 EPIPE` at the moment the reader dies. + +## Verification + +`verify.lua` (run per language with `LSP_LANG` set) asserts observable +behavior: usage errors exit 2; `run` still mirrors exit statuses (3 stays +3, SIGKILL becomes 137 with `killed signal=9`); the pidfd engine supervises +a fail-once-then-succeed child with exactly two +`engine=pidfd child=... pidfd=...` spawn lines, `restart 1/3`, exit 0, and +a log holding two `[out] run-out` and two `[err] run-err` lines; the +sigchld engine drives the same capture with byte-identical restart lines +and no `pidfd=` field anywhere, exits 1 on an exhausted budget; the v2 +timeout stop path still TERMs the child (`killed signal=15`, +`exiting (timeout)`, exit 0) with the pipes attached. The tail scenario +drives the full pipeline with real processes: the FIFO appears on disk, a +`cat` reader receives the backlog and then lines written by a second live +`supervise` run (supervise → log → tail → FIFO → cat), killing the cat +yields exactly one `pmon: tail reader detached` while `kill -0` proves the +relay survived, a reattached `cat` receives both the line appended during +the gap and later appends (no loss), and SIGTERM ends tail with +`pmon: exiting (signal)`, exit 0. Every phase transition is polled with a +bounded `grep` loop, never a blind sleep. diff --git a/examples/18-pipes-fifos-splice/cpp/.clang-format b/examples/18-pipes-fifos-splice/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/18-pipes-fifos-splice/cpp/.clang-tidy b/examples/18-pipes-fifos-splice/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/18-pipes-fifos-splice/cpp/CMakeLists.txt b/examples/18-pipes-fifos-splice/cpp/CMakeLists.txt new file mode 100644 index 0000000..66518bd --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon_v4 CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/18-pipes-fifos-splice/cpp/CMakePresets.json b/examples/18-pipes-fifos-splice/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/18-pipes-fifos-splice/cpp/demo.sh b/examples/18-pipes-fifos-splice/cpp/demo.sh new file mode 100755 index 0000000..31c7632 --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + exec "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run run -- /bin/sh -c "echo pmon self-demo" + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/18-pipes-fifos-splice/cpp/src/main.cpp b/examples/18-pipes-fifos-splice/cpp/src/main.cpp new file mode 100644 index 0000000..624e85f --- /dev/null +++ b/examples/18-pipes-fifos-splice/cpp/src/main.cpp @@ -0,0 +1,805 @@ +// pmon v4 — the process supervisor becomes a log pipeline. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child via SIGCHLD +// delivered as readable data through a signalfd. +// v2 (`supervise --engine pidfd`, the DEFAULT) supervises through +// pidfd_open(2)/poll(2)/waitid(P_PIDFD) — no pid-reuse race. +// v4 (this chapter) keeps both engines and adds pipes: +// * supervise captures the child's stdout AND stderr through two +// pipe(2)s into a log file as "[out] ..."/"[err] ..." lines — the two +// pipe read ends simply join the fd set each engine already polls; +// * `tail --log F --fifo PATH` creates a FIFO and relays log bytes into +// whatever reader attaches — splice(2) fast path (file -> pipe, +// kernel-side), read/write fallback; SIGPIPE ignored, EPIPE reported +// as "pmon: tail reader detached" and survived, nothing lost. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace { + +using std::chrono::milliseconds; +using std::chrono::steady_clock; + +constexpr std::size_t kChunk = 64 * 1024; +constexpr int kPollMs = 50; + +[[nodiscard]] std::error_code last_error() { + return {errno, std::system_category()}; +} + +// --------------------------------------------------------------------------- +// RAII owners — every fd and every spawn attribute has exactly one. +// --------------------------------------------------------------------------- + +class Fd { +public: + Fd() = default; + explicit Fd(int fd) : fd_{fd} {} + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& other) noexcept : fd_{std::exchange(other.fd_, -1)} {} + Fd& operator=(Fd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = std::exchange(other.fd_, -1); + } + return *this; + } + ~Fd() { reset(); } + + void reset() noexcept { + if (fd_ >= 0) { + ::close(fd_); + } + fd_ = -1; + } + [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; } + [[nodiscard]] int get() const noexcept { return fd_; } + +private: + int fd_ = -1; +}; + +[[nodiscard]] std::expected checked(int fd) { + if (fd < 0) { + return std::unexpected(last_error()); + } + return Fd{fd}; +} + +class SpawnAttrs { +public: + SpawnAttrs() { ::posix_spawnattr_init(&attr_); } + SpawnAttrs(const SpawnAttrs&) = delete; + SpawnAttrs& operator=(const SpawnAttrs&) = delete; + ~SpawnAttrs() { ::posix_spawnattr_destroy(&attr_); } + [[nodiscard]] posix_spawnattr_t* get() noexcept { return &attr_; } + +private: + posix_spawnattr_t attr_{}; +}; + +class FileActions { +public: + FileActions() { ::posix_spawn_file_actions_init(&fa_); } + FileActions(const FileActions&) = delete; + FileActions& operator=(const FileActions&) = delete; + ~FileActions() { ::posix_spawn_file_actions_destroy(&fa_); } + [[nodiscard]] posix_spawn_file_actions_t* get() noexcept { return &fa_; } + +private: + posix_spawn_file_actions_t fa_{}; +}; + +// --------------------------------------------------------------------------- +// Spawning and status plumbing shared by every subcommand. +// --------------------------------------------------------------------------- + +// The supervisor blocks signals for its signalfd; the child must NOT inherit +// that mask (a supervised `sleep` with SIGTERM blocked would never stop), so +// posix_spawn resets the child's mask to empty. When out/err are valid fds +// they become the child's stdout/stderr (the v4 capture pipes). +[[nodiscard]] std::expected +spawn_child(const std::vector& cmd, const Fd& out, const Fd& err) { + std::vector argv; + argv.reserve(cmd.size() + 1); + for (const auto& s : cmd) { + argv.push_back(const_cast(s.c_str())); + } + argv.push_back(nullptr); + + SpawnAttrs attrs; + sigset_t empty; + sigemptyset(&empty); + ::posix_spawnattr_setsigmask(attrs.get(), &empty); + ::posix_spawnattr_setflags(attrs.get(), POSIX_SPAWN_SETSIGMASK); + + FileActions fa; + if (out.valid()) { + ::posix_spawn_file_actions_adddup2(fa.get(), out.get(), STDOUT_FILENO); + } + if (err.valid()) { + ::posix_spawn_file_actions_adddup2(fa.get(), err.get(), STDERR_FILENO); + } + + pid_t pid{}; + const int rc = ::posix_spawnp(&pid, argv[0], fa.get(), attrs.get(), argv.data(), environ); + if (rc != 0) { + return std::unexpected(std::error_code{rc, std::system_category()}); + } + return pid; +} + +struct ChildStatus { + bool exited; // true: value is the exit status; false: value is the signal + int value; +}; + +[[nodiscard]] ChildStatus status_from(const siginfo_t& si) { + if (si.si_code == CLD_EXITED) { + return {.exited = true, .value = si.si_status}; + } + return {.exited = false, .value = si.si_status}; // CLD_KILLED / CLD_DUMPED +} + +// Prints the exit-observation line and returns the mirrored exit code. +int report_exit(pid_t pid, ChildStatus st) { + if (st.exited) { + std::println("pmon: child={} exited status={}", pid, st.value); + } else { + std::println("pmon: child={} killed signal={}", pid, st.value); + } + std::fflush(stdout); + return st.exited ? st.value : 128 + st.value; +} + +// Block `sigs` process-wide and return a signalfd that delivers them as data. +[[nodiscard]] std::expected +make_signalfd(std::initializer_list sigs) { + sigset_t mask; + sigemptyset(&mask); + for (const int s : sigs) { + sigaddset(&mask, s); + } + if (::sigprocmask(SIG_BLOCK, &mask, nullptr) != 0) { + return std::unexpected(last_error()); + } + return checked(::signalfd(-1, &mask, SFD_CLOEXEC)); +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror (stdio inherited, no capture). +// --------------------------------------------------------------------------- + +int cmd_run(const std::vector& cmd) { + const auto pid = spawn_child(cmd, Fd{}, Fd{}); + if (!pid) { + std::println(stderr, "pmon: spawn: {}: {}", cmd[0], pid.error().message()); + return 1; + } + std::println(stderr, "pmon: run child={}", *pid); + siginfo_t si{}; + while (::waitid(P_PID, static_cast(*pid), &si, WEXITED) != 0) { + if (errno != EINTR) { + std::println(stderr, "pmon: waitid: {}", last_error().message()); + return 1; + } + } + return report_exit(*pid, status_from(si)); +} + +// --------------------------------------------------------------------------- +// v4 capture: two pipes, line-buffered into the log with [out]/[err] tags. +// --------------------------------------------------------------------------- + +// Accumulates one stream's bytes and appends complete lines to the log fd as +// "[pfx] line\n". Line-buffered: each completed line is written immediately. +class LineRelay { +public: + LineRelay(std::string_view pfx, const Fd& log) : pfx_{pfx}, log_{log} {} + + [[nodiscard]] std::expected feed(std::string_view chunk) { + buf_.append(chunk); + std::size_t start = 0; + for (std::size_t nl = buf_.find('\n', start); nl != std::string::npos; + nl = buf_.find('\n', start)) { + if (auto w = emit(std::string_view{buf_}.substr(start, nl - start)); !w) { + return w; + } + start = nl + 1; + } + buf_.erase(0, start); + if (buf_.size() > kChunk) { // bound the partial-line buffer + auto w = emit(buf_); + buf_.clear(); + return w; + } + return {}; + } + + // Stream closed: a trailing partial line still gets logged, with '\n'. + [[nodiscard]] std::expected flush() { + if (buf_.empty()) { + return {}; + } + auto w = emit(buf_); + buf_.clear(); + return w; + } + +private: + [[nodiscard]] std::expected emit(std::string_view line) { + const std::string rec = std::format("[{}] {}\n", pfx_, line); + std::size_t off = 0; + while (off < rec.size()) { + const ssize_t n = ::write(log_.get(), rec.data() + off, rec.size() - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(last_error()); + } + off += static_cast(n); + } + return {}; + } + + std::string pfx_; + const Fd& log_; + std::string buf_; +}; + +// One child's capture pipes and their relays. +struct Capture { + Fd out_r; + Fd err_r; + LineRelay out_relay; + LineRelay err_relay; + bool out_open = true; + bool err_open = true; + + explicit Capture(const Fd& log) + : out_relay{"out", log}, err_relay{"err", log} {} + + // One readiness-driven read on a pipe; EOF closes it and flushes. + [[nodiscard]] std::expected + drain_once(Fd& fd, LineRelay& relay, bool& open) { + std::array buf; + const ssize_t n = ::read(fd.get(), buf.data(), buf.size()); + if (n < 0) { + if (errno == EINTR || errno == EAGAIN) { + return {}; + } + return std::unexpected(last_error()); + } + if (n == 0) { + open = false; + fd.reset(); + return relay.flush(); + } + return relay.feed(std::string_view{buf.data(), static_cast(n)}); + } + + // The child is reaped: blocking-read both pipes to EOF so every logged + // byte lands before the exit is reported. + [[nodiscard]] std::expected drain_to_eof() { + while (out_open) { + if (auto r = drain_once(out_r, out_relay, out_open); !r) { + return r; + } + } + while (err_open) { + if (auto r = drain_once(err_r, err_relay, err_open); !r) { + return r; + } + } + return {}; + } +}; + +// --------------------------------------------------------------------------- +// v1/v2 engines with the v4 pipes joined into the same poll set. +// --------------------------------------------------------------------------- + +enum class Engine : std::uint8_t { pidfd, sigchld }; +enum class Stop : std::uint8_t { signal, timeout }; +using Outcome = std::variant; + +[[nodiscard]] int remaining_ms(steady_clock::time_point deadline) { + const auto left = + std::chrono::duration_cast(deadline - steady_clock::now()).count(); + return left < 0 ? 0 : static_cast(left); +} + +// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +[[nodiscard]] std::expected reap_pidfd(const Fd& pidfd) { + siginfo_t si{}; + while (::waitid(P_PIDFD, static_cast(pidfd.get()), &si, WEXITED) != 0) { + if (errno != EINTR) { + return std::unexpected(last_error()); + } + } + return status_from(si); +} + +// One poll loop for both engines: the capture pipes, the signalfd, and (pidfd +// engine only) the pidfd. A closed pipe leaves the set as fd=-1, which +// poll(2) ignores. +[[nodiscard]] std::expected +wait_child(Engine engine, pid_t pid, const Fd& pidfd, const Fd& sigfd, + steady_clock::time_point deadline, Capture& cap) { + for (;;) { + std::array fds{{ + {.fd = sigfd.get(), .events = POLLIN, .revents = 0}, + {.fd = cap.out_open ? cap.out_r.get() : -1, .events = POLLIN, .revents = 0}, + {.fd = cap.err_open ? cap.err_r.get() : -1, .events = POLLIN, .revents = 0}, + {.fd = engine == Engine::pidfd ? pidfd.get() : -1, + .events = POLLIN, + .revents = 0}, + }}; + const int n = ::poll(fds.data(), fds.size(), remaining_ms(deadline)); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(last_error()); + } + if (n == 0) { + return Stop::timeout; + } + if (fds[1].revents != 0) { + if (auto r = cap.drain_once(cap.out_r, cap.out_relay, cap.out_open); !r) { + return std::unexpected(r.error()); + } + } + if (fds[2].revents != 0) { + if (auto r = cap.drain_once(cap.err_r, cap.err_relay, cap.err_open); !r) { + return std::unexpected(r.error()); + } + } + if ((fds[0].revents & POLLIN) != 0) { + signalfd_siginfo info{}; + std::ignore = ::read(sigfd.get(), &info, sizeof info); + if (info.ssi_signo != SIGCHLD) { + return Stop::signal; + } + // sigchld engine: WNOHANG-reap; coalesced/stale SIGCHLD is benign. + siginfo_t si{}; + si.si_pid = 0; + if (::waitid(P_PID, static_cast(pid), &si, WEXITED | WNOHANG) != 0) { + return std::unexpected(last_error()); + } + if (si.si_pid == pid) { + return Outcome{status_from(si)}; + } + } + if (engine == Engine::pidfd && fds[3].revents != 0) { + return reap_pidfd(pidfd).transform([](ChildStatus st) { return Outcome{st}; }); + } + } +} + +// Ask the child to terminate and reap it (stop paths). +[[nodiscard]] std::expected +term_and_reap(Engine engine, pid_t pid, const Fd& pidfd) { + if (engine == Engine::pidfd) { + std::ignore = ::syscall(SYS_pidfd_send_signal, pidfd.get(), SIGTERM, nullptr, 0); + return reap_pidfd(pidfd); + } + std::ignore = ::kill(pid, SIGTERM); + siginfo_t si{}; + while (::waitid(P_PID, static_cast(pid), &si, WEXITED) != 0) { + if (errno != EINTR) { + return std::unexpected(last_error()); + } + } + return status_from(si); +} + +int cmd_supervise(Engine engine, int max_restarts, int timeout_ms, + const std::string& log_path, const std::vector& cmd) { + auto log = checked(::open(log_path.c_str(), + O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644)); + if (!log) { + std::println(stderr, "pmon: open {}: {}", log_path, log.error().message()); + return 1; + } + auto sigfd = engine == Engine::pidfd + ? make_signalfd({SIGINT, SIGTERM}) + : make_signalfd({SIGCHLD, SIGINT, SIGTERM}); + if (!sigfd) { + std::println(stderr, "pmon: signalfd: {}", sigfd.error().message()); + return 1; + } + const auto deadline = steady_clock::now() + milliseconds{timeout_ms}; + int restarts = 0; + for (;;) { + Capture cap{*log}; + std::array outp{}, errp{}; + if (::pipe2(outp.data(), O_CLOEXEC) != 0 || ::pipe2(errp.data(), O_CLOEXEC) != 0) { + std::println(stderr, "pmon: pipe2: {}", last_error().message()); + return 1; + } + cap.out_r = Fd{outp[0]}; + Fd out_w{outp[1]}; + cap.err_r = Fd{errp[0]}; + Fd err_w{errp[1]}; + + const auto pid = spawn_child(cmd, out_w, err_w); + if (!pid) { + std::println(stderr, "pmon: spawn: {}: {}", cmd[0], pid.error().message()); + return 1; + } + out_w.reset(); // parent's copies of the write ends must close, + err_w.reset(); // or EOF never arrives + + Fd pidfd; + if (engine == Engine::pidfd) { + auto pf = checked(static_cast(::syscall(SYS_pidfd_open, *pid, 0))); + if (!pf) { + std::println(stderr, "pmon: pidfd_open: {}", pf.error().message()); + return 1; + } + pidfd = std::move(*pf); + std::println(stderr, "pmon: engine=pidfd child={} pidfd={}", *pid, pidfd.get()); + } else { + std::println(stderr, "pmon: engine=sigchld child={}", *pid); + } + + auto outcome = wait_child(engine, *pid, pidfd, *sigfd, deadline, cap); + if (!outcome) { + std::println(stderr, "pmon: wait: {}", outcome.error().message()); + return 1; + } + + if (const auto* stop = std::get_if(&*outcome)) { + const auto st = term_and_reap(engine, *pid, pidfd); + if (!st) { + std::println(stderr, "pmon: waitid: {}", st.error().message()); + return 1; + } + if (auto r = cap.drain_to_eof(); !r) { + std::println(stderr, "pmon: relay: {}", r.error().message()); + return 1; + } + std::ignore = report_exit(*pid, *st); + std::println("pmon: exiting ({})", *stop == Stop::timeout ? "timeout" : "signal"); + std::fflush(stdout); + return 0; + } + + // Reaped: finish the log before reporting, then apply the policy. + if (auto r = cap.drain_to_eof(); !r) { + std::println(stderr, "pmon: relay: {}", r.error().message()); + return 1; + } + const auto st = std::get(*outcome); + std::ignore = report_exit(*pid, st); + if (st.exited && st.value == 0) { + return 0; + } + if (restarts >= max_restarts) { + std::println("pmon: giving up after {} restarts", max_restarts); + std::fflush(stdout); + return 1; + } + ++restarts; + std::println("pmon: restart {}/{}", restarts, max_restarts); + std::fflush(stdout); + } +} + +// --------------------------------------------------------------------------- +// v4: tail — follow the log, relay into a FIFO, survive reader churn. +// --------------------------------------------------------------------------- + +volatile sig_atomic_t g_stop = 0; + +extern "C" void on_stop(int) { + g_stop = 1; +} + +void sleep_poll() { + const timespec ts{.tv_sec = 0, .tv_nsec = kPollMs * 1'000'000}; + ::nanosleep(&ts, nullptr); // EINTR just returns early; callers recheck g_stop +} + +// Open the FIFO for writing without blocking forever: O_NONBLOCK turns +// "no reader yet" into ENXIO, which we poll. Once a reader is attached the +// fd goes back to blocking mode for the relay. +[[nodiscard]] std::expected open_writer(const std::string& fifo) { + while (g_stop == 0) { + auto fd = checked(::open(fifo.c_str(), O_WRONLY | O_NONBLOCK | O_CLOEXEC)); + if (fd) { + const int fl = ::fcntl(fd->get(), F_GETFL); + if (fl < 0 || ::fcntl(fd->get(), F_SETFL, fl & ~O_NONBLOCK) < 0) { + return std::unexpected(last_error()); + } + return fd; + } + if (fd.error().value() != ENXIO && fd.error().value() != EINTR) { + return std::unexpected(fd.error()); + } + sleep_poll(); + } + return Fd{}; // stop requested: invalid fd sentinel +} + +enum class Relay : std::uint8_t { moved, idle, detached, no_splice }; + +// splice(2) fast path: move log bytes kernel-side into the pipe. The file +// offset only advances on success, so a detached reader (EPIPE) loses +// nothing — the same bytes are respliced for the next reader. +[[nodiscard]] std::expected +relay_splice(const Fd& log, const Fd& w) { + const ssize_t n = ::splice(log.get(), nullptr, w.get(), nullptr, kChunk, 0); + if (n > 0) { + return Relay::moved; + } + if (n == 0) { + return Relay::idle; + } + switch (errno) { + case EPIPE: + return Relay::detached; + case EINTR: + case EAGAIN: + return Relay::idle; + case EINVAL: + case ENOSYS: + return Relay::no_splice; // fs without splice support: fall back + default: + return std::unexpected(last_error()); + } +} + +// read/write fallback with the same no-loss contract: on EPIPE the file +// offset is rewound to the first unwritten byte. +[[nodiscard]] std::expected +relay_rw(const Fd& log, const Fd& w) { + const off_t before = ::lseek(log.get(), 0, SEEK_CUR); + std::array buf; + const ssize_t n = ::read(log.get(), buf.data(), buf.size()); + if (n < 0) { + if (errno == EINTR) { + return Relay::idle; + } + return std::unexpected(last_error()); + } + if (n == 0) { + return Relay::idle; + } + ssize_t written = 0; + while (written < n) { + const ssize_t k = ::write(w.get(), buf.data() + written, + static_cast(n - written)); + if (k < 0) { + if (errno == EINTR) { + continue; + } + const int saved = errno; + ::lseek(log.get(), before + written, SEEK_SET); + if (saved == EPIPE) { + return Relay::detached; + } + return std::unexpected(std::error_code{saved, std::system_category()}); + } + written += k; + } + return Relay::moved; +} + +int cmd_tail(const std::string& log_path, const std::string& fifo) { + struct sigaction ign{}; + ign.sa_handler = SIG_IGN; // EPIPE stays an error value, never fatal + ::sigaction(SIGPIPE, &ign, nullptr); + struct sigaction stop{}; + stop.sa_handler = on_stop; // no SA_RESTART: blocking calls return EINTR + ::sigaction(SIGINT, &stop, nullptr); + ::sigaction(SIGTERM, &stop, nullptr); + + if (::mkfifo(fifo.c_str(), 0644) != 0) { + if (errno != EEXIST) { + std::println(stderr, "pmon: mkfifo {}: {}", fifo, last_error().message()); + return 1; + } + struct stat st{}; + if (::stat(fifo.c_str(), &st) != 0 || !S_ISFIFO(st.st_mode)) { + std::println(stderr, "pmon: {} exists and is not a FIFO", fifo); + return 1; + } + } + auto log = checked(::open(log_path.c_str(), O_RDONLY | O_CLOEXEC)); + if (!log) { + std::println(stderr, "pmon: open {}: {}", log_path, log.error().message()); + return 1; + } + std::println(stderr, "pmon: tail ready (fifo {})", fifo); + + bool use_splice = true; + while (g_stop == 0) { + auto w = open_writer(fifo); + if (!w) { + std::println(stderr, "pmon: open {}: {}", fifo, w.error().message()); + return 1; + } + if (!w->valid()) { + break; // stop requested while waiting for a reader + } + bool attached = true; + while (g_stop == 0 && attached) { + const auto r = use_splice ? relay_splice(*log, *w) : relay_rw(*log, *w); + if (!r) { + std::println(stderr, "pmon: relay: {}", r.error().message()); + return 1; + } + switch (*r) { + case Relay::moved: + break; + case Relay::idle: + sleep_poll(); + break; + case Relay::no_splice: + use_splice = false; + break; + case Relay::detached: + std::println("pmon: tail reader detached"); + std::fflush(stdout); + attached = false; + break; + } + } + } + std::println("pmon: exiting (signal)"); + std::fflush(stdout); + return 0; +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +int usage() { + std::println(stderr, "usage: pmon "); + std::println(stderr, + " run -- CMD [ARGS...] spawn CMD, wait, mirror its exit"); + std::println(stderr, + " supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T]"); + std::println(stderr, + " [--log FILE] -- CMD [ARGS...] restart CMD on abnormal exit;"); + std::println(stderr, + " capture stdout/stderr into FILE"); + std::println(stderr, + " (defaults: pidfd, N=3, T=10000,"); + std::println(stderr, + " FILE=pmon.log)"); + std::println(stderr, + " tail --log FILE --fifo PATH relay appended log lines into a"); + std::println(stderr, + " FIFO created at PATH"); + return 2; +} + +[[nodiscard]] std::optional parse_int(std::string_view s) { + int value{}; + const auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value); + if (ec != std::errc{} || ptr != s.data() + s.size()) { + return std::nullopt; + } + return value; +} + +} // namespace + +int main(int argc, char** argv) { + const std::vector args{argv + 1, argv + argc}; + if (args.empty()) { + return usage(); + } + + if (args[0] == "tail") { + std::string log_path, fifo; + std::size_t i = 1; + while (i + 1 < args.size()) { + if (args[i] == "--log") { + log_path = args[i + 1]; + } else if (args[i] == "--fifo") { + fifo = args[i + 1]; + } else { + return usage(); + } + i += 2; + } + if (i != args.size() || log_path.empty() || fifo.empty()) { + return usage(); + } + return cmd_tail(log_path, fifo); + } + + const auto sep = std::ranges::find(args, std::string_view{"--"}); + if (sep == args.end() || sep + 1 == args.end()) { + return usage(); // run and supervise need `-- CMD [ARGS...]` + } + const std::vector flags{args.begin() + 1, sep}; + std::vector cmd; + for (auto it = sep + 1; it != args.end(); ++it) { + cmd.emplace_back(*it); + } + + if (args[0] == "run") { + if (!flags.empty()) { + return usage(); + } + return cmd_run(cmd); + } + if (args[0] == "supervise") { + auto engine = Engine::pidfd; + int max_restarts = 3; + int timeout_ms = 10'000; + std::string log_path = "pmon.log"; + for (std::size_t i = 0; i < flags.size(); i += 2) { + if (i + 1 >= flags.size()) { + return usage(); + } + const auto flag = flags[i]; + const auto value = flags[i + 1]; + if (flag == "--engine") { + if (value == "pidfd") { + engine = Engine::pidfd; + } else if (value == "sigchld") { + engine = Engine::sigchld; + } else { + return usage(); + } + } else if (flag == "--max-restarts") { + const auto n = parse_int(value); + if (!n || *n < 0) { + return usage(); + } + max_restarts = *n; + } else if (flag == "--timeout-ms") { + const auto t = parse_int(value); + if (!t || *t <= 0) { + return usage(); + } + timeout_ms = *t; + } else if (flag == "--log") { + log_path = value; + } else { + return usage(); + } + } + return cmd_supervise(engine, max_restarts, timeout_ms, log_path, cmd); + } + return usage(); +} diff --git a/examples/18-pipes-fifos-splice/demo.sh b/examples/18-pipes-fifos-splice/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/18-pipes-fifos-splice/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/18-pipes-fifos-splice/go/demo.sh b/examples/18-pipes-fifos-splice/go/demo.sh new file mode 100755 index 0000000..7652fdc --- /dev/null +++ b/examples/18-pipes-fifos-splice/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run run -- /bin/sh -c "echo pmon self-demo" ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/18-pipes-fifos-splice/go/go.mod b/examples/18-pipes-fifos-splice/go/go.mod new file mode 100644 index 0000000..c6b3d44 --- /dev/null +++ b/examples/18-pipes-fifos-splice/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/18-pipes-fifos-splice/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/18-pipes-fifos-splice/go/go.sum b/examples/18-pipes-fifos-splice/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/18-pipes-fifos-splice/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/18-pipes-fifos-splice/go/main.go b/examples/18-pipes-fifos-splice/go/main.go new file mode 100644 index 0000000..3e4b40a --- /dev/null +++ b/examples/18-pipes-fifos-splice/go/main.go @@ -0,0 +1,601 @@ +// pmon v4 — the process supervisor becomes a log pipeline. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child via SIGCHLD +// through signal.Notify, reaped with wait4(WNOHANG). +// v2 (`supervise --engine pidfd`, the DEFAULT) supervises through +// unix.PidfdOpen + unix.Poll + waitid(P_PIDFD) — no pid-reuse race. +// v4 (this chapter) keeps both engines and adds pipes: +// - supervise captures the child's stdout AND stderr through two pipes +// into a log file as "[out] ..."/"[err] ..." lines. Where C++/Rust add +// the pipe fds to their poll set, Go inverts idiomatically: one +// goroutine per pipe feeds a channel of prefixed lines into the same +// select loop that already multiplexes exit, signals, and the deadline; +// - `tail --log F --fifo PATH` creates a FIFO and relays log bytes into +// whatever reader attaches — io.Copy per reader session, rewinding the +// log offset to start+written on EPIPE so nothing is lost; SIGPIPE +// ignored, the detach reported as "pmon: tail reader detached". +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "os/signal" + "strconv" + "sync" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + chunk = 64 * 1024 + pollTick = 50 * time.Millisecond +) + +// childStatus is the reaped state of one child. +type childStatus struct { + exited bool // true: value is the exit status; false: value is the signal + value int +} + +// reportExit prints the exit-observation line and returns the mirrored code. +func reportExit(pid int, st childStatus) int { + if st.exited { + fmt.Printf("pmon: child=%d exited status=%d\n", pid, st.value) + return st.value + } + fmt.Printf("pmon: child=%d killed signal=%d\n", pid, st.value) + return 128 + st.value +} + +func fromWaitStatus(ws syscall.WaitStatus) childStatus { + if ws.Signaled() { + return childStatus{exited: false, value: int(ws.Signal())} + } + return childStatus{exited: true, value: ws.ExitStatus()} +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror (stdio inherited, no capture). +// --------------------------------------------------------------------------- + +func cmdRun(cmdline []string) int { + c := exec.Command(cmdline[0], cmdline[1:]...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + if err := c.Start(); err != nil { + fmt.Fprintf(os.Stderr, "pmon: spawn: %s: %v\n", cmdline[0], err) + return 1 + } + pid := c.Process.Pid + fmt.Fprintf(os.Stderr, "pmon: run child=%d\n", pid) + st := childStatus{exited: true, value: 0} + if err := c.Wait(); err != nil { + var ee *exec.ExitError + if !errors.As(err, &ee) { + fmt.Fprintf(os.Stderr, "pmon: wait: %v\n", err) + return 1 + } + st = fromWaitStatus(ee.Sys().(syscall.WaitStatus)) + } + return reportExit(pid, st) +} + +// --------------------------------------------------------------------------- +// v2 plumbing: waitid(P_PIDFD) with an explicit siginfo_t layout — x/sys +// keeps the union opaque, and the CLD_* fields are what we need. +// --------------------------------------------------------------------------- + +type siginfoChld struct { + signo int32 + errno int32 + code int32 + _ int32 // alignment padding before the union on 64-bit + pid int32 + uid uint32 + status int32 + _ [100]byte // pad siginfo_t out to its full 128 bytes +} + +const cldExited = 1 // si_code: child called _exit + +// reapPidfd blocks in waitid(P_PIDFD) until the process behind pidfd is +// reaped — it can never collect a recycled pid. +func reapPidfd(pidfd int) (childStatus, error) { + var si siginfoChld + for { + _, _, errno := unix.Syscall6(unix.SYS_WAITID, uintptr(unix.P_PIDFD), + uintptr(pidfd), uintptr(unsafe.Pointer(&si)), uintptr(unix.WEXITED), 0, 0) + if errno == unix.EINTR { + continue + } + if errno != 0 { + return childStatus{}, fmt.Errorf("waitid: %w", errno) + } + if si.code == cldExited { + return childStatus{exited: true, value: int(si.status)}, nil + } + return childStatus{exited: false, value: int(si.status)}, nil + } +} + +// tryReap collects the child if it has already exited (WNOHANG). +func tryReap(pid int) (childStatus, bool, error) { + var ws syscall.WaitStatus + for { + wpid, err := syscall.Wait4(pid, &ws, syscall.WNOHANG, nil) + if err == syscall.EINTR { + continue + } + if err != nil { + return childStatus{}, false, fmt.Errorf("wait4: %w", err) + } + if wpid != pid { + return childStatus{}, false, nil // coalesced/stale SIGCHLD + } + return fromWaitStatus(ws), true, nil + } +} + +func blockingReap(pid int) (childStatus, error) { + var ws syscall.WaitStatus + for { + _, err := syscall.Wait4(pid, &ws, 0, nil) + if err == syscall.EINTR { + continue + } + if err != nil { + return childStatus{}, fmt.Errorf("wait4: %w", err) + } + return fromWaitStatus(ws), nil + } +} + +// --------------------------------------------------------------------------- +// v4 capture: two pipes, line-buffered into the log with [out]/[err] tags. +// --------------------------------------------------------------------------- + +type logLine struct { + prefix string + text string +} + +// scanInto reads one child stream line by line into the shared channel. +// Scanner also delivers a trailing partial line, so it lands in the log +// with a '\n' like everything else. +func scanInto(wg *sync.WaitGroup, r io.Reader, prefix string, lines chan<- logLine) { + defer wg.Done() + sc := bufio.NewScanner(r) + sc.Buffer(make([]byte, chunk), chunk) // bound the line buffer + for sc.Scan() { + lines <- logLine{prefix: prefix, text: sc.Text()} + } +} + +func writeLine(logw *os.File, l logLine) error { + if _, err := fmt.Fprintf(logw, "[%s] %s\n", l.prefix, l.text); err != nil { + return fmt.Errorf("write log: %w", err) + } + return nil +} + +// capture wires both stdio streams of an un-started command into the log. +// lines closes when both pipes hit EOF — i.e. the capture is complete. +func capture(c *exec.Cmd) (<-chan logLine, error) { + outp, err := c.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("stdout pipe: %w", err) + } + errp, err := c.StderrPipe() + if err != nil { + return nil, fmt.Errorf("stderr pipe: %w", err) + } + lines := make(chan logLine, 64) + var wg sync.WaitGroup + wg.Add(2) + go scanInto(&wg, outp, "out", lines) + go scanInto(&wg, errp, "err", lines) + go func() { + wg.Wait() + close(lines) + }() + return lines, nil +} + +// drainLines relays whatever the scanners still hold after the child was +// reaped, so every logged byte lands before the exit is reported. +func drainLines(lines <-chan logLine, logw *os.File) error { + if lines == nil { + return nil + } + for l := range lines { + if err := writeLine(logw, l); err != nil { + return err + } + } + return nil +} + +// --------------------------------------------------------------------------- +// v1/v2 engines with the v4 line channel joined into the same select. +// --------------------------------------------------------------------------- + +func cmdSupervise(engine string, maxRestarts int, timeout time.Duration, + logPath string, cmdline []string) int { + logw, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: open %s: %v\n", logPath, err) + return 1 + } + defer logw.Close() + + stop := make(chan os.Signal, 2) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + var chld chan os.Signal + if engine == "sigchld" { + chld = make(chan os.Signal, 16) + signal.Notify(chld, syscall.SIGCHLD) // registered BEFORE the first spawn + } + deadline := time.After(timeout) + + restarts := 0 + for { + c := exec.Command(cmdline[0], cmdline[1:]...) + lines, err := capture(c) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + if err := c.Start(); err != nil { + fmt.Fprintf(os.Stderr, "pmon: spawn: %s: %v\n", cmdline[0], err) + return 1 + } + pid := c.Process.Pid + + var st childStatus + stopped := "" // "", "signal", "timeout" + + if engine == "pidfd" { + pidfd, err := unix.PidfdOpen(pid, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: pidfd_open: %v\n", err) + return 1 + } + fmt.Fprintf(os.Stderr, "pmon: engine=pidfd child=%d pidfd=%d\n", pid, pidfd) + exited := make(chan struct{}) + go func() { // the pidfd becomes readable exactly when the child exits + fds := []unix.PollFd{{Fd: int32(pidfd), Events: unix.POLLIN}} + for { + if _, err := unix.Poll(fds, -1); err == unix.EINTR { + continue + } + close(exited) + return + } + }() + linesCh := lines + waiting := true + for waiting { + select { + case l, ok := <-linesCh: + if !ok { + linesCh = nil + continue + } + if err := writeLine(logw, l); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + case <-exited: + waiting = false + case <-stop: + stopped = "signal" + waiting = false + case <-deadline: + stopped = "timeout" + waiting = false + } + } + if stopped != "" { + _ = unix.PidfdSendSignal(pidfd, unix.SIGTERM, nil, 0) + <-exited + } + st, err = reapPidfd(pidfd) + unix.Close(pidfd) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + if err := drainLines(linesCh, logw); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + } else { + fmt.Fprintf(os.Stderr, "pmon: engine=sigchld child=%d\n", pid) + linesCh := lines + done := false + for !done && stopped == "" { + select { + case l, ok := <-linesCh: + if !ok { + linesCh = nil + continue + } + if err := writeLine(logw, l); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + case <-chld: + s, reaped, err := tryReap(pid) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + if reaped { + st, done = s, true + } + case <-stop: + stopped = "signal" + case <-deadline: + stopped = "timeout" + } + } + if stopped != "" { + _ = syscall.Kill(pid, syscall.SIGTERM) + if st, err = blockingReap(pid); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + } + if err := drainLines(linesCh, logw); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + } + _ = c.Process.Release() // child already reaped; drop Go's process handle + + reportExit(pid, st) + if stopped != "" { + fmt.Printf("pmon: exiting (%s)\n", stopped) + return 0 + } + if st.exited && st.value == 0 { + return 0 + } + if restarts >= maxRestarts { + fmt.Printf("pmon: giving up after %d restarts\n", maxRestarts) + return 1 + } + restarts++ + fmt.Printf("pmon: restart %d/%d\n", restarts, maxRestarts) + } +} + +// --------------------------------------------------------------------------- +// v4: tail — follow the log, relay into a FIFO, survive reader churn. +// --------------------------------------------------------------------------- + +// ensureFifo creates PATH as a FIFO, tolerating an existing FIFO at PATH. +func ensureFifo(path string) error { + err := unix.Mkfifo(path, 0o644) + if err == nil { + return nil + } + if !errors.Is(err, unix.EEXIST) { + return fmt.Errorf("mkfifo %s: %w", path, err) + } + st, serr := os.Stat(path) + if serr != nil { + return fmt.Errorf("stat %s: %w", path, serr) + } + if st.Mode()&fs.ModeNamedPipe == 0 { + return fmt.Errorf("%s exists and is not a FIFO", path) + } + return nil +} + +// openWriter opens the FIFO for writing without blocking forever: +// O_NONBLOCK turns "no reader yet" into ENXIO, which we poll. Once a reader +// is attached the fd goes back to blocking mode for the relay. +func openWriter(path string, stop <-chan os.Signal) (*os.File, bool, error) { + for { + select { + case <-stop: + return nil, true, nil + default: + } + fd, err := unix.Open(path, unix.O_WRONLY|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) + if err == nil { + if err := unix.SetNonblock(fd, false); err != nil { + unix.Close(fd) + return nil, false, fmt.Errorf("clear O_NONBLOCK on %s: %w", path, err) + } + return os.NewFile(uintptr(fd), path), false, nil + } + if errors.Is(err, unix.ENXIO) || errors.Is(err, unix.EINTR) { + time.Sleep(pollTick) + continue + } + return nil, false, fmt.Errorf("open %s: %w", path, err) + } +} + +func cmdTail(logPath, fifo string) int { + signal.Ignore(syscall.SIGPIPE) // EPIPE stays an error value, never fatal + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + if err := ensureFifo(fifo); err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + logf, err := os.Open(logPath) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: open %s: %v\n", logPath, err) + return 1 + } + defer logf.Close() + fmt.Fprintf(os.Stderr, "pmon: tail ready (fifo %s)\n", fifo) + +reader: + for { + w, stopped, err := openWriter(fifo, stop) + if err != nil { + fmt.Fprintf(os.Stderr, "pmon: %v\n", err) + return 1 + } + if stopped { + break + } + for { + select { + case <-stop: + w.Close() + break reader + default: + } + // One copy session: on EPIPE (reader detached) io.Copy reports + // how many bytes actually reached the pipe, so the log offset is + // rewound to the first unwritten byte — the next reader loses + // nothing. + start, serr := logf.Seek(0, io.SeekCurrent) + if serr != nil { + w.Close() + fmt.Fprintf(os.Stderr, "pmon: seek %s: %v\n", logPath, serr) + return 1 + } + n, cerr := io.Copy(w, logf) + if cerr == nil { // caught up: poll for appended lines + time.Sleep(pollTick) + continue + } + if errors.Is(cerr, syscall.EPIPE) { + fmt.Println("pmon: tail reader detached") + if _, serr := logf.Seek(start+n, io.SeekStart); serr != nil { + w.Close() + fmt.Fprintf(os.Stderr, "pmon: seek %s: %v\n", logPath, serr) + return 1 + } + w.Close() + continue reader + } + w.Close() + fmt.Fprintf(os.Stderr, "pmon: relay: %v\n", cerr) + return 1 + } + } + fmt.Println("pmon: exiting (signal)") + return 0 +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +func usage() int { + fmt.Fprint(os.Stderr, `usage: pmon + run -- CMD [ARGS...] spawn CMD, wait, mirror its exit + supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T] + [--log FILE] -- CMD [ARGS...] restart CMD on abnormal exit; + capture stdout/stderr into FILE + (defaults: pidfd, N=3, T=10000, + FILE=pmon.log) + tail --log FILE --fifo PATH relay appended log lines into a + FIFO created at PATH +`) + return 2 +} + +func realMain(args []string) int { + if len(args) == 0 { + return usage() + } + + if args[0] == "tail" { + var logPath, fifo string + i := 1 + for i+1 < len(args) { + switch args[i] { + case "--log": + logPath = args[i+1] + case "--fifo": + fifo = args[i+1] + default: + return usage() + } + i += 2 + } + if i != len(args) || logPath == "" || fifo == "" { + return usage() + } + return cmdTail(logPath, fifo) + } + + sep := -1 + for i, a := range args { + if a == "--" { + sep = i + break + } + } + if sep < 0 || sep+1 >= len(args) { + return usage() // run and supervise need `-- CMD [ARGS...]` + } + flags := args[1:sep] + cmdline := args[sep+1:] + + switch args[0] { + case "run": + if len(flags) != 0 { + return usage() + } + return cmdRun(cmdline) + case "supervise": + engine := "pidfd" + maxRestarts := 3 + timeoutMs := 10000 + logPath := "pmon.log" + for i := 0; i < len(flags); i += 2 { + if i+1 >= len(flags) { + return usage() + } + switch flags[i] { + case "--engine": + if flags[i+1] != "pidfd" && flags[i+1] != "sigchld" { + return usage() + } + engine = flags[i+1] + case "--max-restarts": + n, err := strconv.Atoi(flags[i+1]) + if err != nil || n < 0 { + return usage() + } + maxRestarts = n + case "--timeout-ms": + t, err := strconv.Atoi(flags[i+1]) + if err != nil || t <= 0 { + return usage() + } + timeoutMs = t + case "--log": + logPath = flags[i+1] + default: + return usage() + } + } + return cmdSupervise(engine, maxRestarts, + time.Duration(timeoutMs)*time.Millisecond, logPath, cmdline) + } + return usage() +} + +func main() { + os.Exit(realMain(os.Args[1:])) +} diff --git a/examples/18-pipes-fifos-splice/rust/Cargo.lock b/examples/18-pipes-fifos-splice/rust/Cargo.lock new file mode 100644 index 0000000..cda6cfe --- /dev/null +++ b/examples/18-pipes-fifos-splice/rust/Cargo.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "pmon" +version = "0.4.0" +dependencies = [ + "anyhow", + "nix", + "rustix", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/examples/18-pipes-fifos-splice/rust/Cargo.toml b/examples/18-pipes-fifos-splice/rust/Cargo.toml new file mode 100644 index 0000000..519c067 --- /dev/null +++ b/examples/18-pipes-fifos-splice/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pmon" +version = "0.4.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +nix = { version = "0.31", features = ["event", "signal", "process", "fs"] } +rustix = { version = "1", features = ["event", "fs", "pipe", "process"] } diff --git a/examples/18-pipes-fifos-splice/rust/demo.sh b/examples/18-pipes-fifos-splice/rust/demo.sh new file mode 100755 index 0000000..842c13f --- /dev/null +++ b/examples/18-pipes-fifos-splice/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + exec "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run run -- /bin/sh -c "echo pmon self-demo" ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/18-pipes-fifos-splice/rust/rust-toolchain.toml b/examples/18-pipes-fifos-splice/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/18-pipes-fifos-splice/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/18-pipes-fifos-splice/rust/src/main.rs b/examples/18-pipes-fifos-splice/rust/src/main.rs new file mode 100644 index 0000000..9393b56 --- /dev/null +++ b/examples/18-pipes-fifos-splice/rust/src/main.rs @@ -0,0 +1,686 @@ +// pmon v4 — the process supervisor becomes a log pipeline. +// +// v0 (`run`) spawns a command, waits, and mirrors its exit status. +// v1 (`supervise --engine sigchld`) restarts a crashing child via SIGCHLD +// delivered as readable data through a signalfd (nix). +// v2 (`supervise --engine pidfd`, the DEFAULT) supervises through +// rustix::process::pidfd_open + poll + waitid(WaitId::PidFd) — no +// pid-reuse race. +// v4 (this chapter) keeps both engines and adds pipes: +// * supervise captures the child's stdout AND stderr through two pipes +// into a log file as "[out] ..."/"[err] ..." lines — the two pipe read +// ends simply join the fd set each engine already polls; +// * `tail --log F --fifo PATH` creates a FIFO and relays log bytes into +// whatever reader attaches — rustix::pipe::splice fast path +// (file -> pipe, kernel-side), read/write fallback; SIGPIPE ignored, +// EPIPE reported as "pmon: tail reader detached" and survived, +// nothing lost. + +use std::fs::File; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::fd::{AsFd, AsRawFd, OwnedFd}; +use std::os::unix::fs::FileTypeExt; +use std::os::unix::process::{CommandExt, ExitStatusExt}; +use std::process::{ChildStderr, ChildStdout, Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, bail}; +use nix::errno::Errno; +use nix::sys::signal::{ + SaFlags, SigAction, SigHandler, SigSet, Signal as NixSignal, kill, sigaction, +}; +use nix::sys::signalfd::{SfdFlags, SignalFd}; +use nix::sys::stat::Mode as NixMode; +use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid}; +use nix::unistd::{Pid as NixPid, mkfifo}; +use rustix::event::{PollFd, PollFlags, Timespec, poll}; +use rustix::fs::{OFlags, fcntl_setfl}; +use rustix::pipe::{SpliceFlags, splice}; +use rustix::process::{ + Pid, PidfdFlags, Signal, WaitId, WaitIdOptions, pidfd_open, pidfd_send_signal, waitid, +}; + +const CHUNK: usize = 64 * 1024; +const POLL_TICK: Duration = Duration::from_millis(50); + +// --------------------------------------------------------------------------- +// Status plumbing shared by every subcommand. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy)] +struct ChildStatus { + exited: bool, // true: value is the exit status; false: value is the signal + value: i32, +} + +/// Prints the exit-observation line and returns the mirrored exit code. +fn report_exit(pid: i32, st: ChildStatus) -> i32 { + if st.exited { + println!("pmon: child={} exited status={}", pid, st.value); + st.value + } else { + println!("pmon: child={} killed signal={}", pid, st.value); + 128 + st.value + } +} + +/// Spawn with an empty signal mask in the child — the supervisor blocks +/// signals for its signalfd and std does NOT reset the mask across +/// fork+exec (a supervised `sleep` with SIGTERM blocked would never stop). +/// sigprocmask is async-signal-safe, so pre_exec may call it. +fn spawn(cmdline: &[String], piped: bool) -> Result { + let mut cmd = Command::new(&cmdline[0]); + cmd.args(&cmdline[1..]); + if piped { + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + } + unsafe { + cmd.pre_exec(|| { + SigSet::empty() + .thread_set_mask() + .map_err(|e| std::io::Error::from_raw_os_error(e as i32)) + }); + } + cmd.spawn().with_context(|| format!("spawn: {}", cmdline[0])) +} + +// --------------------------------------------------------------------------- +// v0: run — spawn, wait, mirror (stdio inherited, no capture). +// --------------------------------------------------------------------------- + +fn cmd_run(cmdline: &[String]) -> Result { + let mut child = spawn(cmdline, false)?; + let pid = child.id() as i32; + eprintln!("pmon: run child={pid}"); + let status = child.wait().context("wait")?; + let st = match status.signal() { + Some(sig) => ChildStatus { exited: false, value: sig }, + None => ChildStatus { exited: true, value: status.code().unwrap_or(0) }, + }; + Ok(report_exit(pid, st)) +} + +// --------------------------------------------------------------------------- +// v4 capture: two pipes, line-buffered into the log with [out]/[err] tags. +// --------------------------------------------------------------------------- + +/// Accumulates one stream's bytes and appends complete lines to the log as +/// "[pfx] line\n". Line-buffered: each completed line is written immediately. +struct LineRelay { + prefix: &'static str, + buf: Vec, +} + +impl LineRelay { + fn new(prefix: &'static str) -> Self { + Self { prefix, buf: Vec::new() } + } + + fn feed(&mut self, chunk: &[u8], log: &mut File) -> Result<()> { + self.buf.extend_from_slice(chunk); + while let Some(nl) = self.buf.iter().position(|&b| b == b'\n') { + let line: Vec = self.buf.drain(..=nl).collect(); + self.emit(&line[..line.len() - 1], log)?; + } + if self.buf.len() > CHUNK { + // bound the partial-line buffer + let line = std::mem::take(&mut self.buf); + self.emit(&line, log)?; + } + Ok(()) + } + + /// Stream closed: a trailing partial line still gets logged, with '\n'. + fn flush(&mut self, log: &mut File) -> Result<()> { + if self.buf.is_empty() { + return Ok(()); + } + let line = std::mem::take(&mut self.buf); + self.emit(&line, log) + } + + fn emit(&self, line: &[u8], log: &mut File) -> Result<()> { + log.write_all(format!("[{}] ", self.prefix).as_bytes()) + .and_then(|_| log.write_all(line)) + .and_then(|_| log.write_all(b"\n")) + .context("write log") + } +} + +/// One child's capture pipes and their relays. +struct Capture { + out_pipe: ChildStdout, + err_pipe: ChildStderr, + out_relay: LineRelay, + err_relay: LineRelay, + out_open: bool, + err_open: bool, +} + +impl Capture { + fn new(child: &mut std::process::Child) -> Result { + Ok(Self { + out_pipe: child.stdout.take().context("no stdout pipe")?, + err_pipe: child.stderr.take().context("no stderr pipe")?, + out_relay: LineRelay::new("out"), + err_relay: LineRelay::new("err"), + out_open: true, + err_open: true, + }) + } + + /// One readiness-driven read on the stdout pipe; EOF closes and flushes. + fn drain_out(&mut self, log: &mut File) -> Result<()> { + let mut buf = vec![0u8; CHUNK]; + match self.out_pipe.read(&mut buf) { + Ok(0) => { + self.out_open = false; + self.out_relay.flush(log) + } + Ok(n) => self.out_relay.feed(&buf[..n], log), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok(()), + Err(e) => Err(e).context("read stdout pipe"), + } + } + + fn drain_err(&mut self, log: &mut File) -> Result<()> { + let mut buf = vec![0u8; CHUNK]; + match self.err_pipe.read(&mut buf) { + Ok(0) => { + self.err_open = false; + self.err_relay.flush(log) + } + Ok(n) => self.err_relay.feed(&buf[..n], log), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok(()), + Err(e) => Err(e).context("read stderr pipe"), + } + } + + /// The child is reaped: blocking-read both pipes to EOF so every logged + /// byte lands before the exit is reported. + fn drain_to_eof(&mut self, log: &mut File) -> Result<()> { + while self.out_open { + self.drain_out(log)?; + } + while self.err_open { + self.drain_err(log)?; + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// v1/v2 engines with the v4 pipes joined into the same poll set. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, PartialEq)] +enum Engine { + Pidfd, + Sigchld, +} + +#[derive(Clone, Copy)] +enum Stop { + Signal, + Timeout, +} + +enum Outcome { + Reaped(ChildStatus), + Stopped(Stop), +} + +fn remaining(deadline: Instant) -> Timespec { + let left = deadline.saturating_duration_since(Instant::now()); + Timespec { + tv_sec: left.as_secs() as i64, + tv_nsec: i64::from(left.subsec_nanos()), + } +} + +/// Block `sigs` process-wide and return a signalfd delivering them as data. +fn make_signalfd(sigs: &[NixSignal]) -> Result { + let mut mask = SigSet::empty(); + for s in sigs { + mask.add(*s); + } + mask.thread_block().context("sigprocmask")?; + SignalFd::with_flags(&mask, SfdFlags::SFD_CLOEXEC).context("signalfd") +} + +fn status_from_waitid(w: rustix::process::WaitIdStatus) -> ChildStatus { + if let Some(sig) = w.terminating_signal() { + ChildStatus { exited: false, value: sig as i32 } + } else { + ChildStatus { exited: true, value: w.exit_status().unwrap_or(0) } + } +} + +/// waitid(P_PIDFD) reaps the process the fd refers to — never a recycled pid. +fn reap_pidfd(pidfd: &impl AsFd) -> Result { + let status = waitid(WaitId::PidFd(pidfd.as_fd()), WaitIdOptions::EXITED) + .context("waitid(P_PIDFD)")? + .context("waitid(P_PIDFD): no status")?; + Ok(status_from_waitid(status)) +} + +fn blocking_reap(pid: NixPid) -> Result { + match waitpid(pid, None).context("waitpid")? { + WaitStatus::Exited(_, code) => Ok(ChildStatus { exited: true, value: code }), + WaitStatus::Signaled(_, sig, _) => Ok(ChildStatus { exited: false, value: sig as i32 }), + other => bail!("waitpid: unexpected status {other:?}"), + } +} + +/// One poll loop for both engines: the capture pipes, the signalfd, and +/// (pidfd engine only) the pidfd. Closed pipes drop out of the poll set. +fn wait_child( + pid: NixPid, + pidfd: Option<&OwnedFd>, + sigfd: &SignalFd, + deadline: Instant, + cap: &mut Capture, + log: &mut File, +) -> Result { + // Fixed slot order; a closed pipe polls a harmless duplicate of sigfd + // with no requested events instead. + loop { + let mut fds = [ + PollFd::new(sigfd, PollFlags::IN), + PollFd::new( + &cap.out_pipe, + if cap.out_open { PollFlags::IN } else { PollFlags::empty() }, + ), + PollFd::new( + &cap.err_pipe, + if cap.err_open { PollFlags::IN } else { PollFlags::empty() }, + ), + match pidfd { + Some(fd) => PollFd::new(fd, PollFlags::IN), + None => PollFd::new(sigfd, PollFlags::empty()), + }, + ]; + let n = match poll(&mut fds, Some(&remaining(deadline))) { + Err(rustix::io::Errno::INTR) => continue, + other => other.context("poll")?, + }; + if n == 0 { + return Ok(Outcome::Stopped(Stop::Timeout)); + } + let revents: Vec = fds.iter().map(|f| f.revents()).collect(); + if cap.out_open && !revents[1].is_empty() { + cap.drain_out(log)?; + } + if cap.err_open && !revents[2].is_empty() { + cap.drain_err(log)?; + } + if revents[0].contains(PollFlags::IN) { + let info = sigfd.read_signal().context("read signalfd")?; + let signo = info.map(|si| si.ssi_signo as i32).unwrap_or(0); + if signo != NixSignal::SIGCHLD as i32 { + return Ok(Outcome::Stopped(Stop::Signal)); + } + // sigchld engine: WNOHANG-reap; coalesced SIGCHLD is benign. + match waitpid(pid, Some(WaitPidFlag::WNOHANG)).context("waitpid")? { + WaitStatus::Exited(_, code) => { + return Ok(Outcome::Reaped(ChildStatus { exited: true, value: code })); + } + WaitStatus::Signaled(_, sig, _) => { + return Ok(Outcome::Reaped(ChildStatus { + exited: false, + value: sig as i32, + })); + } + _ => {} // our child is still running + } + } + if let Some(fd) = pidfd + && !revents[3].is_empty() + { + return Ok(Outcome::Reaped(reap_pidfd(fd)?)); + } + } +} + +fn cmd_supervise( + engine: Engine, + max_restarts: u32, + timeout_ms: u64, + log_path: &str, + cmdline: &[String], +) -> Result { + let mut log = File::options() + .create(true) + .append(true) + .open(log_path) + .with_context(|| format!("open {log_path}"))?; + let sigfd = match engine { + Engine::Pidfd => make_signalfd(&[NixSignal::SIGINT, NixSignal::SIGTERM])?, + Engine::Sigchld => { + // The mask (and the signalfd over it) exist BEFORE the first + // spawn, so an early exit's SIGCHLD is queued, never lost. + make_signalfd(&[NixSignal::SIGCHLD, NixSignal::SIGINT, NixSignal::SIGTERM])? + } + }; + let deadline = Instant::now() + Duration::from_millis(timeout_ms); + + let mut restarts: u32 = 0; + loop { + let mut child = spawn(cmdline, true)?; + let pid = child.id() as i32; + let mut cap = Capture::new(&mut child)?; + + let pidfd = match engine { + Engine::Pidfd => { + let pidfd = pidfd_open(Pid::from_raw(pid).unwrap(), PidfdFlags::empty()) + .context("pidfd_open")?; + eprintln!("pmon: engine=pidfd child={pid} pidfd={}", pidfd.as_raw_fd()); + Some(pidfd) + } + Engine::Sigchld => { + eprintln!("pmon: engine=sigchld child={pid}"); + None + } + }; + + let outcome = wait_child( + NixPid::from_raw(pid), + pidfd.as_ref(), + &sigfd, + deadline, + &mut cap, + &mut log, + )?; + + match outcome { + Outcome::Stopped(why) => { + // Ask the child to terminate, reap it, finish the log, + // report why we leave. + let st = match &pidfd { + Some(fd) => { + let _ = pidfd_send_signal(fd, Signal::TERM); + reap_pidfd(fd)? + } + None => { + let _ = kill(NixPid::from_raw(pid), NixSignal::SIGTERM); + blocking_reap(NixPid::from_raw(pid))? + } + }; + cap.drain_to_eof(&mut log)?; + let _ = report_exit(pid, st); + let why = match why { + Stop::Timeout => "timeout", + Stop::Signal => "signal", + }; + println!("pmon: exiting ({why})"); + return Ok(0); + } + Outcome::Reaped(st) => { + // Finish the log before reporting, then apply the policy. + cap.drain_to_eof(&mut log)?; + let _ = report_exit(pid, st); + if st.exited && st.value == 0 { + return Ok(0); + } + if restarts >= max_restarts { + println!("pmon: giving up after {max_restarts} restarts"); + return Ok(1); + } + restarts += 1; + println!("pmon: restart {restarts}/{max_restarts}"); + } + } + } +} + +// --------------------------------------------------------------------------- +// v4: tail — follow the log, relay into a FIFO, survive reader churn. +// --------------------------------------------------------------------------- + +static STOP: AtomicBool = AtomicBool::new(false); + +extern "C" fn on_stop(_sig: i32) { + STOP.store(true, Ordering::SeqCst); +} + +fn stop_requested() -> bool { + STOP.load(Ordering::SeqCst) +} + +/// Open the FIFO for writing without blocking forever: O_NONBLOCK turns +/// "no reader yet" into ENXIO, which we poll. Once a reader is attached the +/// fd goes back to blocking mode for the relay. +fn open_writer(fifo: &str) -> Result> { + while !stop_requested() { + match rustix::fs::open( + fifo, + OFlags::WRONLY | OFlags::NONBLOCK | OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) { + Ok(fd) => { + fcntl_setfl(&fd, OFlags::empty()) + .with_context(|| format!("clear O_NONBLOCK on {fifo}"))?; + return Ok(Some(fd)); + } + Err(rustix::io::Errno::NXIO) | Err(rustix::io::Errno::INTR) => { + std::thread::sleep(POLL_TICK); + } + Err(e) => return Err(e).with_context(|| format!("open {fifo}")), + } + } + Ok(None) // stop requested while waiting for a reader +} + +enum Relay { + Moved, + Idle, + Detached, + NoSplice, +} + +/// splice(2) fast path: move log bytes kernel-side into the pipe. The file +/// offset only advances on success, so a detached reader (EPIPE) loses +/// nothing — the same bytes are respliced for the next reader. +fn relay_splice(log: &File, w: &OwnedFd) -> Result { + match splice(log, None, w, None, CHUNK, SpliceFlags::empty()) { + Ok(0) => Ok(Relay::Idle), + Ok(_) => Ok(Relay::Moved), + Err(rustix::io::Errno::PIPE) => Ok(Relay::Detached), + Err(rustix::io::Errno::INTR) | Err(rustix::io::Errno::AGAIN) => Ok(Relay::Idle), + Err(rustix::io::Errno::INVAL) | Err(rustix::io::Errno::NOSYS) => Ok(Relay::NoSplice), + Err(e) => Err(e).context("splice"), + } +} + +/// read/write fallback with the same no-loss contract: on EPIPE the file +/// offset is rewound to the first unwritten byte. +fn relay_rw(log: &mut File, w: &OwnedFd) -> Result { + let before = log.stream_position().context("stream_position")?; + let mut buf = vec![0u8; CHUNK]; + let n = match log.read(&mut buf) { + Ok(0) => return Ok(Relay::Idle), + Ok(n) => n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => return Ok(Relay::Idle), + Err(e) => return Err(e).context("read log"), + }; + let mut written = 0usize; + while written < n { + match rustix::io::write(w, &buf[written..n]) { + Ok(k) => written += k, + Err(rustix::io::Errno::INTR) => continue, + Err(e) => { + log.seek(SeekFrom::Start(before + written as u64)).context("seek log")?; + if e == rustix::io::Errno::PIPE { + return Ok(Relay::Detached); + } + return Err(e).context("write fifo"); + } + } + } + Ok(Relay::Moved) +} + +fn cmd_tail(log_path: &str, fifo: &str) -> Result { + unsafe { + // SIGPIPE must stay an error value (EPIPE), never kill the relay. + sigaction( + NixSignal::SIGPIPE, + &SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty()), + ) + .context("sigaction SIGPIPE")?; + // No SA_RESTART: blocking calls return EINTR so the loops see STOP. + let stop = + SigAction::new(SigHandler::Handler(on_stop), SaFlags::empty(), SigSet::empty()); + sigaction(NixSignal::SIGINT, &stop).context("sigaction SIGINT")?; + sigaction(NixSignal::SIGTERM, &stop).context("sigaction SIGTERM")?; + } + + match mkfifo(fifo, NixMode::from_bits_truncate(0o644)) { + Ok(()) => {} + Err(Errno::EEXIST) => { + let meta = std::fs::metadata(fifo).with_context(|| format!("stat {fifo}"))?; + if !meta.file_type().is_fifo() { + bail!("{fifo} exists and is not a FIFO"); + } + } + Err(e) => return Err(e).with_context(|| format!("mkfifo {fifo}")), + } + let mut log = File::open(log_path).with_context(|| format!("open {log_path}"))?; + eprintln!("pmon: tail ready (fifo {fifo})"); + + let mut use_splice = true; + while !stop_requested() { + let Some(w) = open_writer(fifo)? else { + break; // stop requested while waiting for a reader + }; + let mut attached = true; + while !stop_requested() && attached { + let step = + if use_splice { relay_splice(&log, &w)? } else { relay_rw(&mut log, &w)? }; + match step { + Relay::Moved => {} + Relay::Idle => std::thread::sleep(POLL_TICK), + Relay::NoSplice => use_splice = false, + Relay::Detached => { + println!("pmon: tail reader detached"); + attached = false; + } + } + } + } + println!("pmon: exiting (signal)"); + Ok(0) +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +fn usage() -> i32 { + eprint!( + "usage: pmon \n\ + \x20 run -- CMD [ARGS...] spawn CMD, wait, mirror its exit\n\ + \x20 supervise [--engine pidfd|sigchld] [--max-restarts N] [--timeout-ms T]\n\ + \x20 [--log FILE] -- CMD [ARGS...] restart CMD on abnormal exit;\n\ + \x20 capture stdout/stderr into FILE\n\ + \x20 (defaults: pidfd, N=3, T=10000,\n\ + \x20 FILE=pmon.log)\n\ + \x20 tail --log FILE --fifo PATH relay appended log lines into a\n\ + \x20 FIFO created at PATH\n" + ); + 2 +} + +fn real_main() -> i32 { + let args: Vec = std::env::args().skip(1).collect(); + if args.is_empty() { + return usage(); + } + + if args[0] == "tail" { + let mut log_path = String::new(); + let mut fifo = String::new(); + let mut i = 1; + while i + 1 < args.len() { + match args[i].as_str() { + "--log" => log_path = args[i + 1].clone(), + "--fifo" => fifo = args[i + 1].clone(), + _ => return usage(), + } + i += 2; + } + if i != args.len() || log_path.is_empty() || fifo.is_empty() { + return usage(); + } + return match cmd_tail(&log_path, &fifo) { + Ok(code) => code, + Err(e) => { + eprintln!("pmon: {e:#}"); + 1 + } + }; + } + + let Some(sep) = args.iter().position(|a| a == "--") else { + return usage(); // run and supervise need `-- CMD [ARGS...]` + }; + if sep + 1 >= args.len() { + return usage(); + } + let flags = &args[1..sep]; + let cmdline = &args[sep + 1..]; + + let result = match args[0].as_str() { + "run" => { + if !flags.is_empty() { + return usage(); + } + cmd_run(cmdline) + } + "supervise" => { + let mut engine = Engine::Pidfd; + let mut max_restarts: u32 = 3; + let mut timeout_ms: u64 = 10_000; + let mut log_path = String::from("pmon.log"); + let mut it = flags.iter(); + loop { + let Some(flag) = it.next() else { break }; + let Some(value) = it.next() else { + return usage(); + }; + match flag.as_str() { + "--engine" => match value.as_str() { + "pidfd" => engine = Engine::Pidfd, + "sigchld" => engine = Engine::Sigchld, + _ => return usage(), + }, + "--max-restarts" => match value.parse::() { + Ok(n) => max_restarts = n, + Err(_) => return usage(), + }, + "--timeout-ms" => match value.parse::() { + Ok(t) if t > 0 => timeout_ms = t, + _ => return usage(), + }, + "--log" => log_path = value.clone(), + _ => return usage(), + } + } + cmd_supervise(engine, max_restarts, timeout_ms, &log_path, cmdline) + } + _ => return usage(), + }; + + match result { + Ok(code) => code, + Err(e) => { + eprintln!("pmon: {e:#}"); + 1 + } + } +} + +fn main() { + std::process::exit(real_main()); +} diff --git a/examples/18-pipes-fifos-splice/verify.lua b/examples/18-pipes-fifos-splice/verify.lua new file mode 100644 index 0000000..d42e6b1 --- /dev/null +++ b/examples/18-pipes-fifos-splice/verify.lua @@ -0,0 +1,178 @@ +-- Verify pmon v4 (18-pipes-fifos-splice) for LSP_LANG. +-- +-- Asserts observable behavior, identical across cpp/go/rust: +-- 1. usage errors exit 2 with the usage text +-- 2. v0 `run` still mirrors exits — status 3 stays 3, SIGKILL becomes 137 +-- with "killed signal=9" +-- 3. v2 pidfd engine + v4 capture: a child that fails once and then +-- succeeds yields two "engine=pidfd child=... pidfd=..." spawn lines, +-- "restart 1/3", exit 0 — and the log holds the child's stdout AND +-- stderr as "[out] ..."/"[err] ..." lines from both runs +-- 4. v1 sigchld engine still works with the capture attached: same restart +-- shape, no "pidfd=" anywhere, "giving up" exit 1, log filled +-- 5. v2 stop path survives the pipes: --timeout-ms TERMs the child +-- ("killed signal=15"), prints "exiting (timeout)", exit 0 +-- 6. v4 tail: the FIFO is created; a cat reader receives the log backlog +-- plus lines appended by a live supervise run; killing the cat makes +-- tail print "pmon: tail reader detached" ONCE and survive (kill -0); +-- a reattached cat receives the line appended while detached AND later +-- appends (nothing lost); SIGTERM ends tail with "exiting (signal)", +-- exit 0 +-- +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set; the interpreter may be lua or luajit. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" + +-- Scratch area for logs, FIFOs, and reader captures. +local wd = checks.run("mktemp -d").out:gsub("%s+$", "") +if wd == "" or not wd:match("^/") then + checks.skip("mktemp -d did not yield a directory") +end + +local function sh(script) + return checks.run(script:gsub("@WD@", wd)) +end + +-- --------------------------------------------------------------------------- +-- 1. usage shapes +-- --------------------------------------------------------------------------- + +local bogus = checks.run(demo .. " bogus") +checks.expect_exit(bogus, 2, lang .. ": unknown subcommand exits 2") +checks.expect_match(bogus.out, "usage: pmon", lang .. ": unknown subcommand prints usage") + +local nofifo = checks.run(demo .. " tail --log x.log") +checks.expect_exit(nofifo, 2, lang .. ": tail without --fifo exits 2") + +-- --------------------------------------------------------------------------- +-- 2. v0 run: exit mirroring + stdout passthrough +-- --------------------------------------------------------------------------- + +local r = sh(demo .. [[ run -- /bin/sh -c 'echo out-line; echo err-line >&2; exit 3']]) +checks.expect_exit(r, 3, lang .. ": run mirrors the child's exit status 3") +checks.expect_match(r.out, "pmon: run child=%d+", lang .. ": run announces the child pid") +checks.expect_match(r.out, "out%-line", lang .. ": run passes child stdout through") +checks.expect_match(r.out, "pmon: child=%d+ exited status=3", lang .. ": run reports the exit status") + +local sigd = sh(demo .. [[ run -- /bin/sh -c 'kill -KILL $$']]) +checks.expect_exit(sigd, 137, lang .. ": run exits 128+9 for a SIGKILLed child") +checks.expect_match(sigd.out, "pmon: child=%d+ killed signal=9", lang .. ": run reports the signal") + +-- --------------------------------------------------------------------------- +-- 3. pidfd engine + capture: two pipes into the log, restart on failure +-- --------------------------------------------------------------------------- + +local sup = sh([[ +set -e +rm -f @WD@/ok @WD@/pmon.log +]] .. demo .. [[ supervise --max-restarts 3 --log @WD@/pmon.log -- /bin/sh -c \ + 'echo run-out; echo run-err >&2; if [ -f @WD@/ok ]; then exit 0; else touch @WD@/ok; exit 1; fi' +]]) +checks.expect_exit(sup, 0, lang .. ": supervise exits 0 once the child succeeds") +checks.expect_match(sup.out, "pmon: engine=pidfd child=%d+ pidfd=%d+", + lang .. ": pidfd engine announces child and pidfd") +checks.expect_match(sup.out, "pmon: child=%d+ exited status=1", lang .. ": failing run reported") +checks.expect_match(sup.out, "pmon: restart 1/3", lang .. ": supervise restarts after failure") +checks.expect_match(sup.out, "pmon: child=%d+ exited status=0", lang .. ": clean run reported") +local _, starts = sup.out:gsub("pmon: engine=pidfd child=%d+ pidfd=%d+", "") +checks.expect_match("starts=" .. starts, "^starts=2$", lang .. ": exactly two child launches") + +local log = sh("cat @WD@/pmon.log") +checks.expect_exit(log, 0, lang .. ": supervise created the log file") +local _, outs = log.out:gsub("%[out%] run%-out", "") +local _, errs = log.out:gsub("%[err%] run%-err", "") +checks.expect_match(string.format("out=%d err=%d", outs, errs), "^out=2 err=2$", + lang .. ": log has [out]/[err] prefixed lines from both streams of both runs") + +-- --------------------------------------------------------------------------- +-- 4. sigchld engine still works with the capture attached +-- --------------------------------------------------------------------------- + +local sig = sh(demo .. + [[ supervise --engine sigchld --max-restarts 1 --log @WD@/sigchld.log -- /bin/sh -c 'echo sig-out; exit 5']]) +checks.expect_exit(sig, 1, lang .. ": sigchld engine exits 1 when restarts are exhausted") +checks.expect_match(sig.out, "pmon: engine=sigchld child=%d+", lang .. ": sigchld spawn line") +checks.expect_match(sig.out, "pmon: restart 1/1", lang .. ": sigchld restart line shape") +checks.expect_match(sig.out, "pmon: giving up after 1 restarts", lang .. ": sigchld gives up") +local _, npidfd = sig.out:gsub("pidfd=", "") +checks.expect_match("npidfd=" .. npidfd, "^npidfd=0$", + lang .. ": sigchld output has no pidfd= field") +local slog = sh("cat @WD@/sigchld.log") +local _, souts = slog.out:gsub("%[out%] sig%-out", "") +checks.expect_match("souts=" .. souts, "^souts=2$", + lang .. ": sigchld engine captured both runs into the log") + +-- --------------------------------------------------------------------------- +-- 5. the v2 stop path survives the pipes: timeout TERMs the child +-- --------------------------------------------------------------------------- + +local timeo = sh(demo .. [[ supervise --timeout-ms 500 --log @WD@/timeo.log -- sleep 30]]) +checks.expect_exit(timeo, 0, lang .. ": timeout stop path exits 0") +checks.expect_match(timeo.out, "pmon: child=%d+ killed signal=15", + lang .. ": supervised child is TERMed on timeout") +checks.expect_match(timeo.out, "pmon: exiting %(timeout%)", lang .. ": timeout exit line") + +-- --------------------------------------------------------------------------- +-- 6. tail: FIFO relay, reader detach survival, reattach without loss +-- --------------------------------------------------------------------------- + +-- The FIFO is fed from the pmon.log written by step 3; live appends flow +-- through a second real supervise run, then direct appends around the +-- detach/reattach churn. Every phase transition is polled, not slept blind. +local tail = sh([[ +wait_for() { i=0; while [ $i -lt 100 ]; do grep -qF "$2" "$1" 2>/dev/null && return 0; sleep 0.1; i=$((i+1)); done; return 1; } +rm -f @WD@/t.fifo @WD@/cat1.out @WD@/cat2.out @WD@/tail.out @WD@/tail.err +timeout 120 ]] .. demo .. [[ tail --log @WD@/pmon.log --fifo @WD@/t.fifo > @WD@/tail.out 2> @WD@/tail.err & +tpid=$! +i=0; while [ $i -lt 100 ]; do [ -p @WD@/t.fifo ] && break; sleep 0.1; i=$((i+1)); done +[ -p @WD@/t.fifo ] && echo "fifo-created" +cat @WD@/t.fifo > @WD@/cat1.out & +c1=$! +wait_for @WD@/cat1.out "[out] run-out" && echo "cat1-got-backlog" +rm -f @WD@/ok +]] .. demo .. [[ supervise --max-restarts 0 --log @WD@/pmon.log -- /bin/sh -c 'echo live-out; echo live-err >&2' >/dev/null 2>&1 +wait_for @WD@/cat1.out "[out] live-out" && wait_for @WD@/cat1.out "[err] live-err" && echo "cat1-got-live" +kill "$c1"; wait "$c1" 2>/dev/null +printf '[out] while-detached\n' >> @WD@/pmon.log +wait_for @WD@/tail.out "pmon: tail reader detached" && echo "tail-reported-detach" +if kill -0 "$tpid" 2>/dev/null; then echo "tail-survived"; fi +cat @WD@/t.fifo > @WD@/cat2.out & +c2=$! +wait_for @WD@/cat2.out "[out] while-detached" && echo "cat2-got-missed-line" +printf '[err] after-reattach\n' >> @WD@/pmon.log +wait_for @WD@/cat2.out "[err] after-reattach" && echo "cat2-got-new-line" +kill -TERM "$tpid" +wait "$tpid"; echo "tail-exit=$?" +wait "$c2" 2>/dev/null +echo "detach-count=$(grep -cF 'pmon: tail reader detached' @WD@/tail.out)" +echo "== tail.out =="; cat @WD@/tail.out +echo "== tail.err =="; cat @WD@/tail.err +]]) +checks.expect_exit(tail, 0, lang .. ": tail scenario script completes") +checks.expect_match(tail.out, "fifo%-created", lang .. ": tail creates the FIFO") +checks.expect_match(tail.out, "pmon: tail ready %(fifo ", lang .. ": tail announces readiness") +checks.expect_match(tail.out, "cat1%-got%-backlog", lang .. ": first reader receives the existing log lines") +checks.expect_match(tail.out, "cat1%-got%-live", lang .. + ": lines from a live supervise run flow supervise -> log -> tail -> FIFO -> cat") +checks.expect_match(tail.out, "tail%-reported%-detach", lang .. ": EPIPE is reported as reader detached") +checks.expect_match(tail.out, "tail%-survived", lang .. ": tail keeps running after the reader dies") +checks.expect_match(tail.out, "cat2%-got%-missed%-line", lang .. + ": line appended while detached reaches the reattached reader (no loss)") +checks.expect_match(tail.out, "cat2%-got%-new%-line", lang .. ": reattached reader receives new lines") +checks.expect_match(tail.out, "detach%-count=1", lang .. ": exactly one detach report") +checks.expect_match(tail.out, "tail%-exit=0", lang .. ": SIGTERM ends tail with exit 0") +checks.expect_match(tail.out, "pmon: exiting %(signal%)", lang .. ": tail prints the signal exit line") + +checks.run("rm -rf " .. wd) +checks.finish() diff --git a/examples/19-unix-sockets-fd-passing/README.md b/examples/19-unix-sockets-fd-passing/README.md new file mode 100644 index 0000000..0737cbf --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/README.md @@ -0,0 +1,90 @@ +# 19-unix-sockets-fd-passing — pmon v5 + +pmon grows a control plane. `pmon supervise` runs a child command under +supervision — own process group, stdout+stderr appended to a log file, +automatic restart 300 ms after every exit — and listens on a UNIX stream +socket. `pmon pmctl` (same binary, different subcommand) talks to it: + +``` +$ ./demo.sh cpp run supervise --ctl /tmp/p/ctl.sock --log /tmp/p/pmon.log -- \ + /bin/sh -c 'echo alpha; echo beta; echo gamma; exec sleep 30' & +pmon: listening on /tmp/p/ctl.sock +pmon: started child pid=2601772 + +$ ./demo.sh cpp run pmctl --ctl /tmp/p/ctl.sock status +child=2601772 uptime=4s restarts=0 +# supervisor stderr, from SO_PEERCRED on the accepted connection: +pmon: ctl connect uid=1000 pid=2601779 + +$ ./demo.sh cpp run pmctl --ctl /tmp/p/ctl.sock logfd +via-fd: alpha +via-fd: beta +via-fd: gamma + +$ ./demo.sh cpp run pmctl --ctl /tmp/p/ctl.sock stop +stopping +``` + +## What the chapter is really about + +`logfd` is the headline. The supervisor does not send the log *path* — it +sends its **open log file descriptor** through the socket in an `SCM_RIGHTS` +control message, and pmctl reads the last 3 lines directly off the received +fd (positional reads from offset 0, so the supervisor's `O_APPEND` writer is +undisturbed). The kernel installs a new descriptor in the receiving process +that shares the sender's open file description: an fd is a capability, and +UNIX sockets can hand capabilities between processes. `verify.lua` proves it +by diffing the `via-fd:` output against `tail -3` of the file, byte for byte. + +Two supporting ideas: + +- **SO_PEERCRED** — before trusting a single command byte, the supervisor + asks the kernel who is on the other end of the connection and logs + `pmon: ctl connect uid= pid=

`. Credentials from the kernel, not from + the client's own claims. +- **The log is opened O_RDWR|O_APPEND** — a write-only fd would arrive + write-only in pmctl (the *file description* crosses the socket, flags and + all), and the read side would fail with EBADF. + +## The three implementations + +Identical CLI, stdout/stderr shapes, and exit codes (0 ok / 1 runtime / 2 +usage). Same wire protocol: one command line in, one reply line (or a reply +plus cmsg) out. + +| | control messages | supervision concurrency | +|---|---|---| +| **C++23** | `sendmsg(2)`/`recvmsg(2)` with `CMSG_FIRSTHDR`/`CMSG_LEN`/`CMSG_DATA` built by hand | reaper on a `std::jthread`, state under mutex + condvar, `std::expected` for every fallible step, RAII `Fd` owns each descriptor | +| **Go 1.26** | `unix.UnixRights` / `unix.ParseSocketControlMessage` / `unix.ParseUnixRights` over `net.UnixConn.WriteMsgUnix`/`ReadMsgUnix` | reaper goroutine; restart backoff vs. shutdown is a `select` on a timer and a stop channel; wrapped errors end at `pmon: error:` | +| **Rust (2024)** | `nix::sys::socket` `sendmsg`/`recvmsg` with `ControlMessage::ScmRights` and `cmsg_space!` | reaper thread, `Arc<(Mutex, Condvar)>`; the received fd goes straight into an `OwnedFd` so it closes on drop | + +## Gotchas worth remembering + +- **`sun_path` is 108 bytes.** Bind/connect fail with `EINVAL`-flavored + errors when the socket path is longer (deep CI workspace paths hit this + immediately). Keep control sockets under short directories. +- Passing an fd duplicates the *open file description*, so offset and status + flags travel with it — hence O_RDWR above, and hence pmctl using `pread` + instead of seeking, which would move the shared offset. +- The child gets its own process group (`setpgid`), so `stop` can SIGTERM + the whole tree, including anything the child forked. +- Log lines are written with one `write(2)` each; `O_APPEND` makes every + append atomic even though supervisor and child share the file. + +## Files + +``` +demo.sh # dispatcher: ./demo.sh [cpp|go|rust|all|build] [args...] +verify.lua # behavioral checks (status fields, creds line, fd-passing tail, teardown) +cpp/ go/ rust/ # one pmon each, binary always ./…/app, demo.sh contract identical +``` + +## Verification + +`python3 scripts/test-all-examples.py --only 19-unix-sockets-fd-passing` +builds all three and runs `verify.lua` per language. The checks are +behavioral: status field shapes, a live `/proc/` running the +supervised command, the SO_PEERCRED uid equal to `id -u`, `via-fd:` output +identical to `tail -3` of the log, restart counting consistent between +status, stderr, and the log, and a stop that leaves no socket file and no +child process behind. diff --git a/examples/19-unix-sockets-fd-passing/cpp/.clang-format b/examples/19-unix-sockets-fd-passing/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/19-unix-sockets-fd-passing/cpp/.clang-tidy b/examples/19-unix-sockets-fd-passing/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/19-unix-sockets-fd-passing/cpp/CMakeLists.txt b/examples/19-unix-sockets-fd-passing/cpp/CMakeLists.txt new file mode 100644 index 0000000..c945061 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(pmon CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/19-unix-sockets-fd-passing/cpp/CMakePresets.json b/examples/19-unix-sockets-fd-passing/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/19-unix-sockets-fd-passing/cpp/demo.sh b/examples/19-unix-sockets-fd-passing/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/19-unix-sockets-fd-passing/cpp/src/main.cpp b/examples/19-unix-sockets-fd-passing/cpp/src/main.cpp new file mode 100644 index 0000000..c4e3729 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/cpp/src/main.cpp @@ -0,0 +1,588 @@ +// pmon v5 — a process supervisor with a UNIX-socket control plane. +// +// `pmon supervise` forks/execs a child command (its own process group, +// stdout+stderr appended to a log file), restarts it 300 ms after every +// exit, and serves a SOCK_STREAM control socket. `pmon pmctl` is the client +// side of the same binary: +// +// status -> "child= uptime=s restarts=" +// stop -> "stopping", then the supervisor tears down and exits 0 +// logfd -> the supervisor passes its OPEN log file descriptor through an +// SCM_RIGHTS control message; pmctl reads the last 3 lines +// straight off the received fd ("via-fd: ..."), never touching +// the path. That is the point of the chapter: file descriptors +// are capabilities you can hand across a UNIX socket. +// +// Every accepted connection is identified with SO_PEERCRED before a single +// command byte is trusted. The cmsg plumbing here is done by hand with +// sendmsg(2)/recvmsg(2) — CMSG_FIRSTHDR and friends, no wrapper library. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using std::chrono::duration_cast; +using std::chrono::seconds; +using std::chrono::steady_clock; + +constexpr auto kRestartBackoff = 300ms; +constexpr std::size_t kMaxLogRead = 1u << 20; // pmctl reads at most 1 MiB of log + +[[nodiscard]] std::string errno_text() { + return std::error_code{errno, std::system_category()}.message(); +} + +template +using Result = std::expected; + +// --------------------------------------------------------------------------- +// RAII file descriptor — the only owner of every fd in this program. +// --------------------------------------------------------------------------- + +class Fd { +public: + Fd() = default; + explicit Fd(int fd) : fd_{fd} {} + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + Fd(Fd&& other) noexcept : fd_{std::exchange(other.fd_, -1)} {} + Fd& operator=(Fd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = std::exchange(other.fd_, -1); + } + return *this; + } + ~Fd() { reset(); } + + void reset() noexcept { + if (fd_ >= 0) { + ::close(fd_); + } + fd_ = -1; + } + [[nodiscard]] int get() const noexcept { return fd_; } + [[nodiscard]] bool valid() const noexcept { return fd_ >= 0; } + +private: + int fd_ = -1; +}; + +// --------------------------------------------------------------------------- +// Socket + log plumbing, each step a std::expected +// --------------------------------------------------------------------------- + +Result open_log(const std::string& path) { + // O_RDWR, not O_WRONLY: the fd handed out via SCM_RIGHTS shares this open + // file description, and pmctl must be able to read from it. + Fd fd{::open(path.c_str(), O_CREAT | O_RDWR | O_APPEND | O_CLOEXEC, 0644)}; + if (!fd.valid()) { + return std::unexpected(std::format("open {}: {}", path, errno_text())); + } + return fd; +} + +Result unix_addr(const std::string& path) { + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + if (path.size() >= sizeof(addr.sun_path)) { + return std::unexpected(std::format("{}: socket path too long", path)); + } + std::ranges::copy(path, addr.sun_path); + return addr; +} + +Result listen_ctl(const std::string& path) { + ::unlink(path.c_str()); // stale socket from a previous run; ENOENT is fine + Fd fd{::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)}; + if (!fd.valid()) { + return std::unexpected(std::format("socket: {}", errno_text())); + } + auto addr = unix_addr(path); + if (!addr) { + return std::unexpected(addr.error()); + } + if (::bind(fd.get(), reinterpret_cast(&*addr), sizeof(*addr)) < 0) { + return std::unexpected(std::format("bind {}: {}", path, errno_text())); + } + if (::listen(fd.get(), 8) < 0) { + return std::unexpected(std::format("listen {}: {}", path, errno_text())); + } + return fd; +} + +Result connect_ctl(const std::string& path) { + Fd fd{::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)}; + if (!fd.valid()) { + return std::unexpected(std::format("socket: {}", errno_text())); + } + auto addr = unix_addr(path); + if (!addr) { + return std::unexpected(addr.error()); + } + if (::connect(fd.get(), reinterpret_cast(&*addr), sizeof(*addr)) < 0) { + return std::unexpected(std::format("connect {}: {}", path, errno_text())); + } + return fd; +} + +void write_all(int fd, std::string_view text) { + std::size_t off = 0; + while (off < text.size()) { + ssize_t n = ::write(fd, text.data() + off, text.size() - off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return; // peer gone; nothing sensible to do with a reply error + } + off += static_cast(n); + } +} + +// One write(2) per line: O_APPEND makes each line an atomic append even while +// the child writes to the same file. +void log_line(int log_fd, std::string_view line) { + write_all(log_fd, std::format("{}\n", line)); +} + +// --------------------------------------------------------------------------- +// Child lifecycle +// --------------------------------------------------------------------------- + +Result spawn_child(const std::vector& argv, int log_fd) { + std::vector cargv; + cargv.reserve(argv.size() + 1); + for (const auto& a : argv) { + cargv.push_back(const_cast(a.c_str())); + } + cargv.push_back(nullptr); + + pid_t pid = ::fork(); + if (pid < 0) { + return std::unexpected(std::format("fork: {}", errno_text())); + } + if (pid == 0) { + ::setpgid(0, 0); // own process group, so stop can signal the whole tree + ::dup2(log_fd, STDOUT_FILENO); + ::dup2(log_fd, STDERR_FILENO); + ::execvp(cargv[0], cargv.data()); + std::println(stderr, "pmon: exec {}: {}", argv[0], errno_text()); // lands in the log + ::_exit(127); + } + return pid; +} + +struct State { + std::mutex m; + std::condition_variable cv; + pid_t child = -1; + steady_clock::time_point started{}; + int restarts = 0; + bool stopping = false; + bool child_done = false; // reaper has collected the final child +}; + +[[nodiscard]] int exit_code_of(int wstatus) { + if (WIFEXITED(wstatus)) { + return WEXITSTATUS(wstatus); + } + if (WIFSIGNALED(wstatus)) { + return 128 + WTERMSIG(wstatus); + } + return 1; +} + +// Reaper thread: waitpid the current child; unless we are stopping, back off +// 300 ms and respawn. Ends by reporting child_done through the cv. +void reaper(State& s, const std::vector& argv, int log_fd) { + pid_t pid = 0; + { + std::lock_guard lk(s.m); + pid = s.child; + } + auto finish = [&s] { + std::lock_guard lk(s.m); + s.child_done = true; + s.cv.notify_all(); + }; + for (;;) { + int wstatus = 0; + if (::waitpid(pid, &wstatus, 0) < 0) { + if (errno == EINTR) { + continue; + } + finish(); + return; + } + { + std::lock_guard lk(s.m); + if (s.stopping) { + s.child_done = true; + s.cv.notify_all(); + return; + } + } + std::println(stderr, "pmon: child pid={} exited status={}", pid, exit_code_of(wstatus)); + { + std::unique_lock lk(s.m); + if (s.cv.wait_for(lk, kRestartBackoff, [&s] { return s.stopping; })) { + s.child_done = true; + s.cv.notify_all(); + return; + } + } + auto next = spawn_child(argv, log_fd); + if (!next) { + std::println(stderr, "pmon: error: {}", next.error()); + finish(); + return; + } + int restarts_now = 0; + bool stop_now = false; + { + std::lock_guard lk(s.m); + stop_now = s.stopping; + if (!stop_now) { + s.child = *next; + s.started = steady_clock::now(); + restarts_now = ++s.restarts; + } + } + if (stop_now) { // stop raced with the respawn: undo it + ::kill(-*next, SIGTERM); + ::waitpid(*next, nullptr, 0); + finish(); + return; + } + std::println(stderr, "pmon: restart {} child pid={}", restarts_now, *next); + log_line(log_fd, std::format("pmon: start child pid={}", *next)); + pid = *next; + } +} + +// --------------------------------------------------------------------------- +// SCM_RIGHTS, by hand +// --------------------------------------------------------------------------- + +void send_fd(int sock, int fd) { + char payload[] = "ok\n"; + iovec iov{payload, 3}; + alignas(cmsghdr) char ctrl[CMSG_SPACE(sizeof(int))]{}; + msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + cmsghdr* cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SCM_RIGHTS; + cm->cmsg_len = CMSG_LEN(sizeof(int)); + std::memcpy(CMSG_DATA(cm), &fd, sizeof(int)); + if (::sendmsg(sock, &msg, 0) < 0) { + std::println(stderr, "pmon: sendmsg: {}", errno_text()); + } +} + +Result recv_fd(int sock) { + char payload[16]; + iovec iov{payload, sizeof(payload)}; + alignas(cmsghdr) char ctrl[CMSG_SPACE(sizeof(int))]{}; + msghdr msg{}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = ctrl; + msg.msg_controllen = sizeof(ctrl); + ssize_t n = ::recvmsg(sock, &msg, MSG_CMSG_CLOEXEC); + if (n < 0) { + return std::unexpected(std::format("recvmsg: {}", errno_text())); + } + for (cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm != nullptr; cm = CMSG_NXTHDR(&msg, cm)) { + if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { + int fd = -1; + std::memcpy(&fd, CMSG_DATA(cm), sizeof(int)); + return Fd{fd}; + } + } + return std::unexpected("no SCM_RIGHTS control message in reply"); +} + +// --------------------------------------------------------------------------- +// supervise +// --------------------------------------------------------------------------- + +std::string read_command(int conn) { + std::string cmd; + while (cmd.size() < 64) { + char ch = 0; + ssize_t n = ::read(conn, &ch, 1); + if (n < 0 && errno == EINTR) { + continue; + } + if (n <= 0 || ch == '\n') { + break; + } + cmd.push_back(ch); + } + return cmd; +} + +int cmd_supervise(const std::string& ctl, const std::string& log_path, + const std::vector& child_argv) { + auto log = open_log(log_path); + if (!log) { + std::println(stderr, "pmon: error: {}", log.error()); + return 1; + } + auto lsock = listen_ctl(ctl); + if (!lsock) { + std::println(stderr, "pmon: error: {}", lsock.error()); + return 1; + } + std::println(stderr, "pmon: listening on {}", ctl); + + auto first = spawn_child(child_argv, log->get()); + if (!first) { + std::println(stderr, "pmon: error: {}", first.error()); + ::unlink(ctl.c_str()); + return 1; + } + std::println(stderr, "pmon: started child pid={}", *first); + log_line(log->get(), std::format("pmon: start child pid={}", *first)); + + State s; + s.child = *first; + s.started = steady_clock::now(); + std::jthread reap([&s, &child_argv, log_fd = log->get()] { reaper(s, child_argv, log_fd); }); + + for (;;) { + Fd conn{::accept4(lsock->get(), nullptr, nullptr, SOCK_CLOEXEC)}; + if (!conn.valid()) { + if (errno == EINTR) { + continue; + } + std::println(stderr, "pmon: error: accept: {}", errno_text()); + return 1; + } + + ucred cred{}; + socklen_t cred_len = sizeof(cred); + if (::getsockopt(conn.get(), SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) == 0) { + std::println(stderr, "pmon: ctl connect uid={} pid={}", cred.uid, cred.pid); + } + + const std::string cmd = read_command(conn.get()); + if (cmd == "status") { + std::lock_guard lk(s.m); + auto up = duration_cast(steady_clock::now() - s.started).count(); + write_all(conn.get(), + std::format("child={} uptime={}s restarts={}\n", s.child, up, s.restarts)); + } else if (cmd == "logfd") { + send_fd(conn.get(), log->get()); + } else if (cmd == "stop") { + write_all(conn.get(), "stopping\n"); + conn.reset(); // deliver the reply before tearing down + std::println(stderr, "pmon: stopping"); + pid_t child = -1; + { + std::lock_guard lk(s.m); + s.stopping = true; + child = s.child; + } + s.cv.notify_all(); + if (child > 0) { + ::kill(-child, SIGTERM); // whole process group; ESRCH is fine + } + { + std::unique_lock lk(s.m); + s.cv.wait(lk, [&s] { return s.child_done; }); + } + ::unlink(ctl.c_str()); + return 0; + } else { + write_all(conn.get(), "err unknown command\n"); + } + } +} + +// --------------------------------------------------------------------------- +// pmctl +// --------------------------------------------------------------------------- + +Result> log_tail_via_fd(int fd, std::size_t count) { + std::string data; + off_t off = 0; + char buf[4096]; + while (data.size() < kMaxLogRead) { + ssize_t n = ::pread(fd, buf, sizeof(buf), off); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return std::unexpected(std::format("pread: {}", errno_text())); + } + if (n == 0) { + break; + } + data.append(buf, static_cast(n)); + off += n; + } + if (!data.empty() && data.back() == '\n') { + data.pop_back(); + } + std::vector lines; + if (data.empty()) { + return lines; + } + std::size_t pos = 0; + for (;;) { + std::size_t nl = data.find('\n', pos); + if (nl == std::string::npos) { + lines.emplace_back(data.substr(pos)); + break; + } + lines.emplace_back(data.substr(pos, nl - pos)); + pos = nl + 1; + } + if (lines.size() > count) { + lines.erase(lines.begin(), lines.end() - static_cast(count)); + } + return lines; +} + +int cmd_pmctl(const std::string& ctl, const std::string& action) { + auto conn = connect_ctl(ctl); + if (!conn) { + std::println(stderr, "pmctl: error: {}", conn.error()); + return 1; + } + write_all(conn->get(), std::format("{}\n", action)); + + if (action == "logfd") { + auto fd = recv_fd(conn->get()); + if (!fd) { + std::println(stderr, "pmctl: error: {}", fd.error()); + return 1; + } + auto lines = log_tail_via_fd(fd->get(), 3); + if (!lines) { + std::println(stderr, "pmctl: error: {}", lines.error()); + return 1; + } + for (const auto& line : *lines) { + std::println("via-fd: {}", line); + } + return 0; + } + + // status / stop: the supervisor replies with one line and closes. + std::string reply; + char buf[256]; + for (;;) { + ssize_t n = ::read(conn->get(), buf, sizeof(buf)); + if (n < 0 && errno == EINTR) { + continue; + } + if (n <= 0) { + break; + } + reply.append(buf, static_cast(n)); + } + while (!reply.empty() && reply.back() == '\n') { + reply.pop_back(); + } + if (reply.empty()) { + std::println(stderr, "pmctl: error: empty reply from supervisor"); + return 1; + } + if (reply.starts_with("err ")) { + std::println(stderr, "pmctl: error: {}", reply.substr(4)); + return 1; + } + std::println("{}", reply); + return 0; +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +int usage() { + std::println(stderr, "usage: pmon supervise --ctl SOCK --log FILE -- CMD [ARG...]"); + std::println(stderr, " pmon pmctl --ctl SOCK "); + return 2; +} + +} // namespace + +int main(int argc, char** argv) { + std::vector args(argv + 1, argv + argc); + if (args.empty()) { + return usage(); + } + + if (args[0] == "supervise") { + std::string ctl; + std::string log; + std::vector child; + for (std::size_t i = 1; i < args.size(); ++i) { + if (args[i] == "--ctl" && i + 1 < args.size()) { + ctl = args[++i]; + } else if (args[i] == "--log" && i + 1 < args.size()) { + log = args[++i]; + } else if (args[i] == "--") { + child.assign(args.begin() + static_cast(i) + 1, args.end()); + break; + } else { + return usage(); + } + } + if (ctl.empty() || log.empty() || child.empty()) { + return usage(); + } + return cmd_supervise(ctl, log, child); + } + + if (args[0] == "pmctl") { + std::string ctl; + std::string action; + for (std::size_t i = 1; i < args.size(); ++i) { + if (args[i] == "--ctl" && i + 1 < args.size()) { + ctl = args[++i]; + } else if (action.empty()) { + action = args[i]; + } else { + return usage(); + } + } + if (ctl.empty() || (action != "status" && action != "stop" && action != "logfd")) { + return usage(); + } + return cmd_pmctl(ctl, action); + } + + return usage(); +} diff --git a/examples/19-unix-sockets-fd-passing/demo.sh b/examples/19-unix-sockets-fd-passing/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/19-unix-sockets-fd-passing/go/demo.sh b/examples/19-unix-sockets-fd-passing/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/19-unix-sockets-fd-passing/go/go.mod b/examples/19-unix-sockets-fd-passing/go/go.mod new file mode 100644 index 0000000..9d7da77 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/19-unix-sockets-fd-passing/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/19-unix-sockets-fd-passing/go/go.sum b/examples/19-unix-sockets-fd-passing/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/19-unix-sockets-fd-passing/go/main.go b/examples/19-unix-sockets-fd-passing/go/main.go new file mode 100644 index 0000000..f44bbdd --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/go/main.go @@ -0,0 +1,403 @@ +// pmon v5 — a process supervisor with a UNIX-socket control plane. +// +// `pmon supervise` spawns a child command (its own process group, +// stdout+stderr appended to a log file), restarts it 300 ms after every +// exit, and serves a SOCK_STREAM control socket. `pmon pmctl` is the client +// side of the same binary: +// +// status -> "child= uptime=s restarts=" +// stop -> "stopping", then the supervisor tears down and exits 0 +// logfd -> the supervisor passes its OPEN log file descriptor through an +// SCM_RIGHTS control message; pmctl reads the last 3 lines +// straight off the received fd ("via-fd: ..."), never touching +// the path. +// +// Where C++ and Rust build the cmsg by hand / via nix, Go rides +// net.UnixConn: WriteMsgUnix with unix.UnixRights on the way out, +// ReadMsgUnix + unix.ParseSocketControlMessage + unix.ParseUnixRights on the +// way in. The reaper is a goroutine; restart backoff and stop are a select. +package main + +import ( + "errors" + "fmt" + "net" + "os" + "os/exec" + "strings" + "sync" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +const ( + restartBackoff = 300 * time.Millisecond + maxLogRead = 1 << 20 // pmctl reads at most 1 MiB of log +) + +func usage() int { + fmt.Fprintln(os.Stderr, "usage: pmon supervise --ctl SOCK --log FILE -- CMD [ARG...]") + fmt.Fprintln(os.Stderr, " pmon pmctl --ctl SOCK ") + return 2 +} + +// --------------------------------------------------------------------------- +// Child lifecycle +// --------------------------------------------------------------------------- + +type state struct { + mu sync.Mutex + child int // pid of the current child + started time.Time + restarts int + stopping bool +} + +// spawnChild starts argv in its own process group with stdout+stderr going to +// the (O_APPEND) log file — the fd is inherited directly, no pipe goroutine. +func spawnChild(argv []string, logFile *os.File) (*exec.Cmd, error) { + cmd := exec.Command(argv[0], argv[1:]...) + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("exec %s: %w", argv[0], err) + } + return cmd, nil +} + +func exitCodeOf(err error) int { + var ee *exec.ExitError + if errors.As(err, &ee) { + if ws, ok := ee.Sys().(syscall.WaitStatus); ok { + if ws.Signaled() { + return 128 + int(ws.Signal()) + } + return ws.ExitStatus() + } + } + if err != nil { + return 1 + } + return 0 +} + +func logLine(logFile *os.File, line string) { + // One write per line: O_APPEND makes it an atomic append. + _, _ = logFile.WriteString(line + "\n") +} + +// reaper waits for the current child; unless we are stopping, backs off +// 300 ms (or bails on stopCh) and respawns. Closes done when the final child +// is collected. +func reaper(s *state, cmd *exec.Cmd, argv []string, logFile *os.File, stopCh <-chan struct{}, done chan<- struct{}) { + defer close(done) + for { + pid := cmd.Process.Pid + err := cmd.Wait() + s.mu.Lock() + stopping := s.stopping + s.mu.Unlock() + if stopping { + return + } + fmt.Fprintf(os.Stderr, "pmon: child pid=%d exited status=%d\n", pid, exitCodeOf(err)) + select { + case <-stopCh: + return + case <-time.After(restartBackoff): + } + next, serr := spawnChild(argv, logFile) + if serr != nil { + fmt.Fprintf(os.Stderr, "pmon: error: %v\n", serr) + return + } + s.mu.Lock() + stopping = s.stopping + var restartsNow int + if !stopping { + s.child = next.Process.Pid + s.started = time.Now() + s.restarts++ + restartsNow = s.restarts + } + s.mu.Unlock() + if stopping { // stop raced with the respawn: undo it + _ = unix.Kill(-next.Process.Pid, unix.SIGTERM) + _ = next.Wait() + return + } + fmt.Fprintf(os.Stderr, "pmon: restart %d child pid=%d\n", restartsNow, next.Process.Pid) + logLine(logFile, fmt.Sprintf("pmon: start child pid=%d", next.Process.Pid)) + cmd = next + } +} + +// --------------------------------------------------------------------------- +// supervise +// --------------------------------------------------------------------------- + +func peerCreds(conn *net.UnixConn) { + raw, err := conn.SyscallConn() + if err != nil { + return + } + _ = raw.Control(func(fd uintptr) { + cred, cerr := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) + if cerr == nil { + fmt.Fprintf(os.Stderr, "pmon: ctl connect uid=%d pid=%d\n", cred.Uid, cred.Pid) + } + }) +} + +func readCommand(conn *net.UnixConn) string { + var cmd []byte + one := make([]byte, 1) + for len(cmd) < 64 { + n, err := conn.Read(one) + if n <= 0 || err != nil || one[0] == '\n' { + break + } + cmd = append(cmd, one[0]) + } + return string(cmd) +} + +func cmdSupervise(ctl, logPath string, childArgv []string) error { + // O_RDWR, not O_WRONLY: the fd handed out via SCM_RIGHTS shares this open + // file description, and pmctl must be able to read from it. + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644) + if err != nil { + return fmt.Errorf("open %s: %w", logPath, err) + } + defer logFile.Close() + + _ = os.Remove(ctl) // stale socket from a previous run + listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: ctl, Net: "unix"}) + if err != nil { + return fmt.Errorf("listen %s: %w", ctl, err) + } + // The stop path removes the socket explicitly; keep Go from doing it early. + listener.SetUnlinkOnClose(false) + defer listener.Close() + fmt.Fprintf(os.Stderr, "pmon: listening on %s\n", ctl) + + first, err := spawnChild(childArgv, logFile) + if err != nil { + _ = os.Remove(ctl) + return err + } + fmt.Fprintf(os.Stderr, "pmon: started child pid=%d\n", first.Process.Pid) + logLine(logFile, fmt.Sprintf("pmon: start child pid=%d", first.Process.Pid)) + + s := &state{child: first.Process.Pid, started: time.Now()} + stopCh := make(chan struct{}) + done := make(chan struct{}) + go reaper(s, first, childArgv, logFile, stopCh, done) + + for { + conn, aerr := listener.AcceptUnix() + if aerr != nil { + return fmt.Errorf("accept: %w", aerr) + } + peerCreds(conn) + cmd := readCommand(conn) + switch cmd { + case "status": + s.mu.Lock() + reply := fmt.Sprintf("child=%d uptime=%ds restarts=%d\n", + s.child, int(time.Since(s.started)/time.Second), s.restarts) + s.mu.Unlock() + _, _ = conn.Write([]byte(reply)) + conn.Close() + case "logfd": + rights := unix.UnixRights(int(logFile.Fd())) + if _, _, werr := conn.WriteMsgUnix([]byte("ok\n"), rights, nil); werr != nil { + fmt.Fprintf(os.Stderr, "pmon: sendmsg: %v\n", werr) + } + conn.Close() + case "stop": + _, _ = conn.Write([]byte("stopping\n")) + conn.Close() // deliver the reply before tearing down + fmt.Fprintln(os.Stderr, "pmon: stopping") + s.mu.Lock() + s.stopping = true + child := s.child + s.mu.Unlock() + close(stopCh) + if child > 0 { + _ = unix.Kill(-child, unix.SIGTERM) // whole process group; ESRCH is fine + } + <-done + _ = os.Remove(ctl) + return nil + default: + _, _ = conn.Write([]byte("err unknown command\n")) + conn.Close() + } + } +} + +// --------------------------------------------------------------------------- +// pmctl +// --------------------------------------------------------------------------- + +func recvLogFd(conn *net.UnixConn) (int, error) { + buf := make([]byte, 16) + oob := make([]byte, unix.CmsgSpace(4)) + _, oobn, _, _, err := conn.ReadMsgUnix(buf, oob) + if err != nil { + return -1, fmt.Errorf("recvmsg: %w", err) + } + msgs, err := unix.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return -1, fmt.Errorf("parse cmsg: %w", err) + } + for _, m := range msgs { + fds, ferr := unix.ParseUnixRights(&m) + if ferr == nil && len(fds) > 0 { + return fds[0], nil + } + } + return -1, errors.New("no SCM_RIGHTS control message in reply") +} + +func logTailViaFd(fd int, count int) ([]string, error) { + var data []byte + chunk := make([]byte, 4096) + var off int64 + for len(data) < maxLogRead { + n, err := unix.Pread(fd, chunk, off) + if err != nil { + return nil, fmt.Errorf("pread: %w", err) + } + if n == 0 { + break + } + data = append(data, chunk[:n]...) + off += int64(n) + } + text := strings.TrimSuffix(string(data), "\n") + if text == "" { + return nil, nil + } + lines := strings.Split(text, "\n") + if len(lines) > count { + lines = lines[len(lines)-count:] + } + return lines, nil +} + +func cmdPmctl(ctl, action string) error { + addr := &net.UnixAddr{Name: ctl, Net: "unix"} + conn, err := net.DialUnix("unix", nil, addr) + if err != nil { + return fmt.Errorf("connect %s: %w", ctl, err) + } + defer conn.Close() + if _, err := conn.Write([]byte(action + "\n")); err != nil { + return fmt.Errorf("write %s: %w", ctl, err) + } + + if action == "logfd" { + fd, ferr := recvLogFd(conn) + if ferr != nil { + return ferr + } + defer unix.Close(fd) + lines, terr := logTailViaFd(fd, 3) + if terr != nil { + return terr + } + for _, line := range lines { + fmt.Printf("via-fd: %s\n", line) + } + return nil + } + + // status / stop: the supervisor replies with one line and closes. + var reply []byte + buf := make([]byte, 256) + for { + n, rerr := conn.Read(buf) + if n > 0 { + reply = append(reply, buf[:n]...) + } + if rerr != nil { + break + } + } + text := strings.TrimRight(string(reply), "\n") + if text == "" { + return errors.New("empty reply from supervisor") + } + if strings.HasPrefix(text, "err ") { + return errors.New(strings.TrimPrefix(text, "err ")) + } + fmt.Println(text) + return nil +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +func main() { + args := os.Args[1:] + if len(args) == 0 { + os.Exit(usage()) + } + + switch args[0] { + case "supervise": + var ctl, logPath string + var child []string + i := 1 + for i < len(args) { + switch { + case args[i] == "--ctl" && i+1 < len(args): + ctl = args[i+1] + i += 2 + case args[i] == "--log" && i+1 < len(args): + logPath = args[i+1] + i += 2 + case args[i] == "--": + child = args[i+1:] + i = len(args) + default: + os.Exit(usage()) + } + } + if ctl == "" || logPath == "" || len(child) == 0 { + os.Exit(usage()) + } + if err := cmdSupervise(ctl, logPath, child); err != nil { + fmt.Fprintf(os.Stderr, "pmon: error: %v\n", err) + os.Exit(1) + } + case "pmctl": + var ctl, action string + for i := 1; i < len(args); i++ { + switch { + case args[i] == "--ctl" && i+1 < len(args): + ctl = args[i+1] + i++ + case action == "": + action = args[i] + default: + os.Exit(usage()) + } + } + if ctl == "" || (action != "status" && action != "stop" && action != "logfd") { + os.Exit(usage()) + } + if err := cmdPmctl(ctl, action); err != nil { + fmt.Fprintf(os.Stderr, "pmctl: error: %v\n", err) + os.Exit(1) + } + default: + os.Exit(usage()) + } +} diff --git a/examples/19-unix-sockets-fd-passing/rust/Cargo.lock b/examples/19-unix-sockets-fd-passing/rust/Cargo.lock new file mode 100644 index 0000000..dbd2fd6 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/rust/Cargo.lock @@ -0,0 +1,69 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "pmon" +version = "0.5.0" +dependencies = [ + "anyhow", + "nix", +] diff --git a/examples/19-unix-sockets-fd-passing/rust/Cargo.toml b/examples/19-unix-sockets-fd-passing/rust/Cargo.toml new file mode 100644 index 0000000..87af445 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pmon" +version = "0.5.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +nix = { version = "0.31", features = ["socket", "signal", "process", "uio"] } diff --git a/examples/19-unix-sockets-fd-passing/rust/demo.sh b/examples/19-unix-sockets-fd-passing/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/19-unix-sockets-fd-passing/rust/rust-toolchain.toml b/examples/19-unix-sockets-fd-passing/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/19-unix-sockets-fd-passing/rust/src/main.rs b/examples/19-unix-sockets-fd-passing/rust/src/main.rs new file mode 100644 index 0000000..9b81124 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/rust/src/main.rs @@ -0,0 +1,420 @@ +// pmon v5 — a process supervisor with a UNIX-socket control plane. +// +// `pmon supervise` spawns a child command (its own process group, +// stdout+stderr appended to a log file), restarts it 300 ms after every +// exit, and serves a SOCK_STREAM control socket. `pmon pmctl` is the client +// side of the same binary: +// +// status -> "child= uptime=s restarts=" +// stop -> "stopping", then the supervisor tears down and exits 0 +// logfd -> the supervisor passes its OPEN log file descriptor through an +// SCM_RIGHTS control message; pmctl reads the last 3 lines +// straight off the received fd ("via-fd: ..."), never touching +// the path. +// +// The cmsg plumbing is nix::sys::socket sendmsg/recvmsg with +// ControlMessage::ScmRights — explicit, not a convenience crate — and the +// received fd is wrapped in an OwnedFd immediately so it closes on drop. + +use std::fs::File; +use std::io::{IoSlice, IoSliceMut, Read, Write}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use std::os::unix::fs::FileExt; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::os::unix::process::{CommandExt, ExitStatusExt}; +use std::process::{Child, Command, ExitCode, Stdio}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, anyhow, bail}; +use nix::sys::signal::{Signal, kill}; +use nix::sys::socket::sockopt::PeerCredentials; +use nix::sys::socket::{ + ControlMessage, ControlMessageOwned, MsgFlags, getsockopt, recvmsg, sendmsg, +}; +use nix::unistd::Pid; + +const RESTART_BACKOFF: Duration = Duration::from_millis(300); +const MAX_LOG_READ: u64 = 1 << 20; // pmctl reads at most 1 MiB of log + +fn usage() -> ExitCode { + eprintln!("usage: pmon supervise --ctl SOCK --log FILE -- CMD [ARG...]"); + eprintln!(" pmon pmctl --ctl SOCK "); + ExitCode::from(2) +} + +// --------------------------------------------------------------------------- +// Child lifecycle +// --------------------------------------------------------------------------- + +struct State { + child: i32, // pid of the current child + started: Instant, + restarts: u32, + stopping: bool, + child_done: bool, // reaper has collected the final child +} + +type Shared = Arc<(Mutex, Condvar)>; + +/// Spawn argv in its own process group with stdout+stderr appended to the +/// log file — the fd is inherited directly, no pipe thread. +fn spawn_child(argv: &[String], log: &File) -> Result { + let mut cmd = Command::new(&argv[0]); + cmd.args(&argv[1..]) + .stdout(Stdio::from(log.try_clone().context("dup log fd")?)) + .stderr(Stdio::from(log.try_clone().context("dup log fd")?)) + .process_group(0); // own process group, so stop can signal the whole tree + cmd.spawn().with_context(|| format!("exec {}", argv[0])) +} + +fn exit_code_of(status: std::process::ExitStatus) -> i32 { + if let Some(code) = status.code() { + code + } else { + 128 + status.signal().unwrap_or(1) + } +} + +/// One write per line: O_APPEND makes it an atomic append. +fn log_line(mut log: &File, line: &str) { + let _ = log.write_all(format!("{line}\n").as_bytes()); +} + +fn mark_done(shared: &Shared) { + let (lock, cv) = &**shared; + lock.lock().unwrap().child_done = true; + cv.notify_all(); +} + +/// Reaper thread: wait for the current child; unless we are stopping, back +/// off 300 ms and respawn. Ends by reporting child_done through the condvar. +fn reaper(shared: Shared, mut child: Child, argv: Vec, log: File) { + let (lock, cv) = &*shared; + loop { + let pid = child.id() as i32; + let status = match child.wait() { + Ok(status) => status, + Err(err) => { + eprintln!("pmon: error: waitpid: {err}"); + mark_done(&shared); + return; + } + }; + if lock.lock().unwrap().stopping { + mark_done(&shared); + return; + } + eprintln!( + "pmon: child pid={} exited status={}", + pid, + exit_code_of(status) + ); + { + let guard = lock.lock().unwrap(); + let (guard, _timed_out) = cv + .wait_timeout_while(guard, RESTART_BACKOFF, |s| !s.stopping) + .unwrap(); + if guard.stopping { + drop(guard); + mark_done(&shared); + return; + } + } + let next = match spawn_child(&argv, &log) { + Ok(next) => next, + Err(err) => { + eprintln!("pmon: error: {err:#}"); + mark_done(&shared); + return; + } + }; + let npid = next.id() as i32; + let mut restarts_now = 0; + let stop_now = { + let mut s = lock.lock().unwrap(); + if !s.stopping { + s.child = npid; + s.started = Instant::now(); + s.restarts += 1; + restarts_now = s.restarts; + } + s.stopping + }; + if stop_now { + // stop raced with the respawn: undo it + let mut next = next; + let _ = kill(Pid::from_raw(-npid), Signal::SIGTERM); + let _ = next.wait(); + mark_done(&shared); + return; + } + eprintln!("pmon: restart {restarts_now} child pid={npid}"); + log_line(&log, &format!("pmon: start child pid={npid}")); + child = next; + } +} + +// --------------------------------------------------------------------------- +// supervise +// --------------------------------------------------------------------------- + +fn read_command(conn: &mut UnixStream) -> String { + let mut cmd = Vec::new(); + let mut one = [0u8; 1]; + while cmd.len() < 64 { + match conn.read(&mut one) { + Ok(1) if one[0] != b'\n' => cmd.push(one[0]), + _ => break, + } + } + String::from_utf8_lossy(&cmd).into_owned() +} + +fn send_log_fd(conn: &UnixStream, log: &File) { + let payload = [IoSlice::new(b"ok\n")]; + let fds = [log.as_raw_fd()]; + let cmsg = [ControlMessage::ScmRights(&fds)]; + if let Err(err) = sendmsg::<()>(conn.as_raw_fd(), &payload, &cmsg, MsgFlags::empty(), None) { + eprintln!("pmon: sendmsg: {err}"); + } +} + +fn cmd_supervise(ctl: &str, log_path: &str, child_argv: Vec) -> Result<()> { + // read(true), too: the fd handed out via SCM_RIGHTS shares this open + // file description, and pmctl must be able to read from it. + let log = File::options() + .create(true) + .read(true) + .append(true) + .open(log_path) + .with_context(|| format!("open {log_path}"))?; + + let _ = std::fs::remove_file(ctl); // stale socket from a previous run + let listener = UnixListener::bind(ctl).with_context(|| format!("listen {ctl}"))?; + eprintln!("pmon: listening on {ctl}"); + + let first = match spawn_child(&child_argv, &log) { + Ok(first) => first, + Err(err) => { + let _ = std::fs::remove_file(ctl); + return Err(err); + } + }; + let first_pid = first.id() as i32; + eprintln!("pmon: started child pid={first_pid}"); + log_line(&log, &format!("pmon: start child pid={first_pid}")); + + let shared: Shared = Arc::new(( + Mutex::new(State { + child: first_pid, + started: Instant::now(), + restarts: 0, + stopping: false, + child_done: false, + }), + Condvar::new(), + )); + let reaper_handle = { + let shared = Arc::clone(&shared); + let argv = child_argv.clone(); + let log = log.try_clone().context("dup log fd")?; + std::thread::spawn(move || reaper(shared, first, argv, log)) + }; + + for conn in listener.incoming() { + let mut conn = conn.context("accept")?; + + if let Ok(cred) = getsockopt(&conn, PeerCredentials) { + eprintln!("pmon: ctl connect uid={} pid={}", cred.uid(), cred.pid()); + } + + let cmd = read_command(&mut conn); + let (lock, cv) = &*shared; + match cmd.as_str() { + "status" => { + let reply = { + let s = lock.lock().unwrap(); + format!( + "child={} uptime={}s restarts={}\n", + s.child, + s.started.elapsed().as_secs(), + s.restarts + ) + }; + let _ = conn.write_all(reply.as_bytes()); + } + "logfd" => send_log_fd(&conn, &log), + "stop" => { + let _ = conn.write_all(b"stopping\n"); + drop(conn); // deliver the reply before tearing down + eprintln!("pmon: stopping"); + let child = { + let mut s = lock.lock().unwrap(); + s.stopping = true; + s.child + }; + cv.notify_all(); + if child > 0 { + // whole process group; ESRCH is fine + let _ = kill(Pid::from_raw(-child), Signal::SIGTERM); + } + { + let guard = lock.lock().unwrap(); + let _guard = cv.wait_while(guard, |s| !s.child_done).unwrap(); + } + let _ = reaper_handle.join(); + let _ = std::fs::remove_file(ctl); + return Ok(()); + } + _ => { + let _ = conn.write_all(b"err unknown command\n"); + } + } + } + Ok(()) +} + +// --------------------------------------------------------------------------- +// pmctl +// --------------------------------------------------------------------------- + +fn recv_log_fd(conn: &UnixStream) -> Result { + let mut payload = [0u8; 16]; + let mut iov = [IoSliceMut::new(&mut payload)]; + let mut cmsg_buf = nix::cmsg_space!(RawFd); + let msg = recvmsg::<()>( + conn.as_raw_fd(), + &mut iov, + Some(&mut cmsg_buf), + MsgFlags::MSG_CMSG_CLOEXEC, + ) + .context("recvmsg")?; + for cmsg in msg.cmsgs().context("parse cmsg")? { + if let ControlMessageOwned::ScmRights(fds) = cmsg + && let Some(&fd) = fds.first() + { + // SAFETY: the kernel installed this fd for us via SCM_RIGHTS; + // we are its only owner. + return Ok(unsafe { OwnedFd::from_raw_fd(fd) }); + } + } + bail!("no SCM_RIGHTS control message in reply") +} + +fn log_tail_via_fd(fd: OwnedFd, count: usize) -> Result> { + let file = File::from(fd); + let mut data = Vec::new(); + let mut chunk = [0u8; 4096]; + let mut off: u64 = 0; + while off < MAX_LOG_READ { + let n = file.read_at(&mut chunk, off).context("pread")?; + if n == 0 { + break; + } + data.extend_from_slice(&chunk[..n]); + off += n as u64; + } + let text = String::from_utf8_lossy(&data); + let text = text.strip_suffix('\n').unwrap_or(&text); + if text.is_empty() { + return Ok(Vec::new()); + } + let lines: Vec = text.split('\n').map(str::to_owned).collect(); + let skip = lines.len().saturating_sub(count); + Ok(lines[skip..].to_vec()) +} + +fn cmd_pmctl(ctl: &str, action: &str) -> Result<()> { + let mut conn = UnixStream::connect(ctl).with_context(|| format!("connect {ctl}"))?; + conn.write_all(format!("{action}\n").as_bytes()) + .with_context(|| format!("write {ctl}"))?; + + if action == "logfd" { + let fd = recv_log_fd(&conn)?; + for line in log_tail_via_fd(fd, 3)? { + println!("via-fd: {line}"); + } + return Ok(()); + } + + // status / stop: the supervisor replies with one line and closes. + let mut reply = String::new(); + conn.read_to_string(&mut reply).context("read reply")?; + let text = reply.trim_end_matches('\n'); + if text.is_empty() { + bail!("empty reply from supervisor"); + } + if let Some(err) = text.strip_prefix("err ") { + return Err(anyhow!("{err}")); + } + println!("{text}"); + Ok(()) +} + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let Some(sub) = args.first() else { + return usage(); + }; + + match sub.as_str() { + "supervise" => { + let mut ctl = String::new(); + let mut log = String::new(); + let mut child: Vec = Vec::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--ctl" && i + 1 < args.len() { + ctl = args[i + 1].clone(); + i += 2; + } else if args[i] == "--log" && i + 1 < args.len() { + log = args[i + 1].clone(); + i += 2; + } else if args[i] == "--" { + child = args[i + 1..].to_vec(); + break; + } else { + return usage(); + } + } + if ctl.is_empty() || log.is_empty() || child.is_empty() { + return usage(); + } + if let Err(err) = cmd_supervise(&ctl, &log, child) { + eprintln!("pmon: error: {err:#}"); + return ExitCode::from(1); + } + ExitCode::SUCCESS + } + "pmctl" => { + let mut ctl = String::new(); + let mut action = String::new(); + let mut i = 1; + while i < args.len() { + if args[i] == "--ctl" && i + 1 < args.len() { + ctl = args[i + 1].clone(); + i += 2; + } else if action.is_empty() { + action = args[i].clone(); + i += 1; + } else { + return usage(); + } + } + if ctl.is_empty() || !matches!(action.as_str(), "status" | "stop" | "logfd") { + return usage(); + } + if let Err(err) = cmd_pmctl(&ctl, &action) { + eprintln!("pmctl: error: {err:#}"); + return ExitCode::from(1); + } + ExitCode::SUCCESS + } + _ => usage(), + } +} diff --git a/examples/19-unix-sockets-fd-passing/verify.lua b/examples/19-unix-sockets-fd-passing/verify.lua new file mode 100644 index 0000000..9db65c9 --- /dev/null +++ b/examples/19-unix-sockets-fd-passing/verify.lua @@ -0,0 +1,167 @@ +-- Verify pmon v5 (19-unix-sockets-fd-passing) for LSP_LANG. +-- +-- Asserts observable behavior, identical across cpp/go/rust: +-- 1. usage errors exit 2; pmctl against a missing socket exits 1 +-- 2. supervise: control socket appears; status reports child/uptime/restarts +-- and the reported child pid is a live process running the supervised +-- command (process-tree check via /proc) +-- 3. SO_PEERCRED: the supervisor logs the CALLING uid for every ctl connect +-- 4. logfd: pmctl's "via-fd:" output equals `tail -3` of the log file byte +-- for byte — proof the fd crossed the socket and was read directly +-- 5. restarts: a short-lived child drives restarts=N>0 in status, restart +-- lines on stderr, and one "pmon: start child" log line per (re)start +-- 6. stop: reply is exactly "stopping"; supervisor exits, removes the +-- socket, and the child process group is gone +-- +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set; the interpreter may be lua or luajit. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" + +-- Scratch area. NB: sun_path caps UNIX socket paths at ~107 bytes, so this +-- must stay short — mktemp -d under /tmp is fine, deep CI workspaces are not. +local wd = checks.run("mktemp -d").out:gsub("%s+$", "") +if wd == "" or not wd:match("^/") then + checks.skip("mktemp -d did not yield a directory") +end +local uid = checks.run("id -u").out:gsub("%s+$", "") + +local function sh(script) + return checks.run(script:gsub("@WD@", wd)) +end + +-- --------------------------------------------------------------------------- +-- 1. error shapes +-- --------------------------------------------------------------------------- + +local noargs = checks.run(demo) +checks.expect_exit(noargs, 2, lang .. ": no arguments exits 2") +checks.expect_match(noargs.out, "usage: pmon", lang .. ": no arguments prints usage") + +local badact = sh(demo .. " pmctl --ctl @WD@/x.sock frobnicate") +checks.expect_exit(badact, 2, lang .. ": unknown pmctl action exits 2") + +local nosock = sh(demo .. " pmctl --ctl @WD@/absent.sock status") +checks.expect_exit(nosock, 1, lang .. ": pmctl against a missing socket exits 1") +checks.expect_match(nosock.out, "pmctl: error:", lang .. ": connect failure is reported") + +-- --------------------------------------------------------------------------- +-- 2. supervise + status (long-lived quiet child) +-- --------------------------------------------------------------------------- + +local start = sh([[ +]] .. demo .. [[ supervise --ctl @WD@/ctl.sock --log @WD@/pmon.log -- \ + /bin/sh -c 'echo alpha; echo beta; echo gamma; exec sleep 30' \ + > @WD@/sup.out 2> @WD@/sup.err & +echo $! > @WD@/sup.pid +for i in $(seq 1 100); do [ -S @WD@/ctl.sock ] && break; sleep 0.05; done +sleep 0.3 +test -S @WD@/ctl.sock +]]) +checks.expect_exit(start, 0, lang .. ": control socket appears after supervise starts") + +local st = sh(demo .. " pmctl --ctl @WD@/ctl.sock status") +checks.expect_exit(st, 0, lang .. ": status exits 0") +checks.expect_match(st.out, "^child=%d+ uptime=%d+s restarts=0\n$", + lang .. ": status is exactly 'child= uptime=s restarts='") + +local child = st.out:match("child=(%d+)") or "0" +local tree = checks.run("tr '\\0' ' ' < /proc/" .. child .. "/cmdline") +checks.expect_exit(tree, 0, lang .. ": status child pid is a live process") +checks.expect_match(tree.out, "sleep 30", lang .. ": child pid is running the supervised command") + +-- --------------------------------------------------------------------------- +-- 3. SO_PEERCRED +-- --------------------------------------------------------------------------- + +local creds = sh("cat @WD@/sup.err") +checks.expect_match(creds.out, "pmon: listening on " .. wd .. "/ctl%.sock", + lang .. ": supervisor announces the ctl socket") +checks.expect_match(creds.out, "pmon: started child pid=" .. child, + lang .. ": supervisor announces the child pid") +checks.expect_match(creds.out, "pmon: ctl connect uid=" .. uid .. " pid=%d+", + lang .. ": SO_PEERCRED line shows the calling uid") + +-- --------------------------------------------------------------------------- +-- 4. logfd: the tail arrives through the passed descriptor +-- --------------------------------------------------------------------------- + +local lf = sh(demo .. " pmctl --ctl @WD@/ctl.sock logfd | tee @WD@/viafd.out") +checks.expect_exit(lf, 0, lang .. ": logfd exits 0") +checks.expect_match(lf.out, "via%-fd: gamma", lang .. ": log tail read via the received fd") +local _, nlines = lf.out:gsub("via%-fd: [^\n]*\n", "") +checks.expect_match("lines=" .. nlines, "^lines=3$", lang .. ": logfd prints exactly 3 via-fd lines") + +local cmp = sh([[ +tail -n 3 @WD@/pmon.log | sed 's/^/via-fd: /' > @WD@/expect.out +diff -u @WD@/expect.out @WD@/viafd.out +]]) +checks.expect_exit(cmp, 0, lang .. ": via-fd output equals tail -3 of the log file byte for byte") + +-- --------------------------------------------------------------------------- +-- 5. restarts (short-lived child) +-- --------------------------------------------------------------------------- + +local r2 = sh([[ +]] .. demo .. [[ supervise --ctl @WD@/ctl2.sock --log @WD@/pmon2.log -- \ + /bin/sh -c 'exec sleep 0.2' > /dev/null 2> @WD@/sup2.err & +for i in $(seq 1 100); do [ -S @WD@/ctl2.sock ] && break; sleep 0.05; done +sleep 1.3 +]] .. demo .. [[ pmctl --ctl @WD@/ctl2.sock status +]]) +checks.expect_exit(r2, 0, lang .. ": status works while the child is being restarted") +checks.expect_match(r2.out, "child=%d+ uptime=%d+s restarts=[1-9]%d*", + lang .. ": a dying child drives restarts > 0") + +local r2err = sh("cat @WD@/sup2.err") +checks.expect_match(r2err.out, "pmon: child pid=%d+ exited status=0", + lang .. ": each child exit is reported with its status") +checks.expect_match(r2err.out, "pmon: restart 1 child pid=%d+", + lang .. ": restarts are numbered on stderr") + +-- One "pmon: start child" log line per (re)start. Another restart may land +-- between the status read and this grep, so assert >= rather than ==. +local restarts = tonumber(r2.out:match("restarts=(%d+)")) or 0 +local starts = tonumber(sh("grep -c '^pmon: start child pid=' @WD@/pmon2.log").out:match("%d+")) or 0 +local label = lang .. ": log has one 'pmon: start child' line per (re)start" +if starts >= restarts + 1 and starts >= 2 then + checks.expect_match("ok", "ok", label) +else + checks.expect_match(string.format("starts=%d restarts=%d", starts, restarts), "^impossible$", label) +end + +local stop2 = sh(demo .. " pmctl --ctl @WD@/ctl2.sock stop") +checks.expect_exit(stop2, 0, lang .. ": stop of the restarting supervisor exits 0") + +-- --------------------------------------------------------------------------- +-- 6. stop: graceful teardown of the first supervisor +-- --------------------------------------------------------------------------- + +local sp = sh(demo .. " pmctl --ctl @WD@/ctl.sock stop") +checks.expect_exit(sp, 0, lang .. ": stop exits 0") +checks.expect_match(sp.out, "^stopping\n$", lang .. ": stop reply is exactly 'stopping'") + +local gone = sh([[ +for i in $(seq 1 100); do kill -0 $(cat @WD@/sup.pid) 2>/dev/null || break; sleep 0.05; done +kill -0 $(cat @WD@/sup.pid) 2>/dev/null && exit 1 +test -e @WD@/ctl.sock && exit 2 +kill -0 ]] .. child .. [[ 2>/dev/null && exit 3 +exit 0 +]]) +checks.expect_exit(gone, 0, + lang .. ": after stop the supervisor exits, unlinks the socket, and the child group is gone") +checks.expect_match(sh("cat @WD@/sup.err").out, "pmon: stopping", + lang .. ": supervisor logs the stop") + +checks.run("rm -rf " .. wd) +checks.finish() diff --git a/examples/20-shared-memory-coordination/README.md b/examples/20-shared-memory-coordination/README.md new file mode 100644 index 0000000..2d25537 --- /dev/null +++ b/examples/20-shared-memory-coordination/README.md @@ -0,0 +1,116 @@ +# 20-shared-memory-coordination — shmkv v1 + +shmkv grows from a passive shared-memory key-value file (v0) into a live +coordination fabric: one process publishes updates, other processes learn +about them **without polling**, through a seqlock for consistency and a futex +for wakeups — the same pair of primitives glibc builds mutexes and condition +variables from. Implemented three times (C++23, Go 1.26, Rust 2024) with +identical observable behavior. + +## The v1 file format (SHKV2) + +``` +offset size field +0 8 magic "SHKV2\0\0\0" +8 4 seqlock word (u32; odd = writer in critical section) +12 4 futex word (u32; low 32 bits of the update counter) +16 8 update counter (u64; total updates published) +24 40 reserved +64 512 8 slots x 64 bytes: key[24] NUL-padded, value[40] NUL-padded +``` + +One page (4096 bytes), mapped `MAP_SHARED` by every participant. The slots +form a ring keyed by `(id-1) % 8`, so a watcher that sleeps through a wake +can back-fill up to 8 missed updates from the ring. + +## Subcommands + +``` +shmkv serve FILE [--updates N] [--interval-ms T] +shmkv watch FILE [--events N] +shmkv bench FILE [--rounds N] [--channel futex|mq|poll] +``` + +- **serve** (writer): creates/initializes FILE, then publishes N updates + (`k=value-`), each under the seqlock, bumping the u64 counter and the + u32 futex word, then `FUTEX_WAKE`-ing all waiters. Prints one + `published update : =` line per update. +- **watch** (reader): validates the magic, then loops `FUTEX_WAIT` on the + futex word; on each wake it takes a seqlock-consistent snapshot and prints + `observed update : =` for every new id, exiting 0 after N + events (or 1 after a 30 s self-termination deadline). +- **bench**: producer/consumer threads over the same mapping measure one-way + notification latency for three channels — `futex` (wake/wait), `mq` (a + POSIX message queue carrying the update id), `poll` (a 1 ms sleep loop + re-reading the counter; it sleeps *before* looking, so every observation + costs at least one full nap). Rounds are strictly serialized by an ack word + so queueing never masquerades as latency. Output is one row: + `bench: channel= rounds= p50_us= p99_us=`. The message queue + is unlinked on exit; nothing survives in `/dev/mqueue`. + +Errors: usage problems exit 2; a missing FILE or a file without the SHKV2 +magic exits 1 with a `shmkv:`-prefixed message on stderr. + +## The seqlock protocol + +``` +write: seq += 1 (relaxed) ; release fence ; mutate slots + counter ; seq += 1 (release) +read: s1 = seq (acquire) ; if s1 odd retry + copy counter + slots ; acquire fence + s2 = seq (relaxed) ; consistent iff s1 == s2, else retry +``` + +The data copy races the writer on purpose; a torn copy is detected by the +seq re-check and discarded. That trade-off — writers never block, readers +retry — is exactly the kernel's seqlock. The retry loop is bounded (10^6 +attempts) so a corrupt file cannot livelock a reader forever. + +The futex is **shared** (no `FUTEX_PRIVATE_FLAG`): the kernel keys waiters +by the file's inode, which is what lets two unrelated processes rendezvous +through the same mapped page. `EAGAIN`, `ETIMEDOUT`, and `EINTR` from +`FUTEX_WAIT` are all normal — the waiter re-reads shared state and decides +again. + +## Language notes + +- **C++23**: RAII wrappers for the fd, the mapping, and the message queue; + `std::atomic_ref` binds atomics onto the mapped words; raw + `syscall(SYS_futex, ...)`; `std::jthread` for the bench consumer; + `std::expected` carries setup errors to `main`. +- **Go**: `golang.org/x/sys/unix.Syscall6` for `SYS_FUTEX` and the four + `mq_*` syscalls (the kernel takes the queue name *without* glibc's leading + slash). **Futex caveat**: `FUTEX_WAIT` parks the calling OS thread in the + kernel where the goroutine scheduler cannot reach it; sysmon hands the P to + another thread so the program keeps running, but every concurrent waiter + pins a kernel thread. Fine for one watcher per process; channels and the + netpoller remain the scalable Go answer. +- **Rust (edition 2024)**: `rustix` `open` returns an `OwnedFd`; a small + `Drop` type munmaps; `AtomicU32::from_ptr` overlays atomics on the mapping; + the futex is the raw `libc::syscall(libc::SYS_futex, ...)` so the kernel + contract stays visible; `std::thread::scope` runs the bench consumer. + +## Run it + +```sh +./demo.sh build # all three languages +./demo.sh cpp run serve /tmp/kv.shm --updates 8 --interval-ms 100 & +./demo.sh rust run watch /tmp/kv.shm --events 8 # cross-language: same file +./demo.sh go run bench /tmp/b.shm --rounds 200 --channel futex +``` + +Languages interoperate: a Go `serve` wakes a Rust `watch` on the same file, +because the contract is the page layout plus the futex, not the runtime. + +## Verification (behavioral) + +`verify.lua` asserts, per language: + +1. usage → exit 2; missing file → exit 1; non-SHKV2 file → exit 1. +2. `serve` and `watch` run as two real processes (checked mid-run via + `pgrep -f`); all 6 updates are observed with consistent `k=value-` + pairs; both exit 0. +3. the file afterwards: SHKV2 magic, futex word = 6, u64 counter = 6, the + final value string present in a slot, size exactly 4096. +4. `bench` emits a parseable row for each of futex/mq/poll; `p99 >= p50`; + poll p50 ≥ 500 µs by construction; futex p50 < poll p50. +5. `/dev/mqueue` holds no `shmkv-bench-*` leftovers. diff --git a/examples/20-shared-memory-coordination/cpp/.clang-format b/examples/20-shared-memory-coordination/cpp/.clang-format new file mode 100644 index 0000000..2c8640f --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +Standard: c++20 +IndentWidth: 4 +ColumnLimit: 100 +PointerAlignment: Left +AllowShortFunctionsOnASingleLine: Inline diff --git a/examples/20-shared-memory-coordination/cpp/.clang-tidy b/examples/20-shared-memory-coordination/cpp/.clang-tidy new file mode 100644 index 0000000..2f275ba --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/.clang-tidy @@ -0,0 +1,11 @@ +--- +Checks: > + bugprone-*, + modernize-*, + performance-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length +WarningsAsErrors: '' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/examples/20-shared-memory-coordination/cpp/CMakeLists.txt b/examples/20-shared-memory-coordination/cpp/CMakeLists.txt new file mode 100644 index 0000000..6bdb445 --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.25) +project(template_hello CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(app src/main.cpp) diff --git a/examples/20-shared-memory-coordination/cpp/CMakePresets.json b/examples/20-shared-memory-coordination/cpp/CMakePresets.json new file mode 100644 index 0000000..e31bd2f --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/CMakePresets.json @@ -0,0 +1,62 @@ +{ + "version": 6, + "cmakeMinimumRequired": { "major": 3, "minor": 25, "patch": 0 }, + "configurePresets": [ + { + "name": "_base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "inherits": "_base", + "displayName": "Release (-O2 with debug info)", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "debug", + "inherits": "_base", + "displayName": "Debug", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } + }, + { + "name": "asan", + "inherits": "_base", + "displayName": "ASan + UBSan instrumented build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS": "-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined", + "CMAKE_EXE_LINKER_FLAGS": "-fsanitize=address,undefined" + } + }, + { + "name": "release-clang", + "inherits": "release", + "displayName": "Release (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "debug-clang", + "inherits": "debug", + "displayName": "Debug (clang)", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" }, + { "name": "asan", "configurePreset": "asan" }, + { "name": "release-clang", "configurePreset": "release-clang" }, + { "name": "debug-clang", "configurePreset": "debug-clang" } + ] +} diff --git a/examples/20-shared-memory-coordination/cpp/demo.sh b/examples/20-shared-memory-coordination/cpp/demo.sh new file mode 100755 index 0000000..6af6509 --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/demo.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=build/release/app + +build() { + cmake --preset release + cmake --build --preset release +} + +run() { + if [[ ! -x "$BIN" ]]; then + build + fi + if [[ -n "${TARGET:-}" ]]; then + "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) + build + ;; + run) + shift + run "$@" + ;; + "") + build + run + ;; + *) + echo "usage: $0 [build|run [args]]" >&2 + exit 2 + ;; +esac diff --git a/examples/20-shared-memory-coordination/cpp/src/main.cpp b/examples/20-shared-memory-coordination/cpp/src/main.cpp new file mode 100644 index 0000000..21b1ff7 --- /dev/null +++ b/examples/20-shared-memory-coordination/cpp/src/main.cpp @@ -0,0 +1,528 @@ +// shmkv v1 — cross-process coordination over a shared-memory key-value file. +// +// The v0 file format (fixed slots in an mmap'd file) grows a v1 header with +// SHKV2 magic: +// +// offset size field +// 0 8 magic "SHKV2\0\0\0" +// 8 4 seqlock word (u32; odd = writer in critical section) +// 12 4 futex word (u32; low 32 bits of the update counter) +// 16 8 update counter (u64; total updates published) +// 24 40 reserved +// 64 512 8 slots x 64 bytes: key[24] NUL-padded, value[40] NUL-padded +// +// Three coordination channels are demonstrated: +// serve — writer: seqlock-protected publish + FUTEX_WAKE after each update +// watch — reader: FUTEX_WAIT loop + seqlock-consistent snapshot reads +// bench — latency shoot-out: futex wake vs POSIX message queue vs 1 ms +// sleep-polling of the counter +// +// Seqlock protocol (single writer): +// write: seq += 1 (relaxed) ; release fence ; mutate data ; seq += 1 (release) +// read: s1 = seq (acquire); if odd retry; copy data; acquire fence; +// s2 = seq (relaxed); consistent iff s1 == s2 +// The data copy itself is a plain memcpy raced against the writer; the retry +// loop discards any torn copy, which is the standard seqlock trade-off. +// +// Everything is RAII (fd, mapping, message queue), errors travel as +// std::expected until main turns them into exit codes. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; + +constexpr std::array kMagic{'S', 'H', 'K', 'V', '2', 0, 0, 0}; +constexpr std::size_t kFileSize = 4096; +constexpr std::size_t kOffSeq = 8; +constexpr std::size_t kOffFutex = 12; +constexpr std::size_t kOffCounter = 16; +constexpr std::size_t kOffSlots = 64; +constexpr std::size_t kSlotCount = 8; +constexpr std::size_t kKeySize = 24; +constexpr std::size_t kValSize = 40; +constexpr std::size_t kSlotSize = 64; +constexpr std::size_t kSlotsBytes = kSlotCount * kSlotSize; + +std::string errno_text(int err) { return std::strerror(err); } + +// --- RAII wrappers --------------------------------------------------------- + +class Fd { + public: + Fd() = default; + explicit Fd(int fd) : fd_(fd) {} + Fd(Fd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {} + Fd& operator=(Fd&& o) noexcept { + if (this != &o) { + reset(); + fd_ = std::exchange(o.fd_, -1); + } + return *this; + } + Fd(const Fd&) = delete; + Fd& operator=(const Fd&) = delete; + ~Fd() { reset(); } + void reset() { + if (fd_ >= 0) ::close(fd_); + fd_ = -1; + } + int get() const { return fd_; } + + private: + int fd_ = -1; +}; + +class Mapping { + public: + Mapping() = default; + Mapping(void* p, std::size_t len) : p_(p), len_(len) {} + Mapping(Mapping&& o) noexcept + : p_(std::exchange(o.p_, nullptr)), len_(std::exchange(o.len_, 0)) {} + Mapping& operator=(Mapping&& o) noexcept { + if (this != &o) { + reset(); + p_ = std::exchange(o.p_, nullptr); + len_ = std::exchange(o.len_, 0); + } + return *this; + } + Mapping(const Mapping&) = delete; + Mapping& operator=(const Mapping&) = delete; + ~Mapping() { reset(); } + void reset() { + if (p_ != nullptr) ::munmap(p_, len_); + p_ = nullptr; + } + unsigned char* bytes() const { return static_cast(p_); } + + private: + void* p_ = nullptr; + std::size_t len_ = 0; +}; + +// mq_close + mq_unlink on scope exit — no queue name survives a bench run. +class MessageQueue { + public: + MessageQueue(mqd_t d, std::string name) : d_(d), name_(std::move(name)) {} + MessageQueue(const MessageQueue&) = delete; + MessageQueue& operator=(const MessageQueue&) = delete; + ~MessageQueue() { + ::mq_close(d_); + ::mq_unlink(name_.c_str()); + } + mqd_t get() const { return d_; } + + private: + mqd_t d_; + std::string name_; +}; + +// --- the shared file ------------------------------------------------------- + +struct Shm { + Fd fd; + Mapping map; + + unsigned char* base() const { return map.bytes(); } + std::atomic_ref seq() const { + return std::atomic_ref{*reinterpret_cast(base() + kOffSeq)}; + } + std::atomic_ref futex_word() const { + return std::atomic_ref{*reinterpret_cast(base() + kOffFutex)}; + } + std::uint32_t* futex_ptr() const { + return reinterpret_cast(base() + kOffFutex); + } + std::atomic_ref counter() const { + return std::atomic_ref{*reinterpret_cast(base() + kOffCounter)}; + } +}; + +std::expected open_shm(const std::string& path, bool create) { + int flags = O_RDWR | O_CLOEXEC | (create ? O_CREAT : 0); + Fd fd(::open(path.c_str(), flags, 0644)); + if (fd.get() < 0) { + return std::unexpected(std::format("open {}: {}", path, errno_text(errno))); + } + if (create) { + if (::ftruncate(fd.get(), static_cast(kFileSize)) != 0) { + return std::unexpected(std::format("ftruncate {}: {}", path, errno_text(errno))); + } + } else { + struct stat st{}; + if (::fstat(fd.get(), &st) != 0) { + return std::unexpected(std::format("fstat {}: {}", path, errno_text(errno))); + } + if (st.st_size < static_cast(kFileSize)) { + return std::unexpected(std::format("{}: bad magic (want SHKV2)", path)); + } + } + void* p = ::mmap(nullptr, kFileSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0); + if (p == MAP_FAILED) { + return std::unexpected(std::format("mmap {}: {}", path, errno_text(errno))); + } + Shm shm{std::move(fd), Mapping{p, kFileSize}}; + if (create) { + std::memset(shm.base(), 0, kFileSize); + std::memcpy(shm.base(), kMagic.data(), kMagic.size()); + } else if (std::memcmp(shm.base(), kMagic.data(), kMagic.size()) != 0) { + return std::unexpected(std::format("{}: bad magic (want SHKV2)", path)); + } + return shm; +} + +// --- seqlock --------------------------------------------------------------- + +template +void seqlock_write(const Shm& shm, Mutate&& mutate) { + auto seq = shm.seq(); + seq.fetch_add(1, std::memory_order_relaxed); // odd: writer active + std::atomic_thread_fence(std::memory_order_release); + mutate(); + seq.fetch_add(1, std::memory_order_release); // even: consistent again +} + +struct Snapshot { + std::uint64_t counter = 0; + std::array slots{}; +}; + +std::expected seqlock_read(const Shm& shm) { + auto seq = shm.seq(); + for (int attempt = 0; attempt < 1'000'000; ++attempt) { + std::uint32_t s1 = seq.load(std::memory_order_acquire); + if (s1 & 1) { // writer mid-update: retry + std::this_thread::yield(); + continue; + } + Snapshot snap; + snap.counter = shm.counter().load(std::memory_order_relaxed); + std::memcpy(snap.slots.data(), shm.base() + kOffSlots, kSlotsBytes); + std::atomic_thread_fence(std::memory_order_acquire); + if (seq.load(std::memory_order_relaxed) == s1) return snap; + } + return std::unexpected(std::string("seqlock read livelocked")); +} + +// --- futex ----------------------------------------------------------------- + +// Shared (non-PRIVATE) futex: the kernel keys on the file's inode, so waiters +// and wakers in different processes rendezvous through the same mapping. +void futex_wait(std::uint32_t* addr, std::uint32_t expected, + std::chrono::milliseconds timeout) { + timespec ts{.tv_sec = timeout.count() / 1000, + .tv_nsec = (timeout.count() % 1000) * 1'000'000}; + // EAGAIN (word already moved on), ETIMEDOUT and EINTR are all normal here: + // the caller re-checks shared state and decides whether to wait again. + ::syscall(SYS_futex, addr, FUTEX_WAIT, expected, &ts, nullptr, 0); +} + +void futex_wake_all(std::uint32_t* addr) { + ::syscall(SYS_futex, addr, FUTEX_WAKE, INT_MAX, nullptr, nullptr, 0); +} + +// --- helpers --------------------------------------------------------------- + +std::string cstr(const unsigned char* p, std::size_t n) { + std::size_t len = 0; + while (len < n && p[len] != 0) ++len; + return std::string(reinterpret_cast(p), len); +} + +void write_slot(const Shm& shm, std::uint64_t id, std::string_view key, + std::string_view val) { + unsigned char* slot = shm.base() + kOffSlots + ((id - 1) % kSlotCount) * kSlotSize; + std::memset(slot, 0, kSlotSize); + std::memcpy(slot, key.data(), std::min(key.size(), kKeySize - 1)); + std::memcpy(slot + kKeySize, val.data(), std::min(val.size(), kValSize - 1)); +} + +std::pair read_slot(const Snapshot& snap, std::uint64_t id) { + const unsigned char* slot = snap.slots.data() + ((id - 1) % kSlotCount) * kSlotSize; + return {cstr(slot, kKeySize), cstr(slot + kKeySize, kValSize)}; +} + +std::int64_t now_ns() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +// --- serve ----------------------------------------------------------------- + +int cmd_serve(const std::string& path, std::uint64_t updates, std::uint64_t interval_ms) { + auto shm = open_shm(path, /*create=*/true); + if (!shm) { + std::println(stderr, "shmkv: {}", shm.error()); + return 1; + } + std::println("serve: file={} updates={} interval_ms={}", path, updates, interval_ms); + for (std::uint64_t k = 1; k <= updates; ++k) { + std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms)); + std::string key = std::format("k{}", k); + std::string val = std::format("value-{}", k); + seqlock_write(*shm, [&] { + write_slot(*shm, k, key, val); + shm->counter().store(k, std::memory_order_relaxed); + }); + shm->futex_word().store(static_cast(k), std::memory_order_release); + futex_wake_all(shm->futex_ptr()); + std::println("published update {}: {}={}", k, key, val); + } + std::println("serve: complete updates={}", updates); + return 0; +} + +// --- watch ----------------------------------------------------------------- + +int cmd_watch(const std::string& path, std::uint64_t events) { + auto shm = open_shm(path, /*create=*/false); + if (!shm) { + std::println(stderr, "shmkv: {}", shm.error()); + return 1; + } + std::println("watch: file={} events={}", path, events); + const auto deadline = std::chrono::steady_clock::now() + 30s; + std::uint64_t last = 0; + std::uint64_t printed = 0; + while (printed < events) { + if (std::chrono::steady_clock::now() > deadline) { + std::println(stderr, "shmkv: watch: timed out waiting for updates"); + return 1; + } + auto snap = seqlock_read(*shm); + if (!snap) { + std::println(stderr, "shmkv: {}", snap.error()); + return 1; + } + if (snap->counter > last) { + // The slot ring holds the last kSlotCount updates, so a watcher that + // slept through a wake can still back-fill every missed id. + for (std::uint64_t id = last + 1; id <= snap->counter && printed < events; ++id) { + auto [key, val] = read_slot(*snap, id); + std::println("observed update {}: {}={}", id, key, val); + ++printed; + } + last = snap->counter; + continue; + } + std::uint32_t w = shm->futex_word().load(std::memory_order_acquire); + if (w != static_cast(last)) continue; // moved on: re-read now + futex_wait(shm->futex_ptr(), w, 2000ms); + } + std::println("watch: complete events={}", events); + return 0; +} + +// --- bench ----------------------------------------------------------------- + +enum class Channel { kFutex, kMq, kPoll }; + +timespec abs_deadline(std::chrono::seconds ahead) { + timespec ts{}; + ::clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += ahead.count(); + return ts; +} + +int cmd_bench(const std::string& path, std::uint64_t rounds, Channel channel, + const std::string& channel_name) { + auto shm = open_shm(path, /*create=*/true); + if (!shm) { + std::println(stderr, "shmkv: {}", shm.error()); + return 1; + } + + std::unique_ptr mq; + if (channel == Channel::kMq) { + std::string name = std::format("/shmkv-bench-{}", ::getpid()); + mq_attr attr{}; + attr.mq_maxmsg = 8; + attr.mq_msgsize = 8; + mqd_t d = ::mq_open(name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600, &attr); + if (d == static_cast(-1)) { + std::println(stderr, "shmkv: mq_open {}: {}", name, errno_text(errno)); + return 1; + } + mq = std::make_unique(d, name); + } + + std::vector send_ns(rounds), recv_ns(rounds); + std::atomic ack{0}; + + std::jthread consumer([&] { + for (std::uint64_t i = 1; i <= rounds; ++i) { + switch (channel) { + case Channel::kFutex: + for (;;) { + std::uint32_t w = shm->futex_word().load(std::memory_order_acquire); + if (w >= static_cast(i)) break; + futex_wait(shm->futex_ptr(), w, 200ms); + } + break; + case Channel::kPoll: + // Sleep FIRST, then look: by construction every observation costs + // at least one full 1 ms nap — the cost futex wakeups avoid. + for (;;) { + std::this_thread::sleep_for(1ms); + if (shm->counter().load(std::memory_order_acquire) >= i) break; + } + break; + case Channel::kMq: { + char buf[8]; + for (;;) { + timespec ts = abs_deadline(10s); + ssize_t n = ::mq_timedreceive(mq->get(), buf, sizeof buf, nullptr, &ts); + if (n == 8) break; + if (n < 0 && errno == EINTR) continue; + std::println(stderr, "shmkv: mq_timedreceive: {}", errno_text(errno)); + std::exit(1); + } + break; + } + } + recv_ns[i - 1] = now_ns(); + ack.store(i, std::memory_order_release); + } + }); + + const auto spin_deadline = std::chrono::steady_clock::now() + 30s; + for (std::uint64_t i = 1; i <= rounds; ++i) { + // Spin until the consumer acknowledged round i-1 so rounds never overlap + // (an overlapped round would measure queueing, not wakeup latency). + while (ack.load(std::memory_order_acquire) != i - 1) { + if (std::chrono::steady_clock::now() > spin_deadline) { + std::println(stderr, "shmkv: bench: consumer stalled"); + std::exit(1); + } + std::this_thread::yield(); + } + send_ns[i - 1] = now_ns(); + if (channel == Channel::kMq) { + char buf[8]; + std::uint64_t id = i; + std::memcpy(buf, &id, sizeof buf); + timespec ts = abs_deadline(10s); + if (::mq_timedsend(mq->get(), buf, sizeof buf, 0, &ts) != 0) { + std::println(stderr, "shmkv: mq_timedsend: {}", errno_text(errno)); + std::exit(1); + } + } else { + seqlock_write(*shm, [&] { shm->counter().store(i, std::memory_order_relaxed); }); + shm->futex_word().store(static_cast(i), std::memory_order_release); + if (channel == Channel::kFutex) futex_wake_all(shm->futex_ptr()); + } + } + consumer.join(); + + std::vector us(rounds); + for (std::uint64_t i = 0; i < rounds; ++i) { + us[i] = std::max(0, recv_ns[i] - send_ns[i]) / 1000; + } + std::ranges::sort(us); + auto pct = [&](std::uint64_t p) { + std::uint64_t idx = rounds * p / 100; + if (idx >= rounds) idx = rounds - 1; + return us[idx]; + }; + std::println("bench: channel={} rounds={} p50_us={} p99_us={}", channel_name, + rounds, pct(50), pct(99)); + return 0; +} + +// --- CLI ------------------------------------------------------------------- + +int usage() { + std::println(stderr, "usage: shmkv serve FILE [--updates N] [--interval-ms T]"); + std::println(stderr, " shmkv watch FILE [--events N]"); + std::println(stderr, " shmkv bench FILE [--rounds N] [--channel futex|mq|poll]"); + return 2; +} + +std::expected parse_u64(std::string_view s) { + std::uint64_t v = 0; + auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v); + if (ec != std::errc{} || ptr != s.data() + s.size() || v == 0 || v > 1'000'000) { + return std::unexpected(std::format("bad number: {}", s)); + } + return v; +} + +} // namespace + +int main(int argc, char** argv) { + std::vector args(argv + 1, argv + argc); + if (args.size() < 2) return usage(); + const std::string& cmd = args[0]; + const std::string& file = args[1]; + + std::uint64_t updates = 8, interval_ms = 100, events = 4, rounds = 100; + std::string channel_name = "futex"; + for (std::size_t i = 2; i < args.size(); i += 2) { + if (i + 1 >= args.size()) return usage(); + const std::string& flag = args[i]; + const std::string& val = args[i + 1]; + if (flag == "--channel" && cmd == "bench") { + channel_name = val; + continue; + } + std::expected n = parse_u64(val); + if (!n) return usage(); + if (flag == "--updates" && cmd == "serve") { + updates = *n; + } else if (flag == "--interval-ms" && cmd == "serve") { + interval_ms = *n; + } else if (flag == "--events" && cmd == "watch") { + events = *n; + } else if (flag == "--rounds" && cmd == "bench") { + rounds = *n; + } else { + return usage(); + } + } + + if (cmd == "serve") return cmd_serve(file, updates, interval_ms); + if (cmd == "watch") return cmd_watch(file, events); + if (cmd == "bench") { + Channel ch{}; + if (channel_name == "futex") { + ch = Channel::kFutex; + } else if (channel_name == "mq") { + ch = Channel::kMq; + } else if (channel_name == "poll") { + ch = Channel::kPoll; + } else { + return usage(); + } + return cmd_bench(file, rounds, ch, channel_name); + } + return usage(); +} diff --git a/examples/20-shared-memory-coordination/demo.sh b/examples/20-shared-memory-coordination/demo.sh new file mode 100755 index 0000000..afbbe4c --- /dev/null +++ b/examples/20-shared-memory-coordination/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +langs=(cpp go rust) + +case "${1:-all}" in + cpp|go|rust) + lang="$1" + shift + exec "./$lang/demo.sh" "$@" + ;; + all) + for lang in "${langs[@]}"; do "./$lang/demo.sh"; done + ;; + build) + for lang in "${langs[@]}"; do "./$lang/demo.sh" build; done + ;; + *) + echo "usage: $0 [cpp|go|rust|all|build] [args...]" >&2 + exit 2 + ;; +esac diff --git a/examples/20-shared-memory-coordination/go/demo.sh b/examples/20-shared-memory-coordination/go/demo.sh new file mode 100755 index 0000000..28606df --- /dev/null +++ b/examples/20-shared-memory-coordination/go/demo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +REPO_ROOT="$(cd ../../.. && pwd)" +BIN=bin/app + +build() { + go build -o "$BIN" . +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + exec "$REPO_ROOT/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + fi + exec "./$BIN" "$@" +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/20-shared-memory-coordination/go/go.mod b/examples/20-shared-memory-coordination/go/go.mod new file mode 100644 index 0000000..6a952bc --- /dev/null +++ b/examples/20-shared-memory-coordination/go/go.mod @@ -0,0 +1,7 @@ +module github.com/patterncatalyst/linux-systems-programming/examples/20-shared-memory-coordination/go + +go 1.26 + +toolchain go1.26.5 + +require golang.org/x/sys v0.47.0 diff --git a/examples/20-shared-memory-coordination/go/go.sum b/examples/20-shared-memory-coordination/go/go.sum new file mode 100644 index 0000000..37ee2d4 --- /dev/null +++ b/examples/20-shared-memory-coordination/go/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/examples/20-shared-memory-coordination/go/main.go b/examples/20-shared-memory-coordination/go/main.go new file mode 100644 index 0000000..aa1a82b --- /dev/null +++ b/examples/20-shared-memory-coordination/go/main.go @@ -0,0 +1,513 @@ +// shmkv v1 — cross-process coordination over a shared-memory key-value file. +// +// Same SHKV2 layout as the C++ and Rust versions: +// +// offset size field +// 0 8 magic "SHKV2\0\0\0" +// 8 4 seqlock word (u32; odd = writer in critical section) +// 12 4 futex word (u32; low 32 bits of the update counter) +// 16 8 update counter (u64; total updates published) +// 24 40 reserved +// 64 512 8 slots x 64 bytes: key[24] NUL-padded, value[40] NUL-padded +// +// Go futex caveat: FUTEX_WAIT is a blocking syscall, so it parks the calling +// OS thread inside the kernel — the goroutine scheduler cannot preempt it. +// The runtime notices the thread is gone (sysmon) and hands its P to another +// thread, so the rest of the program keeps running, but every concurrent +// waiter pins one kernel thread for the duration. Channels and the netpoller +// multiplex thousands of waiters onto a few threads; raw futex waiters do +// not. Fine for one watcher per process, wrong as a general Go pattern. +// +// Go atomics are sequentially consistent, so the seqlock uses atomic ops on +// the seq word around a plain copy of the slot bytes; a torn copy is +// detected by the seq re-check and retried, as in the other languages. +package main + +import ( + "fmt" + "os" + "runtime" + "sort" + "strconv" + "sync/atomic" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + fileSize = 4096 + offSeq = 8 + offFutex = 12 + offCounter = 16 + offSlots = 64 + slotCount = 8 + keySize = 24 + valSize = 40 + slotSize = 64 + slotsBytes = slotCount * slotSize +) + +var magic = [8]byte{'S', 'H', 'K', 'V', '2', 0, 0, 0} + +// futex(2) op codes (uapi linux/futex.h); x/sys/unix does not export them. +// No FUTEX_PRIVATE_FLAG: the whole point is waking across processes. +const ( + futexWaitOp = 0 // FUTEX_WAIT + futexWakeOp = 1 // FUTEX_WAKE +) + +// --- the shared file ------------------------------------------------------- + +type shm struct { + f *os.File + m []byte +} + +func (s *shm) seq() *uint32 { return (*uint32)(unsafe.Pointer(&s.m[offSeq])) } +func (s *shm) futex() *uint32 { return (*uint32)(unsafe.Pointer(&s.m[offFutex])) } +func (s *shm) counter() *uint64 { return (*uint64)(unsafe.Pointer(&s.m[offCounter])) } + +func (s *shm) close() { + _ = unix.Munmap(s.m) + _ = s.f.Close() +} + +func openShm(path string, create bool) (*shm, error) { + flags := os.O_RDWR + if create { + flags |= os.O_CREATE + } + f, err := os.OpenFile(path, flags, 0o644) + if err != nil { + return nil, err // "open : no such file or directory" + } + if create { + if err := f.Truncate(fileSize); err != nil { + f.Close() + return nil, fmt.Errorf("ftruncate %s: %w", path, err) + } + } else { + st, err := f.Stat() + if err != nil { + f.Close() + return nil, fmt.Errorf("fstat %s: %w", path, err) + } + if st.Size() < fileSize { + f.Close() + return nil, fmt.Errorf("%s: bad magic (want SHKV2)", path) + } + } + m, err := unix.Mmap(int(f.Fd()), 0, fileSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) + if err != nil { + f.Close() + return nil, fmt.Errorf("mmap %s: %w", path, err) + } + s := &shm{f: f, m: m} + if create { + clear(s.m) + copy(s.m, magic[:]) + } else if string(s.m[:8]) != string(magic[:]) { + s.close() + return nil, fmt.Errorf("%s: bad magic (want SHKV2)", path) + } + return s, nil +} + +// --- seqlock --------------------------------------------------------------- + +func seqlockWrite(s *shm, mutate func()) { + atomic.AddUint32(s.seq(), 1) // odd: writer active + mutate() + atomic.AddUint32(s.seq(), 1) // even: consistent again +} + +type snapshot struct { + counter uint64 + slots [slotsBytes]byte +} + +func seqlockRead(s *shm) (snapshot, error) { + for attempt := 0; attempt < 1_000_000; attempt++ { + s1 := atomic.LoadUint32(s.seq()) + if s1&1 != 0 { // writer mid-update: retry + runtime.Gosched() + continue + } + var snap snapshot + snap.counter = atomic.LoadUint64(s.counter()) + copy(snap.slots[:], s.m[offSlots:offSlots+slotsBytes]) + if atomic.LoadUint32(s.seq()) == s1 { + return snap, nil + } + } + return snapshot{}, fmt.Errorf("seqlock read livelocked") +} + +// --- futex ----------------------------------------------------------------- + +// Shared (non-PRIVATE) futex: the kernel keys on the file's inode, so waiters +// and wakers in different processes rendezvous through the same mapping. +func futexWait(addr *uint32, expected uint32, timeout time.Duration) { + ts := unix.NsecToTimespec(timeout.Nanoseconds()) + // EAGAIN (word already moved on), ETIMEDOUT and EINTR are all normal + // here: the caller re-checks shared state before waiting again. + _, _, _ = unix.Syscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(addr)), + futexWaitOp, uintptr(expected), uintptr(unsafe.Pointer(&ts)), 0, 0) +} + +func futexWakeAll(addr *uint32) { + _, _, _ = unix.Syscall6(unix.SYS_FUTEX, uintptr(unsafe.Pointer(addr)), + futexWakeOp, uintptr(int(^uint32(0)>>1)), 0, 0, 0) +} + +// --- POSIX message queue via raw syscalls ---------------------------------- +// (glibc's mq_open lives in librt; the kernel interface is four syscalls. +// The kernel wants the name WITHOUT the leading slash glibc requires.) + +type mqAttr struct { + Flags int64 + Maxmsg int64 + Msgsize int64 + Curmsgs int64 + _ [4]int64 +} + +func mqOpen(name string) (int, error) { + nptr, err := unix.BytePtrFromString(name) + if err != nil { + return -1, err + } + attr := mqAttr{Maxmsg: 8, Msgsize: 8} + fd, _, errno := unix.Syscall6(unix.SYS_MQ_OPEN, uintptr(unsafe.Pointer(nptr)), + uintptr(unix.O_CREAT|unix.O_EXCL|unix.O_RDWR), 0o600, + uintptr(unsafe.Pointer(&attr)), 0, 0) + if errno != 0 { + return -1, fmt.Errorf("mq_open /%s: %w", name, errno) + } + return int(fd), nil +} + +func mqUnlink(name string) { + nptr, err := unix.BytePtrFromString(name) + if err != nil { + return + } + _, _, _ = unix.Syscall(unix.SYS_MQ_UNLINK, uintptr(unsafe.Pointer(nptr)), 0, 0) +} + +func mqDeadline() unix.Timespec { + var ts unix.Timespec + _ = unix.ClockGettime(unix.CLOCK_REALTIME, &ts) + ts.Sec += 10 + return ts +} + +func mqSend(fd int, id uint64) error { + var buf [8]byte + for i := 0; i < 8; i++ { + buf[i] = byte(id >> (8 * i)) // little-endian, same as the C++/Rust memcpy + } + ts := mqDeadline() + _, _, errno := unix.Syscall6(unix.SYS_MQ_TIMEDSEND, uintptr(fd), + uintptr(unsafe.Pointer(&buf[0])), 8, 0, uintptr(unsafe.Pointer(&ts)), 0) + if errno != 0 { + return fmt.Errorf("mq_timedsend: %w", errno) + } + return nil +} + +func mqReceive(fd int) (uint64, error) { + var buf [8]byte + for { + ts := mqDeadline() + n, _, errno := unix.Syscall6(unix.SYS_MQ_TIMEDRECEIVE, uintptr(fd), + uintptr(unsafe.Pointer(&buf[0])), 8, 0, uintptr(unsafe.Pointer(&ts)), 0) + if errno == unix.EINTR { + continue + } + if errno != 0 { + return 0, fmt.Errorf("mq_timedreceive: %w", errno) + } + if n != 8 { + return 0, fmt.Errorf("mq_timedreceive: short message (%d bytes)", n) + } + var id uint64 + for i := 0; i < 8; i++ { + id |= uint64(buf[i]) << (8 * i) + } + return id, nil + } +} + +// --- helpers --------------------------------------------------------------- + +func cstr(b []byte) string { + for i, c := range b { + if c == 0 { + return string(b[:i]) + } + } + return string(b) +} + +func writeSlot(s *shm, id uint64, key, val string) { + off := offSlots + int((id-1)%slotCount)*slotSize + slot := s.m[off : off+slotSize] + clear(slot) + copy(slot[:keySize-1], key) + copy(slot[keySize:keySize+valSize-1], val) +} + +func readSlot(snap *snapshot, id uint64) (string, string) { + off := int((id-1)%slotCount) * slotSize + slot := snap.slots[off : off+slotSize] + return cstr(slot[:keySize]), cstr(slot[keySize:]) +} + +// --- serve ----------------------------------------------------------------- + +func cmdServe(path string, updates, intervalMs uint64) error { + s, err := openShm(path, true) + if err != nil { + return err + } + defer s.close() + fmt.Printf("serve: file=%s updates=%d interval_ms=%d\n", path, updates, intervalMs) + for k := uint64(1); k <= updates; k++ { + time.Sleep(time.Duration(intervalMs) * time.Millisecond) + key := fmt.Sprintf("k%d", k) + val := fmt.Sprintf("value-%d", k) + seqlockWrite(s, func() { + writeSlot(s, k, key, val) + atomic.StoreUint64(s.counter(), k) + }) + atomic.StoreUint32(s.futex(), uint32(k)) + futexWakeAll(s.futex()) + fmt.Printf("published update %d: %s=%s\n", k, key, val) + } + fmt.Printf("serve: complete updates=%d\n", updates) + return nil +} + +// --- watch ----------------------------------------------------------------- + +func cmdWatch(path string, events uint64) error { + s, err := openShm(path, false) + if err != nil { + return err + } + defer s.close() + fmt.Printf("watch: file=%s events=%d\n", path, events) + deadline := time.Now().Add(30 * time.Second) + var last, printed uint64 + for printed < events { + if time.Now().After(deadline) { + return fmt.Errorf("watch: timed out waiting for updates") + } + snap, err := seqlockRead(s) + if err != nil { + return err + } + if snap.counter > last { + // The slot ring holds the last slotCount updates, so a watcher + // that slept through a wake can still back-fill missed ids. + for id := last + 1; id <= snap.counter && printed < events; id++ { + key, val := readSlot(&snap, id) + fmt.Printf("observed update %d: %s=%s\n", id, key, val) + printed++ + } + last = snap.counter + continue + } + w := atomic.LoadUint32(s.futex()) + if w != uint32(last) { + continue // moved on while we looked: re-read now + } + futexWait(s.futex(), w, 2*time.Second) + } + fmt.Printf("watch: complete events=%d\n", events) + return nil +} + +// --- bench ----------------------------------------------------------------- + +func cmdBench(path string, rounds uint64, channel string) error { + s, err := openShm(path, true) + if err != nil { + return err + } + defer s.close() + + mqFd := -1 + if channel == "mq" { + name := fmt.Sprintf("shmkv-bench-%d", os.Getpid()) + mqFd, err = mqOpen(name) + if err != nil { + return err + } + defer func() { + unix.Close(mqFd) + mqUnlink(name) + }() + } + + sendTs := make([]time.Time, rounds) + recvTs := make([]time.Time, rounds) + var ack atomic.Uint64 + done := make(chan error, 1) + + go func() { + for i := uint64(1); i <= rounds; i++ { + switch channel { + case "futex": + for { + w := atomic.LoadUint32(s.futex()) + if w >= uint32(i) { + break + } + futexWait(s.futex(), w, 200*time.Millisecond) + } + case "poll": + // Sleep FIRST, then look: by construction every observation + // costs at least one full 1 ms nap — what futex wakes avoid. + for { + time.Sleep(time.Millisecond) + if atomic.LoadUint64(s.counter()) >= i { + break + } + } + case "mq": + if _, err := mqReceive(mqFd); err != nil { + done <- err + return + } + } + recvTs[i-1] = time.Now() + ack.Store(i) + } + done <- nil + }() + + spinDeadline := time.Now().Add(30 * time.Second) + for i := uint64(1); i <= rounds; i++ { + // Spin until the consumer acknowledged round i-1 so rounds never + // overlap (overlap would measure queueing, not wakeup latency). + for ack.Load() != i-1 { + if time.Now().After(spinDeadline) { + return fmt.Errorf("bench: consumer stalled") + } + runtime.Gosched() + } + sendTs[i-1] = time.Now() + if channel == "mq" { + if err := mqSend(mqFd, i); err != nil { + return err + } + } else { + k := i + seqlockWrite(s, func() { atomic.StoreUint64(s.counter(), k) }) + atomic.StoreUint32(s.futex(), uint32(k)) + if channel == "futex" { + futexWakeAll(s.futex()) + } + } + } + + select { + case err := <-done: + if err != nil { + return err + } + case <-time.After(30 * time.Second): + return fmt.Errorf("bench: consumer did not finish") + } + + us := make([]int64, rounds) + for i := range us { + d := recvTs[i].Sub(sendTs[i]).Microseconds() + if d < 0 { + d = 0 + } + us[i] = d + } + sort.Slice(us, func(a, b int) bool { return us[a] < us[b] }) + pct := func(p uint64) int64 { + idx := rounds * p / 100 + if idx >= rounds { + idx = rounds - 1 + } + return us[idx] + } + fmt.Printf("bench: channel=%s rounds=%d p50_us=%d p99_us=%d\n", + channel, rounds, pct(50), pct(99)) + return nil +} + +// --- CLI ------------------------------------------------------------------- + +func usage() { + fmt.Fprintln(os.Stderr, "usage: shmkv serve FILE [--updates N] [--interval-ms T]") + fmt.Fprintln(os.Stderr, " shmkv watch FILE [--events N]") + fmt.Fprintln(os.Stderr, " shmkv bench FILE [--rounds N] [--channel futex|mq|poll]") + os.Exit(2) +} + +func parseU64(s string) uint64 { + v, err := strconv.ParseUint(s, 10, 64) + if err != nil || v == 0 || v > 1_000_000 { + usage() + } + return v +} + +func main() { + args := os.Args[1:] + if len(args) < 2 { + usage() + } + cmd, file := args[0], args[1] + + updates, intervalMs, events, rounds := uint64(8), uint64(100), uint64(4), uint64(100) + channel := "futex" + for i := 2; i < len(args); i += 2 { + if i+1 >= len(args) { + usage() + } + flag, val := args[i], args[i+1] + switch { + case flag == "--updates" && cmd == "serve": + updates = parseU64(val) + case flag == "--interval-ms" && cmd == "serve": + intervalMs = parseU64(val) + case flag == "--events" && cmd == "watch": + events = parseU64(val) + case flag == "--rounds" && cmd == "bench": + rounds = parseU64(val) + case flag == "--channel" && cmd == "bench": + channel = val + default: + usage() + } + } + + var err error + switch cmd { + case "serve": + err = cmdServe(file, updates, intervalMs) + case "watch": + err = cmdWatch(file, events) + case "bench": + if channel != "futex" && channel != "mq" && channel != "poll" { + usage() + } + err = cmdBench(file, rounds, channel) + default: + usage() + } + if err != nil { + fmt.Fprintf(os.Stderr, "shmkv: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/20-shared-memory-coordination/rust/Cargo.lock b/examples/20-shared-memory-coordination/rust/Cargo.lock new file mode 100644 index 0000000..0683e85 --- /dev/null +++ b/examples/20-shared-memory-coordination/rust/Cargo.lock @@ -0,0 +1,74 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shmkv" +version = "0.1.0" +dependencies = [ + "anyhow", + "libc", + "rustix", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/examples/20-shared-memory-coordination/rust/Cargo.toml b/examples/20-shared-memory-coordination/rust/Cargo.toml new file mode 100644 index 0000000..3b1184f --- /dev/null +++ b/examples/20-shared-memory-coordination/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "shmkv" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "app" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +libc = "0.2" +rustix = { version = "1", features = ["fs", "mm"] } diff --git a/examples/20-shared-memory-coordination/rust/demo.sh b/examples/20-shared-memory-coordination/rust/demo.sh new file mode 100755 index 0000000..220feea --- /dev/null +++ b/examples/20-shared-memory-coordination/rust/demo.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +BIN=target/release/app + +build() { + cargo build --release +} + +run() { + if [[ -n "${TARGET:-}" ]]; then + local repo_root + repo_root=$(cd ../../.. && pwd) + "$repo_root/scripts/lab/deploy-to-vm.sh" "$TARGET" "$BIN" -- "$@" + else + "./$BIN" "$@" + fi +} + +case "${1:-}" in + build) build ;; + run) shift; run "$@" ;; + "") build; run ;; + *) echo "usage: $0 [build|run [args...]]" >&2; exit 2 ;; +esac diff --git a/examples/20-shared-memory-coordination/rust/rust-toolchain.toml b/examples/20-shared-memory-coordination/rust/rust-toolchain.toml new file mode 100644 index 0000000..4099f1a --- /dev/null +++ b/examples/20-shared-memory-coordination/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.97.1" +components = ["rustfmt", "clippy", "rust-src"] diff --git a/examples/20-shared-memory-coordination/rust/src/main.rs b/examples/20-shared-memory-coordination/rust/src/main.rs new file mode 100644 index 0000000..c7a0ada --- /dev/null +++ b/examples/20-shared-memory-coordination/rust/src/main.rs @@ -0,0 +1,513 @@ +// shmkv v1 — cross-process coordination over a shared-memory key-value file. +// +// Same SHKV2 layout as the C++ and Go versions: +// +// offset size field +// 0 8 magic "SHKV2\0\0\0" +// 8 4 seqlock word (u32; odd = writer in critical section) +// 12 4 futex word (u32; low 32 bits of the update counter) +// 16 8 update counter (u64; total updates published) +// 24 40 reserved +// 64 512 8 slots x 64 bytes: key[24] NUL-padded, value[40] NUL-padded +// +// The file is opened through rustix (OwnedFd — the descriptor closes on +// drop), the mapping is a small RAII type that munmaps on drop, and the +// futex is the raw syscall via libc::syscall — no wrapper crate, so the +// FUTEX_WAIT/FUTEX_WAKE contract stays visible. The header words are +// accessed as &AtomicU32/&AtomicU64 references into the mapping; the slot +// bytes are copied non-atomically under the seqlock, and a torn copy is +// detected by the seq re-check and retried — the standard seqlock trade-off. + +use std::os::fd::{AsFd, OwnedFd}; +use std::process::ExitCode; +use std::ptr; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering, fence}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, bail}; +use rustix::fs::{Mode, OFlags, fstat, ftruncate, open}; +use rustix::mm::{MapFlags, ProtFlags, mmap, munmap}; + +const MAGIC: [u8; 8] = *b"SHKV2\0\0\0"; +const FILE_SIZE: usize = 4096; +const OFF_SEQ: usize = 8; +const OFF_FUTEX: usize = 12; +const OFF_COUNTER: usize = 16; +const OFF_SLOTS: usize = 64; +const SLOT_COUNT: u64 = 8; +const KEY_SIZE: usize = 24; +const VAL_SIZE: usize = 40; +const SLOT_SIZE: usize = 64; +const SLOTS_BYTES: usize = SLOT_COUNT as usize * SLOT_SIZE; + +// --- the shared file ------------------------------------------------------- + +struct Shm { + _fd: OwnedFd, // keeps the descriptor alive; closed on drop + base: *mut u8, +} + +// The raw pointer targets a MAP_SHARED mapping that lives as long as self. +unsafe impl Send for Shm {} +unsafe impl Sync for Shm {} + +impl Drop for Shm { + fn drop(&mut self) { + unsafe { + let _ = munmap(self.base.cast(), FILE_SIZE); + } + } +} + +impl Shm { + fn seq(&self) -> &AtomicU32 { + unsafe { AtomicU32::from_ptr(self.base.add(OFF_SEQ).cast()) } + } + fn futex_word(&self) -> &AtomicU32 { + unsafe { AtomicU32::from_ptr(self.base.add(OFF_FUTEX).cast()) } + } + fn counter(&self) -> &AtomicU64 { + unsafe { AtomicU64::from_ptr(self.base.add(OFF_COUNTER).cast()) } + } + fn slots(&self) -> *mut u8 { + unsafe { self.base.add(OFF_SLOTS) } + } +} + +fn open_shm(path: &str, create: bool) -> Result { + let mut flags = OFlags::RDWR | OFlags::CLOEXEC; + if create { + flags |= OFlags::CREATE; + } + let fd = open(path, flags, Mode::from_raw_mode(0o644)) + .map_err(std::io::Error::from) + .with_context(|| format!("open {path}"))?; + if create { + ftruncate(fd.as_fd(), FILE_SIZE as u64) + .map_err(std::io::Error::from) + .with_context(|| format!("ftruncate {path}"))?; + } else { + let st = fstat(fd.as_fd()) + .map_err(std::io::Error::from) + .with_context(|| format!("fstat {path}"))?; + if (st.st_size as u64) < FILE_SIZE as u64 { + bail!("{path}: bad magic (want SHKV2)"); + } + } + let base = unsafe { + mmap( + ptr::null_mut(), + FILE_SIZE, + ProtFlags::READ | ProtFlags::WRITE, + MapFlags::SHARED, + fd.as_fd(), + 0, + ) + .map_err(std::io::Error::from) + .with_context(|| format!("mmap {path}"))? + } + .cast::(); + let shm = Shm { _fd: fd, base }; + if create { + unsafe { + ptr::write_bytes(shm.base, 0, FILE_SIZE); + ptr::copy_nonoverlapping(MAGIC.as_ptr(), shm.base, MAGIC.len()); + } + } else { + let head = unsafe { std::slice::from_raw_parts(shm.base, MAGIC.len()) }; + if head != MAGIC { + bail!("{path}: bad magic (want SHKV2)"); + } + } + Ok(shm) +} + +// --- seqlock --------------------------------------------------------------- + +fn seqlock_write(shm: &Shm, mutate: impl FnOnce()) { + shm.seq().fetch_add(1, Ordering::Relaxed); // odd: writer active + fence(Ordering::Release); + mutate(); + shm.seq().fetch_add(1, Ordering::Release); // even: consistent again +} + +struct Snapshot { + counter: u64, + slots: [u8; SLOTS_BYTES], +} + +fn seqlock_read(shm: &Shm) -> Result { + for _ in 0..1_000_000 { + let s1 = shm.seq().load(Ordering::Acquire); + if s1 & 1 != 0 { + std::thread::yield_now(); // writer mid-update: retry + continue; + } + let mut snap = Snapshot { counter: shm.counter().load(Ordering::Relaxed), slots: [0; SLOTS_BYTES] }; + unsafe { + ptr::copy_nonoverlapping(shm.slots(), snap.slots.as_mut_ptr(), SLOTS_BYTES); + } + fence(Ordering::Acquire); + if shm.seq().load(Ordering::Relaxed) == s1 { + return Ok(snap); + } + } + bail!("seqlock read livelocked") +} + +// --- futex ----------------------------------------------------------------- + +// Shared (non-PRIVATE) futex: the kernel keys on the file's inode, so waiters +// and wakers in different processes rendezvous through the same mapping. +fn futex_wait(word: &AtomicU32, expected: u32, timeout: Duration) { + let ts = libc::timespec { + tv_sec: timeout.as_secs() as libc::time_t, + tv_nsec: timeout.subsec_nanos() as libc::c_long, + }; + // EAGAIN (word already moved on), ETIMEDOUT and EINTR are all normal + // here: the caller re-checks shared state before waiting again. + unsafe { + libc::syscall( + libc::SYS_futex, + word.as_ptr(), + libc::FUTEX_WAIT, + expected, + &ts as *const libc::timespec, + 0usize, + 0u32, + ); + } +} + +fn futex_wake_all(word: &AtomicU32) { + unsafe { + libc::syscall( + libc::SYS_futex, + word.as_ptr(), + libc::FUTEX_WAKE, + i32::MAX, + 0usize, + 0usize, + 0u32, + ); + } +} + +// --- POSIX message queue (libc wrappers) ----------------------------------- + +/// mq_close + mq_unlink on drop — no queue name survives a bench run. +struct MessageQueue { + mqd: libc::mqd_t, + name: std::ffi::CString, +} + +impl MessageQueue { + fn create(name: &str) -> Result { + let cname = std::ffi::CString::new(name)?; + let mut attr: libc::mq_attr = unsafe { std::mem::zeroed() }; + attr.mq_maxmsg = 8; + attr.mq_msgsize = 8; + let mqd = unsafe { + libc::mq_open( + cname.as_ptr(), + libc::O_CREAT | libc::O_EXCL | libc::O_RDWR, + 0o600 as libc::c_uint, + &mut attr as *mut libc::mq_attr, + ) + }; + if mqd == -1 { + return Err(std::io::Error::last_os_error()).with_context(|| format!("mq_open {name}")); + } + Ok(Self { mqd, name: cname }) + } +} + +impl Drop for MessageQueue { + fn drop(&mut self) { + unsafe { + libc::mq_close(self.mqd); + libc::mq_unlink(self.name.as_ptr()); + } + } +} + +fn mq_deadline() -> libc::timespec { + let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 }; + unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut ts) }; + ts.tv_sec += 10; + ts +} + +fn mq_send_id(mq: &MessageQueue, id: u64) -> Result<()> { + let buf = id.to_le_bytes(); + let ts = mq_deadline(); + let rc = unsafe { + libc::mq_timedsend(mq.mqd, buf.as_ptr().cast(), buf.len(), 0, &ts) + }; + if rc != 0 { + return Err(std::io::Error::last_os_error()).context("mq_timedsend"); + } + Ok(()) +} + +fn mq_receive_id(mq: &MessageQueue) -> Result { + let mut buf = [0u8; 8]; + loop { + let ts = mq_deadline(); + let n = unsafe { + libc::mq_timedreceive(mq.mqd, buf.as_mut_ptr().cast(), buf.len(), ptr::null_mut(), &ts) + }; + if n == 8 { + return Ok(u64::from_le_bytes(buf)); + } + let err = std::io::Error::last_os_error(); + if n < 0 && err.raw_os_error() == Some(libc::EINTR) { + continue; + } + return Err(err).context("mq_timedreceive"); + } +} + +// --- helpers --------------------------------------------------------------- + +fn cstr(bytes: &[u8]) -> String { + let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + String::from_utf8_lossy(&bytes[..end]).into_owned() +} + +fn write_slot(shm: &Shm, id: u64, key: &str, val: &str) { + let off = ((id - 1) % SLOT_COUNT) as usize * SLOT_SIZE; + unsafe { + let slot = shm.slots().add(off); + ptr::write_bytes(slot, 0, SLOT_SIZE); + let klen = key.len().min(KEY_SIZE - 1); + ptr::copy_nonoverlapping(key.as_ptr(), slot, klen); + let vlen = val.len().min(VAL_SIZE - 1); + ptr::copy_nonoverlapping(val.as_ptr(), slot.add(KEY_SIZE), vlen); + } +} + +fn read_slot(snap: &Snapshot, id: u64) -> (String, String) { + let off = ((id - 1) % SLOT_COUNT) as usize * SLOT_SIZE; + let slot = &snap.slots[off..off + SLOT_SIZE]; + (cstr(&slot[..KEY_SIZE]), cstr(&slot[KEY_SIZE..])) +} + +// --- serve ----------------------------------------------------------------- + +fn cmd_serve(path: &str, updates: u64, interval_ms: u64) -> Result<()> { + let shm = open_shm(path, true)?; + println!("serve: file={path} updates={updates} interval_ms={interval_ms}"); + for k in 1..=updates { + std::thread::sleep(Duration::from_millis(interval_ms)); + let key = format!("k{k}"); + let val = format!("value-{k}"); + seqlock_write(&shm, || { + write_slot(&shm, k, &key, &val); + shm.counter().store(k, Ordering::Relaxed); + }); + shm.futex_word().store(k as u32, Ordering::Release); + futex_wake_all(shm.futex_word()); + println!("published update {k}: {key}={val}"); + } + println!("serve: complete updates={updates}"); + Ok(()) +} + +// --- watch ----------------------------------------------------------------- + +fn cmd_watch(path: &str, events: u64) -> Result<()> { + let shm = open_shm(path, false)?; + println!("watch: file={path} events={events}"); + let deadline = Instant::now() + Duration::from_secs(30); + let mut last = 0u64; + let mut printed = 0u64; + while printed < events { + if Instant::now() > deadline { + bail!("watch: timed out waiting for updates"); + } + let snap = seqlock_read(&shm)?; + if snap.counter > last { + // The slot ring holds the last SLOT_COUNT updates, so a watcher + // that slept through a wake can still back-fill missed ids. + let mut id = last + 1; + while id <= snap.counter && printed < events { + let (key, val) = read_slot(&snap, id); + println!("observed update {id}: {key}={val}"); + printed += 1; + id += 1; + } + last = snap.counter; + continue; + } + let w = shm.futex_word().load(Ordering::Acquire); + if w != last as u32 { + continue; // moved on while we looked: re-read now + } + futex_wait(shm.futex_word(), w, Duration::from_secs(2)); + } + println!("watch: complete events={events}"); + Ok(()) +} + +// --- bench ----------------------------------------------------------------- + +fn cmd_bench(path: &str, rounds: u64, channel: &str) -> Result<()> { + let shm = open_shm(path, true)?; + let mq = if channel == "mq" { + Some(MessageQueue::create(&format!( + "/shmkv-bench-{}", + std::process::id() + ))?) + } else { + None + }; + + let ack = AtomicU64::new(0); + let n = rounds as usize; + let mut send_ts = vec![Instant::now(); n]; + + let recv_ts = std::thread::scope(|scope| -> Result> { + let consumer = scope.spawn(|| -> Result> { + let mut recv = vec![Instant::now(); n]; + for i in 1..=rounds { + match channel { + "futex" => loop { + let w = shm.futex_word().load(Ordering::Acquire); + if w >= i as u32 { + break; + } + futex_wait(shm.futex_word(), w, Duration::from_millis(200)); + }, + "poll" => loop { + // Sleep FIRST, then look: by construction every + // observation costs at least one full 1 ms nap. + std::thread::sleep(Duration::from_millis(1)); + if shm.counter().load(Ordering::Acquire) >= i { + break; + } + }, + _ => { + mq_receive_id(mq.as_ref().unwrap())?; + } + } + recv[i as usize - 1] = Instant::now(); + ack.store(i, Ordering::Release); + } + Ok(recv) + }); + + let spin_deadline = Instant::now() + Duration::from_secs(30); + for i in 1..=rounds { + // Spin until the consumer acknowledged round i-1 so rounds never + // overlap (overlap would measure queueing, not wakeup latency). + while ack.load(Ordering::Acquire) != i - 1 { + if Instant::now() > spin_deadline { + bail!("bench: consumer stalled"); + } + std::hint::spin_loop(); + } + send_ts[i as usize - 1] = Instant::now(); + if channel == "mq" { + mq_send_id(mq.as_ref().unwrap(), i)?; + } else { + seqlock_write(&shm, || shm.counter().store(i, Ordering::Relaxed)); + shm.futex_word().store(i as u32, Ordering::Release); + if channel == "futex" { + futex_wake_all(shm.futex_word()); + } + } + } + consumer.join().expect("consumer thread panicked") + })?; + + let mut us: Vec = (0..n) + .map(|i| { + recv_ts[i] + .checked_duration_since(send_ts[i]) + .unwrap_or_default() + .as_micros() as u64 + }) + .collect(); + us.sort_unstable(); + let pct = |p: u64| { + let idx = (rounds * p / 100).min(rounds - 1) as usize; + us[idx] + }; + println!( + "bench: channel={channel} rounds={rounds} p50_us={} p99_us={}", + pct(50), + pct(99) + ); + Ok(()) +} + +// --- CLI ------------------------------------------------------------------- + +fn usage() -> ExitCode { + eprintln!("usage: shmkv serve FILE [--updates N] [--interval-ms T]"); + eprintln!(" shmkv watch FILE [--events N]"); + eprintln!(" shmkv bench FILE [--rounds N] [--channel futex|mq|poll]"); + ExitCode::from(2) +} + +fn parse_u64(s: &str) -> Option { + match s.parse::() { + Ok(v) if v >= 1 && v <= 1_000_000 => Some(v), + _ => None, + } +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + if args.len() < 2 { + return usage(); + } + let (cmd, file) = (args[0].as_str(), args[1].as_str()); + + let (mut updates, mut interval_ms, mut events, mut rounds) = (8u64, 100u64, 4u64, 100u64); + let mut channel = String::from("futex"); + let mut i = 2; + while i < args.len() { + if i + 1 >= args.len() { + return usage(); + } + let (flag, val) = (args[i].as_str(), args[i + 1].as_str()); + match (flag, cmd) { + ("--channel", "bench") => channel = val.to_string(), + ("--updates", "serve") | ("--interval-ms", "serve") | ("--events", "watch") + | ("--rounds", "bench") => { + let Some(v) = parse_u64(val) else { + return usage(); + }; + match flag { + "--updates" => updates = v, + "--interval-ms" => interval_ms = v, + "--events" => events = v, + _ => rounds = v, + } + } + _ => return usage(), + } + i += 2; + } + + let result = match cmd { + "serve" => cmd_serve(file, updates, interval_ms), + "watch" => cmd_watch(file, events), + "bench" => { + if !matches!(channel.as_str(), "futex" | "mq" | "poll") { + return usage(); + } + cmd_bench(file, rounds, &channel) + } + _ => return usage(), + }; + match result { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("shmkv: {e:#}"); + ExitCode::FAILURE + } + } +} diff --git a/examples/20-shared-memory-coordination/verify.lua b/examples/20-shared-memory-coordination/verify.lua new file mode 100644 index 0000000..c64d587 --- /dev/null +++ b/examples/20-shared-memory-coordination/verify.lua @@ -0,0 +1,163 @@ +-- Verify shmkv v1 (20-shared-memory-coordination) for LSP_LANG. +-- +-- Asserts observable behavior, identical across cpp/go/rust: +-- 1. usage errors exit 2; a missing file exits 1; a non-SHKV2 file exits 1 +-- 2. serve + watch as two REAL processes (shell background jobs): while +-- both run, exactly one "/app serve" and one "/app watch" process +-- exists; every one of the N updates is observed with a seqlock- +-- consistent key=value pair; both exit 0 +-- 3. file contents after serve: SHKV2 magic, futex word and u64 update +-- counter equal to N, last value string present in a slot +-- 4. bench prints one parseable row per channel (futex, mq, poll) with +-- integer microsecond percentiles; futex p50 beats poll p50, and poll +-- p50 >= 500 us (the 1 ms sleep-poll pays for itself by construction) +-- 5. no POSIX message queue name is left behind in /dev/mqueue +-- +-- Invoked from the example directory with LSP_LANG (cpp|go|rust) and +-- EXAMPLE_DIR set; the interpreter may be lua or luajit. + +local script_dir = arg[0]:match("(.*/)") or "./" +local repo_root = os.getenv("REPO_ROOT") or (script_dir .. "../..") +package.path = package.path .. ";" .. repo_root .. "/scripts/lib/?.lua" +local checks = require("checks") + +local lang = os.getenv("LSP_LANG") +if lang ~= "cpp" and lang ~= "go" and lang ~= "rust" then + checks.skip("LSP_LANG must be cpp|go|rust (got " .. tostring(lang) .. ")") +end + +local demo = "./demo.sh " .. lang .. " run" + +local wd = checks.run("mktemp -d").out:gsub("%s+$", "") +if wd == "" or not wd:match("^/") then + checks.skip("mktemp -d did not yield a directory") +end + +local function sh(script) + return checks.run(script:gsub("@WD@", wd)) +end + +-- expect_true: route a computed boolean through expect_exit so it lands in +-- the same pass/fail accounting as everything else. +local function expect_true(cond, label) + checks.expect_exit({ exit = cond and 0 or 1 }, 0, label) +end + +-- --------------------------------------------------------------------------- +-- 1. error contract +-- --------------------------------------------------------------------------- + +local noargs = sh(demo) +checks.expect_exit(noargs, 2, lang .. ": no arguments exits 2") +checks.expect_match(noargs.out, "usage: shmkv serve FILE", lang .. ": usage names serve") +checks.expect_match(noargs.out, "bench FILE %[%-%-rounds N%] %[%-%-channel futex|mq|poll%]", + lang .. ": usage names the bench channels") + +local badchan = sh(demo .. " bench @WD@/x.shm --rounds 10 --channel pigeon") +checks.expect_exit(badchan, 2, lang .. ": unknown bench channel exits 2") + +local missing = sh(demo .. " watch @WD@/missing.shm --events 1") +checks.expect_exit(missing, 1, lang .. ": watch on missing file exits 1") +checks.expect_match(missing.out, "shmkv: ", lang .. ": missing-file error has shmkv prefix") +checks.expect_match(missing.out, "[Nn]o such file", lang .. ": missing-file error names ENOENT") + +local badmagic = sh([[ +printf 'garbage' > @WD@/bad.shm +]] .. demo .. [[ watch @WD@/bad.shm --events 1 +]]) +checks.expect_exit(badmagic, 1, lang .. ": watch on non-SHKV2 file exits 1") +checks.expect_match(badmagic.out, "bad magic %(want SHKV2%)", lang .. ": bad-magic error names SHKV2") + +-- --------------------------------------------------------------------------- +-- 2. serve + watch across two real processes +-- --------------------------------------------------------------------------- + +local updates = 6 +local sw = sh([[ +]] .. demo .. [[ serve @WD@/kv.shm --updates ]] .. updates .. [[ --interval-ms 80 \ + > @WD@/serve.out 2>&1 & +spid=$! +# Attach the watcher only once the header is initialized (magic in place). +for i in $(seq 1 100); do + [ "$(head -c 5 @WD@/kv.shm 2>/dev/null)" = "SHKV2" ] && break + sleep 0.02 +done +]] .. demo .. [[ watch @WD@/kv.shm --events ]] .. updates .. [[ \ + > @WD@/watch.out 2>&1 & +wpid=$! +sleep 0.25 +# Bracket trick so this shell's own command line never matches the pattern. +sprocs=$(pgrep -c -f "/ap[p] serve" || true) +wprocs=$(pgrep -c -f "/ap[p] watch" || true) +s_rc=0; wait $spid || s_rc=$? +w_rc=0; wait $wpid || w_rc=$? +echo "sprocs=$sprocs wprocs=$wprocs s_rc=$s_rc w_rc=$w_rc" +echo "--- serve ---"; cat @WD@/serve.out +echo "--- watch ---"; cat @WD@/watch.out +]]) +checks.expect_exit(sw, 0, lang .. ": serve+watch harness exits 0") +checks.expect_match(sw.out, "sprocs=1 wprocs=1", lang .. ": one serve and one watch process alive mid-run") +checks.expect_match(sw.out, "s_rc=0 w_rc=0", lang .. ": both processes exit 0") +checks.expect_match(sw.out, "serve: file=" .. wd .. "/kv%.shm updates=6 interval_ms=80", + lang .. ": serve announces its parameters") +checks.expect_match(sw.out, "serve: complete updates=6", lang .. ": serve completion line") +checks.expect_match(sw.out, "watch: complete events=6", lang .. ": watch completion line") +for k = 1, updates do + checks.expect_match(sw.out, "published update " .. k .. ": k" .. k .. "=value%-" .. k, + lang .. ": serve published update " .. k) + checks.expect_match(sw.out, "observed update " .. k .. ": k" .. k .. "=value%-" .. k, + lang .. ": watch observed update " .. k .. " with consistent value") +end + +-- --------------------------------------------------------------------------- +-- 3. file contents after serve +-- --------------------------------------------------------------------------- + +local file = sh([[ +echo "magic=$(head -c 5 @WD@/kv.shm)" +echo "futexword=$(od -An -t u4 -j 12 -N 4 @WD@/kv.shm | tr -d ' ')" +echo "counter=$(od -An -t u8 -j 16 -N 8 @WD@/kv.shm | tr -d ' ')" +echo "lastvalue=$(strings @WD@/kv.shm | grep -c '^value-6$')" +echo "size=$(stat -c %s @WD@/kv.shm)" +]]) +checks.expect_exit(file, 0, lang .. ": file inspection exits 0") +checks.expect_match(file.out, "magic=SHKV2", lang .. ": file carries SHKV2 magic") +checks.expect_match(file.out, "futexword=6\n", lang .. ": u32 futex word settled at 6") +checks.expect_match(file.out, "counter=6\n", lang .. ": u64 update counter settled at 6") +checks.expect_match(file.out, "lastvalue=1", lang .. ": a slot holds the final value string") +checks.expect_match(file.out, "size=4096", lang .. ": file is exactly one page") + +-- --------------------------------------------------------------------------- +-- 4. bench: three channels, parseable latencies, sane ordering +-- --------------------------------------------------------------------------- + +local rounds = 60 +local p50 = {} +for _, channel in ipairs({ "futex", "mq", "poll" }) do + local r = sh(demo .. " bench @WD@/bench.shm --rounds " .. rounds .. " --channel " .. channel) + checks.expect_exit(r, 0, lang .. ": bench " .. channel .. " exits 0") + checks.expect_match(r.out, + "bench: channel=" .. channel .. " rounds=" .. rounds .. " p50_us=%d+ p99_us=%d+", + lang .. ": bench " .. channel .. " row is parseable") + p50[channel] = tonumber(r.out:match("p50_us=(%d+)")) + local p99 = tonumber(r.out:match("p99_us=(%d+)")) + expect_true(p50[channel] ~= nil and p99 ~= nil and p99 >= p50[channel], + lang .. ": bench " .. channel .. " has p99 >= p50") +end +expect_true(p50.poll ~= nil and p50.poll >= 500, + lang .. ": poll p50 >= 500us by construction (got " .. tostring(p50.poll) .. ")") +expect_true(p50.futex ~= nil and p50.poll ~= nil and p50.futex < p50.poll, + lang .. ": futex p50 (" .. tostring(p50.futex) .. "us) beats poll p50 (" .. + tostring(p50.poll) .. "us)") + +-- --------------------------------------------------------------------------- +-- 5. mq cleanup +-- --------------------------------------------------------------------------- + +local mqleft = sh([[ +echo "leftover=$(ls /dev/mqueue 2>/dev/null | grep -c shmkv-bench || true)" +]]) +checks.expect_match(mqleft.out, "leftover=0", lang .. ": no shmkv-bench queue left in /dev/mqueue") + +checks.run("rm -rf " .. wd) +checks.finish() diff --git a/examples/manifest.yaml b/examples/manifest.yaml index bb84d69..52d0b3e 100644 --- a/examples/manifest.yaml +++ b/examples/manifest.yaml @@ -45,3 +45,43 @@ examples: langs: [cpp, go, rust] mode: local timeout: 300 + - id: 11-process-lifecycle + langs: [cpp, go, rust] + mode: local + timeout: 180 + - id: 12-signals-and-safe-handling + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 13-pidfd-modern-process-mgmt + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 14-identity-and-privilege + langs: [cpp, go, rust] + mode: vm + timeout: 300 + - id: 15-virtual-memory + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 16-mmap-and-shared-mappings + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 17-allocators-and-gc-runtimes + langs: [cpp, go, rust] + mode: local + timeout: 300 + - id: 18-pipes-fifos-splice + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 19-unix-sockets-fd-passing + langs: [cpp, go, rust] + mode: local + timeout: 240 + - id: 20-shared-memory-coordination + langs: [cpp, go, rust] + mode: local + timeout: 300