System Components

Developing a system component for an operating system in C++ involves a wide range of low-level tasks. The relevant Boost libraries will largely depend on the specifics of your project. Some operating systems may not support all of the features of these libraries, and for low-level tasks, it may be more appropriate to use system APIs directly. For higher-level system operations, or cross-platform tasks, Boost libraries have a lot to offer.

Libraries

Here are some Boost libraries that are useful in building system components:

  • Boost.Filesystem : This library provides a portable way of querying and manipulating paths, files, and directories. It can be very helpful for system-level tasks that need to interact with the file system.

  • Boost.ProgramOptions : This library allows program options to be defined, with types and default values, and their values to be retrieved from the command line, from config files, and programmatically.

  • Boost.System : This library provides simple, light-weight error_code objects that encapsulate system-specific "error codes", distinct from C++ exceptions.

  • Boost.Chrono : This library provides a set of handy features for measuring time, which might be useful for system-level tasks that need to measure or manipulate time.

  • Boost.Asio : This library provides a consistent asynchronous model using a modern C++ approach for network and low-level I/O programming. This might be useful for network-related components or any component that interacts with hardware.

  • Boost.Interprocess : This library provides a way of sharing memory and communicating between processes. It’s useful for creating shared memory regions, handling inter-process communication, managing shared objects, and synchronizing processes.

  • Boost.Thread : This library provides a portable interface for multithreading. It includes features for creating and managing threads, mutexes, condition variables, and futures.

  • Boost.Fiber : A fiber is a lightweight thread of execution. Boost.Fiber provides a framework for creating and managing fibers, which can be useful in some system-level programming tasks.

  • Boost.Container : This provides advanced data structures beyond the ones provided by the C++ standard library, which may be useful in certain scenarios.

  • Boost.Process : This library allows you to create child processes, setup their environment and provides means to communicate with them asynchronously through various streams.

    Note

    The code in this tutorial was written and tested using Microsoft Visual Studio (Visual C++ 2022, Console App project) with Boost version 1.88.0.

Sample System File and Error Handling

Two core features of most systems are in file handling and robust error reporting. For a simpler sample we’ll create an app that uses Boost.Filesystem to manipulate files and directories, and Boost.System to capture and display specific errors.

The following sample creates a directory (example_directory) if it does not exist, writes a file (example_file.txt.) to the directory, reads from the file, and handles system-specific errors. Finally, it cleans up by deleting the file and directory, still handling any errors.

For examples of networking and threading code, refer to Networking and Parallel Computation.

#include <boost/filesystem.hpp>
#include <iostream>
#include <fstream>

// Give the Boost namespaces shorter names to make the example easier to read.
// "fs" is for Boost.Filesystem and "sys" is for Boost.System.
namespace fs = boost::filesystem;
namespace sys = boost::system;


// Report the result of a filesystem operation that uses a Boost.System error_code.
//
// Many Boost.Filesystem functions have overloads that accept an error_code.
// Instead of throwing an exception when something goes wrong, these overloads
// store the error information in the supplied error_code object.
//
// This function demonstrates how that error information can be inspected.
void report_status(const sys::error_code& error_code, const std::string& operation)
{
    if (error_code)
    {
        // error_code::message() converts the platform-specific error into
        // a human-readable description, while value() provides the numeric code.
        std::cerr << "Error while " << operation << ": "
            << error_code.message()
            << " (Code: " << error_code.value() << ")\n";
    }
    else
    {
        // A default-constructed or successfully cleared error_code evaluates
        // to false, meaning that the operation did not report an error.
        std::cout << "All OK while " << operation << '\n';
    }
}


