Skip to content
C++
Domain Track
Difficulty 3/5

Systems Programming in C++

C++ for systems programming — POSIX APIs, process management, signals, memory-mapped files, sockets, file descriptors, and Linux-specific facilities.

TL;DR

Systems programming in C++ means wrapping POSIX/Linux C APIs with RAII. Use std::system_error for error reporting. Always check return values; use errno via std::errc.


RAII File Descriptor

cpp
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <system_error>

class FileDescriptor {
    int fd_ = -1;

public:
    FileDescriptor() = default;
    explicit FileDescriptor(int fd) : fd_{fd} {}

    // Non-copyable, movable
    FileDescriptor(const FileDescriptor&) = delete;
    FileDescriptor& operator=(const FileDescriptor&) = delete;

    FileDescriptor(FileDescriptor&& o) noexcept : fd_{std::exchange(o.fd_, -1)} {}
    FileDescriptor& operator=(FileDescriptor&& o) noexcept {
        if (this != &o) { close(); fd_ = std::exchange(o.fd_, -1); }
        return *this;
    }

    ~FileDescriptor() { close(); }

    int get() const { return fd_; }
    bool valid() const { return fd_ >= 0; }
    int release() { return std::exchange(fd_, -1); }

private:
    void close() { if (fd_ >= 0) ::close(fd_); }
};

// Usage
FileDescriptor fd{::open("/etc/hosts", O_RDONLY)};
if (!fd.valid()) throw std::system_error(errno, std::generic_category());

char buf[1024];
ssize_t n = ::read(fd.get(), buf, sizeof(buf));
// fd closed automatically at end of scope

Process Management

cpp
#include <unistd.h>
#include <sys/wait.h>

// Fork a child process
pid_t child = fork();
if (child < 0) {
    throw std::system_error(errno, std::generic_category(), "fork");
} else if (child == 0) {
    // Child process
    execl("/usr/bin/ls", "ls", "-la", nullptr);
    _exit(1);  // exec failed
} else {
    // Parent process: wait for child
    int status;
    waitpid(child, &status, 0);
    if (WIFEXITED(status))
        std::println("child exited: {}", WEXITSTATUS(status));
}

// Run a command and capture output (popen)
FILE* pipe = popen("date", "r");
if (!pipe) throw std::runtime_error("popen failed");
char line[256];
while (fgets(line, sizeof(line), pipe))
    std::print("{}", line);
pclose(pipe);

Memory-Mapped Files

cpp
#include <sys/mman.h>
#include <sys/stat.h>

class MappedFile {
    void*  addr_ = MAP_FAILED;
    size_t size_ = 0;

public:
    MappedFile(const char* path, bool writable = false) {
        int flags = writable ? O_RDWR : O_RDONLY;
        FileDescriptor fd{::open(path, flags)};
        if (!fd.valid()) throw std::system_error(errno, std::generic_category());

        struct stat st;
        if (fstat(fd.get(), &st) < 0)
            throw std::system_error(errno, std::generic_category());
        size_ = st.st_size;

        int prot = PROT_READ | (writable ? PROT_WRITE : 0);
        addr_ = mmap(nullptr, size_, prot, MAP_SHARED, fd.get(), 0);
        if (addr_ == MAP_FAILED)
            throw std::system_error(errno, std::generic_category());
    }

    ~MappedFile() { if (addr_ != MAP_FAILED) munmap(addr_, size_); }

    MappedFile(const MappedFile&) = delete;
    MappedFile& operator=(const MappedFile&) = delete;

    std::span<const std::byte> bytes() const {
        return {static_cast<const std::byte*>(addr_), size_};
    }
    std::string_view str() const {
        return {static_cast<const char*>(addr_), size_};
    }
    size_t size() const { return size_; }
};

// Parse a large file without reading it into memory
MappedFile f{"/var/log/app.log"};
auto content = f.str();
// Process content as string_view — no copy

POSIX Sockets

cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// TCP server
int setup_server(uint16_t port) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) throw std::system_error(errno, std::generic_category());

    // Allow reuse of port immediately after close
    int yes = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));

    sockaddr_in addr{};
    addr.sin_family      = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port        = htons(port);

    if (bind(sock, (sockaddr*)&addr, sizeof(addr)) < 0)
        throw std::system_error(errno, std::generic_category());
    if (listen(sock, SOMAXCONN) < 0)
        throw std::system_error(errno, std::generic_category());

    return sock;
}

// Accept loop
FileDescriptor server{setup_server(8080)};
while (true) {
    sockaddr_in client_addr{};
    socklen_t   client_len = sizeof(client_addr);
    int client_fd = accept(server.get(), (sockaddr*)&client_addr, &client_len);
    if (client_fd < 0) continue;

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
    std::println("connection from {}", ip);

    FileDescriptor client{client_fd};
    // handle client in a thread
}

Signal Handling

cpp
#include <signal.h>
#include <atomic>

// Use atomic for signal-handler communication — signal-safe
std::atomic<bool> g_running{true};

// Simple signal handler
extern "C" void on_sigint(int) {
    g_running.store(false, std::memory_order_relaxed);
}

int main() {
    struct sigaction sa{};
    sa.sa_handler = on_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // restart interrupted syscalls
    sigaction(SIGINT, &sa, nullptr);
    sigaction(SIGTERM, &sa, nullptr);

    while (g_running.load(std::memory_order_relaxed)) {
        // main loop
    }
    std::println("shutting down");
}

// Block signals in threads, handle in a dedicated signal thread
void signal_thread_setup() {
    sigset_t mask;
    sigfillset(&mask);
    pthread_sigmask(SIG_BLOCK, &mask, nullptr);  // block all in this thread

    // Then create a thread that calls sigwait() to handle signals
}

Pipes and IPC

cpp
#include <unistd.h>

// Anonymous pipe (parent-child communication)
int pipefd[2];
if (pipe(pipefd) < 0) throw std::system_error(errno, std::generic_category());

FileDescriptor read_end{pipefd[0]};
FileDescriptor write_end{pipefd[1]};

pid_t child = fork();
if (child == 0) {
    // Child: close write end, read from pipe
    write_end = {};  // closes write_end
    char buf[256];
    ssize_t n = read(read_end.get(), buf, sizeof(buf));
    // process buf[0..n]
    _exit(0);
}
// Parent: close read end, write to pipe
read_end = {};  // closes read_end
const char msg[] = "hello from parent";
write(write_end.get(), msg, sizeof(msg));
write_end = {};  // close to send EOF
waitpid(child, nullptr, 0);

epoll — Scalable I/O Multiplexing

cpp
#include <sys/epoll.h>

class EventLoop {
    int epfd_;
    std::unordered_map<int, std::function<void(uint32_t)>> handlers_;

public:
    EventLoop() {
        epfd_ = epoll_create1(EPOLL_CLOEXEC);
        if (epfd_ < 0) throw std::system_error(errno, std::generic_category());
    }
    ~EventLoop() { ::close(epfd_); }

    void add(int fd, uint32_t events, std::function<void(uint32_t)> cb) {
        handlers_[fd] = std::move(cb);
        epoll_event ev{};
        ev.events  = events;
        ev.data.fd = fd;
        epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
    }

    void run() {
        std::array<epoll_event, 64> events;
        while (true) {
            int n = epoll_wait(epfd_, events.data(), events.size(), -1);
            for (int i = 0; i < n; ++i) {
                int fd = events[i].data.fd;
                if (auto it = handlers_.find(fd); it != handlers_.end())
                    it->second(events[i].events);
            }
        }
    }
};

EventLoop loop;
loop.add(server_fd, EPOLLIN, [&](uint32_t events) {
    int client = accept(server_fd, nullptr, nullptr);
    loop.add(client, EPOLLIN | EPOLLET, [client](uint32_t ev) {
        char buf[4096];
        ssize_t n = read(client, buf, sizeof(buf));
        if (n <= 0) { ::close(client); return; }
        write(client, buf, n);  // echo
    });
});
loop.run();