int main()
{
    // A boost::filesystem::path represents a filesystem path.
    // Using path rather than a plain string makes it possible for
    // Boost.Filesystem to handle paths in a platform-independent way.
    fs::path directory_path = "example_directory";

    // The / operator combines paths. This avoids manually inserting
    // platform-specific path separators such as '\' or '/'.
    fs::path text_file_path = directory_path / "example_file.txt";

    // This error_code object will be reused for filesystem operations.
    // Operations that accept an error_code generally leave it unchanged
    // unless they encounter an error, so it is good practice to clear it
    // before reusing it when the distinction matters.
    sys::error_code filesystem_error;


    // -------------------------------------------------------------------------
    // Create the directory if it does not already exist.
    // -------------------------------------------------------------------------

    if (!fs::exists(directory_path))
    {
        // The error_code overload reports an error without throwing an
        // exception. The result can then be examined by report_status().
        fs::create_directory(directory_path, filesystem_error);

        report_status(filesystem_error, "creating directory " + directory_path.string());
    }


    // -------------------------------------------------------------------------
    // Create the file and write some text to it.
    // -------------------------------------------------------------------------

    {
        // std::ofstream is part of the C++ Standard Library, rather than
        // Boost.Filesystem. Boost.Filesystem is being used here to construct
        // the path; the actual file I/O is performed by the standard stream.
        //
        // path::string() converts the Boost path to a std::string suitable
        // for this stream constructor.
        std::ofstream output_file(text_file_path.string());

        if (!output_file)
        {
            std::cerr << "Failed to open file for writing!\n";
            return 1;
        }

        output_file << "Hello, Boost.Filesystem!\n";

        // The output_file object is automatically closed when it goes out
        // of scope. The extra braces above ensure this happens before we
        // subsequently try to read and remove the file.
    }


    // -------------------------------------------------------------------------
    // Open the file again and read the text back.
    // -------------------------------------------------------------------------

    {
        // Use a descriptive name such as input_file rather than "ifs".
        // "ifs" is technically a common abbreviation for ifstream, but it
        // can be confusing when reading code because "if" is also a C++
        // language keyword.
        std::ifstream input_file(text_file_path.string());

        if (!input_file)
        {
            std::cerr << "Failed to open file for reading!\n";
            return 1;
        }

        std::string file_content;
        std::getline(input_file, file_content);

        std::cout << "File content: " << file_content << '\n';

        // As with the output stream, input_file is automatically closed
        // when it goes out of scope.
    }


    // -------------------------------------------------------------------------
    // Remove the file.
    // -------------------------------------------------------------------------

    // Boost.Filesystem::remove() removes a file or an empty directory.
    // The error_code overload lets us handle an error explicitly rather
    // than having Boost.Filesystem throw an exception.
    fs::remove(text_file_path, filesystem_error);

    report_status(filesystem_error, "removing file " + text_file_path.string());


    // -------------------------------------------------------------------------
    // Finally, remove the now-empty directory.
    // -------------------------------------------------------------------------

    fs::remove(directory_path, filesystem_error);

    report_status(filesystem_error, "removing directory " + directory_path.string());


    return 0;
}
Note

Boost.Filesystem ensures directory and file management is platform-independent.

Running this sample should give you:

All OK while creating directory example_directory
File content: Hello, Boost.Filesystem!
All OK while removing file example_directory\example_file.txt
All OK while removing directory example_directory
Tip

The next time you run this code, comment out the code to remove the file and directory at the end. After running the code, locate example_directory, then you can both verify the content of example_file.txt and record the parent directory of example_directory - which you will need in later examples.

Support Configuration Settings

We are now going to include Boost.ProgramOptions to allow configuration settings via command-line arguments and configuration files.

The code now allows users to specify directory and file names, reads settings from a config.ini file, and uses default values when one is not specified or located.

#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <iostream>
#include <fstream>

namespace fs = boost::filesystem;
namespace sys = boost::system;
namespace po = boost::program_options;

//  Check and report system-specific errors
void report_status(const sys::error_code& ec, const std::string& action) {
    if (ec) {
        std::cerr << "Error while " << action << " : " << ec.message()
            << " (Code: " << ec.value() << ")\n";
    }
    else
    {
        std::cout << "All OK while " << action << '\n';
    }
}

int main(int argc, char* argv[]) {

    // Default configuration values
    std::string dir = "default_directory";
    std::string filename = "default_file.txt";
    std::string config_file = "config.ini";

    // Define command-line options
    po::options_description desc("Allowed options");
    desc.add_options()
        ("help,h", "Show help message")
        ("dir,d", po::value<std::string>(&dir), "Directory name")
        ("file,f", po::value<std::string>(&filename), "File name")
        ("config,c", po::value<std::string>(&config_file)->default_value("config.ini"), "Configuration file");

    // Parse command-line options
    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (vm.count("help")) {
        std::cout << desc << std::endl;
        return 0;
    }

    // Read options from configuration file (if available)
    std::ifstream ifs(config_file);
    if (ifs) {
        po::store(po::parse_config_file(ifs, desc), vm);
        po::notify(vm);
    }

    fs::path directory(dir);
    fs::path file = directory / filename;
    sys::error_code ec;

    // Create directory if it doesn't exist
    if (!fs::exists(directory)) {
        fs::create_directory(directory, ec);
        report_status(ec, "creating directory " + dir);
    }

    // Write to the file
    {
        std::ofstream ofs(file.string());
        if (!ofs) {
            std::cerr << "Failed to open file for writing!\n";
            return 1;
        }
        ofs << "Hello, Boost.Program_Options and Boost.Filesystem!\n";
    }

    // Read from the file
    {
        std::ifstream ifs(file.string());
        if (!ifs) {
            std::cerr << "Failed to open file for reading!\n";
            return 1;
        }
        std::string content;
        std::getline(ifs, content);
        std::cout << "File content: " << content << '\n';
    }

    // Remove file
    fs::remove(file, ec);
    report_status(ec, "removing file " + filename);

    // Remove directory
    fs::remove(directory, ec);
    report_status(ec, "removing directory " + dir);

    return 0;
}

The command line options accepted by the sample are:

Option Description

--dir or -d

Specify the directory.

--file or -f

Specify the filename.

--config or -c

Specify the configuration file.

--help or -h

Display available options.

Build the sample, and navigate (in a Command Window) to the directory containing the executable file (.exe) for it.

The following is an example config.ini file, create it and store it to the directory containing the executable. And say you have called the executable cpp-system.exe.

dir = my_directory
file = my_file.txt

The following command lines show how to run with defaults, run with options specified manually, and then run with a config file:

cpp-system

cpp-system --dir=my_data --file=data.txt

cpp-system --config=config.ini

Run these three commands, and verify you get the expected output - something like:

All OK while creating directory peters_directory
File content: Hello, Boost.Program_Options and Boost.Filesystem!
All OK while removing file peters_file.txt
All OK while removing directory peters_directory

Time the System Operations

It might be important to record the time taken for system operations, both in testing and in the operation of a system app. So, let’s integrate Boost.Chrono to measure the time taken for key filesystem operations, such as creating directories, writing to files, reading files, and deleting files.

system timing
#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <boost/chrono.hpp>
#include <iostream>
#include <fstream>

namespace fs = boost::filesystem;
namespace sys = boost::system;
namespace po = boost::program_options;
namespace chrono = boost::chrono;

//  Check and report system-specific errors
void report_status(const sys::error_code& ec, const std::string& action) {
    if (ec) {
        std::cerr << "Error while " << action << ": " << ec.message()
            << " (Code: " << ec.value() << ")\n";
    }
    else
    {
        std::cout << "All OK while " << action << '\n';
    }
}

int main(int argc, char* argv[]) {

    // Default configuration values
    std::string dir = "default_directory";
    std::string filename = "default_file.txt";
    std::string config_file = "config.ini";

    // Define command-line options
    po::options_description desc("Allowed options");
    desc.add_options()
        ("help,h", "Show help message")
        ("dir,d", po::value<std::string>(&dir), "Directory name")
        ("file,f", po::value<std::string>(&filename), "File name")
        ("config,c", po::value<std::string>(&config_file)->default_value("config.ini"), "Configuration file");

    // Parse command-line options
    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (vm.count("help")) {
        std::cout << desc << std::endl;
        return 0;
    }

    // Read options from configuration file (if available)
    std::ifstream ifs_config(config_file);
    if (ifs_config) {
        po::store(po::parse_config_file(ifs_config, desc), vm);
        po::notify(vm);
    }

    fs::path directory(dir);
    fs::path file = directory / filename;
    sys::error_code ec;

    // Measure time for directory creation
    auto start = chrono::steady_clock::now();
    if (!fs::exists(directory)) {
        fs::create_directory(directory, ec);
        report_status(ec, "creating directory " + dir);
    }
    auto end = chrono::steady_clock::now();
    std::cout << "Directory creation took: "
        << chrono::duration_cast<chrono::microseconds>(end - start).count()
        << " microseconds\n";

    // Measure time for writing to file
    start = chrono::steady_clock::now();
    {
        std::ofstream ofs(file.string());
        if (!ofs) {
            std::cerr << "Failed to open file for writing!\n";
            return 1;
        }
        ofs << "Hello, Boost.Program_Options, Boost.Filesystem, and Boost.Chrono!\n";
    }
    end = chrono::steady_clock::now();
    std::cout << "File writing took: "
        << chrono::duration_cast<chrono::microseconds>(end - start).count()
        << " microseconds\n";

    // Measure time for reading from file
    start = chrono::steady_clock::now();
    {
        std::ifstream ifs(file.string());
        if (!ifs) {
            std::cerr << "Failed to open file for reading!\n";
            return 1;
        }
        std::string content;
        std::getline(ifs, content);
        std::cout << "File content: " << content << '\n';
    }
    end = chrono::steady_clock::now();
    std::cout << "File reading took: "
        << chrono::duration_cast<chrono::microseconds>(end - start).count()
        << " microseconds\n";

    // Measure time for file deletion
    start = chrono::steady_clock::now();
    fs::remove(file, ec);
    report_status(ec, "removing file " + filename);
    end = chrono::steady_clock::now();
    std::cout << "File deletion took: "
        << chrono::duration_cast<chrono::microseconds>(end - start).count()
        << " microseconds\n";

    // Measure time for directory deletion
    start = chrono::steady_clock::now();
    fs::remove(directory, ec);
    report_status(ec, "removing directory " + dir);
    end = chrono::steady_clock::now();
    std::cout << "Directory deletion took: "
        << chrono::duration_cast<chrono::microseconds>(end - start).count()
        << " microseconds\n";

    return 0;
}

The following is example output from running the sample:

All OK while creating directory peters_directory
Directory creation took: 769 microseconds
File writing took: 672 microseconds
File content: Hello, Boost.Program_Options, Boost.Filesystem, and Boost.Chrono!
File reading took: 2302 microseconds
All OK while removing file peters_file.txt
File deletion took: 1162 microseconds
All OK while removing directory peters_directory
Directory deletion took: 586 microseconds

Adding timing features to your system operations will help you maintain more robust and performance-aware code, so as code is updated you will have built in the checks and balances so that if something goes awry - you will be able to capture and correct it early in the development cycle.

Handle Atomic Operations and Racing Threads

In systems programming, having atomic variables can often be useful. A normal increment of a counter has to read a current value, add 1, then write the result back. If threads are accessing the variable at the same time, increments can be missed. The solution is for these three operations to be treated as one - an atomic (that is, indivisible) operation - and Boost.Atomic provides this feature.

This first example shows multiple threads (managed by Boost.Thread) safely incrementing an atomic counter:

#include <boost/thread.hpp>
#include <boost/atomic.hpp>
#include <iostream>

boost::atomic<int> counter(0);

void worker(int id)
{
    for (int i = 0; i < 5; ++i)
    {
        int value = ++counter;

        std::cout
            << "Thread "
            << id
            << " incremented counter to "
            << value
            << "\n";

        boost::this_thread::sleep_for(
            boost::chrono::milliseconds(100));
    }
}

int main()
{
    boost::thread t1([] { worker(1); });
    boost::thread t2([] { worker(2); });

    t1.join();
    t2.join();
}

Run the program several times:

Thread 1 incremented counter to 1
Thread 2 incremented counter to 2
Thread 1 incremented counter to 3
Thread 2 incremented counter to 4
Thread Thread 2 incremented counter to 6
1 incremented counter to 5
Thread 1 incremented counter to 7
Thread 2 incremented counter to 8
Thread 2 incremented counter to 9
Thread 1 incremented counter to 10

You will notice that sometimes the text output interleaves "Thread Thread 2 incremented counter to 6 incremented counter to 5", which can look weird. The problem is not the atomic counter. The counter is working correctly. The problem is that std::cout is shared state too.

The simplest solution to this is to protect output with a Boost.Thread mutex:

#include <boost/thread.hpp>
#include <boost/atomic.hpp>
#include <iostream>

boost::atomic<int> counter(0);
boost::mutex cout_mutex;

void worker(int id)
{
    for (int i = 0; i < 5; i++)
    {
        int value = ++counter;

        {
            boost::lock_guard<boost::mutex> lock(cout_mutex);

            std::cout
                << "Thread "
                << id
                << " incremented counter to "
                << value
                << "\n";
        }
    }
}


int main()
{
    boost::thread t1([] { worker(1); });
    boost::thread t2([] { worker(2); });

    t1.join();
    t2.join();
}

Run this program several times, and notice there will be no interleaving:

Thread 2 incremented counter to 1
Thread 1 incremented counter to 2
Thread 2 incremented counter to 3
Thread 2 incremented counter to 5
Thread 2 incremented counter to 6
Thread 2 incremented counter to 7
Thread 1 incremented counter to 4
Thread 1 incremented counter to 8
Thread 1 incremented counter to 9
Thread 1 incremented counter to 10

You will notice though that the increments are out of order.

The important point here is that threads race naturally, the increment order is nondeterministic. Best not to write code that assumes otherwise.

A possible issue with the above code though is that a thread’s output (cout<<) can block other threads working. To remove this burden, the following example does not write directly to std::cout, each worker creates a log message that goes into a Boost.Lockfree managed lock-free queue. A dedicated logger thread then removes messages and prints them safely:

#include <boost/thread.hpp>
#include <boost/atomic.hpp>
#include <boost/lockfree/queue.hpp>
#include <iostream>

// --------------------------------------------------
// Shared atomic counter
// --------------------------------------------------

boost::atomic<int> counter(0);

// --------------------------------------------------
// Log message object
// (queue stores pointers, not std::string directly)
// --------------------------------------------------

struct LogMessage
{
    std::string text;

    LogMessage(const std::string& s)
        : text(s)
    {
    }
};

// Lock-free queue for messages
boost::lockfree::queue<LogMessage*> log_queue(128);

// Signal when workers are finished
boost::atomic<bool> done(false);

// --------------------------------------------------
// Worker threads
// --------------------------------------------------

void worker(int id)
{
    for (int i = 0; i < 15; i++)
    {
        int value = ++counter;

        std::string msg =
            "Thread " +
            std::to_string(id) +
            " incremented counter to " +
            std::to_string(value);

        // Allocate message and push to queue
        while (!log_queue.push(new LogMessage(msg)))
        {
            // queue full, retry
        }

        boost::this_thread::sleep_for(
            boost::chrono::milliseconds(100));
    }
}

// --------------------------------------------------
// Logger thread
// --------------------------------------------------

void logger()
{
    while (!done || !log_queue.empty())
    {
        LogMessage* msg = nullptr;

        if (log_queue.pop(msg))
        {
            std::cout << msg->text << "\n";

            delete msg;
        }
        else
        {
            boost::this_thread::sleep_for(
                boost::chrono::milliseconds(10));
        }
    }
}

// --------------------------------------------------
// Main
// --------------------------------------------------

int main()
{
    boost::thread log_thread(logger);

    boost::thread t1([] { worker(1); });
    boost::thread t2([] { worker(2); });
    boost::thread t3([] { worker(3); });

    t1.join();
    t2.join();
    t3.join();

    done = true;

    log_thread.join();

    std::cout
        << "Final counter value: "
        << counter.load()
        << "\n";
}

Notice that the log_thread.join(); does not interfere with the other threads. Also notice the counter output is nondeterministic by nature (blank lines added for clarity):

Thread 2 incremented counter to 1
Thread 3 incremented counter to 2
Thread 1 incremented counter to 3
Thread 3 incremented counter to 4
Thread 2 incremented counter to 5
Thread 1 incremented counter to 6

Thread 1 incremented counter to 8
Thread 2 incremented counter to 7

Thread 3 incremented counter to 9
Thread 2 incremented counter to 10
Thread 1 incremented counter to 11
Thread 3 incremented counter to 12
Thread 1 incremented counter to 14
Thread 3 incremented counter to 13
Thread 2 incremented counter to 15
Final counter value: 15

Next Steps

The onus on a systems program is to work reliably, fast, and, critically, with minimal impact on resources.

The line new LogMessage(msg) in the last sample typically would not be used in a production system, because of its frequent heap allocations. For an industrial strength production system, consider using an object pool, or fixed-size buffers, or a ring buffer to handle message logging.