From 824cc5a96f8a6f666a5c963897c8ab9076b48373 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Mon, 17 Aug 2026 15:20:57 +0200 Subject: kapi: add power management support to drivers --- kapi/kapi/devices.hpp | 1 + kapi/kapi/devices/driver.hpp | 15 +++ kapi/kapi/devices/power.hpp | 38 +++++++ kernel/CMakeLists.txt | 1 + kernel/kapi/devices/driver.cpp | 12 +++ kernel/kapi/devices/power.cpp | 93 +++++++++++++++++ kernel/kapi/devices/power.tests.cpp | 203 ++++++++++++++++++++++++++++++++++++ 7 files changed, 363 insertions(+) create mode 100644 kapi/kapi/devices/power.hpp create mode 100644 kernel/kapi/devices/power.cpp create mode 100644 kernel/kapi/devices/power.tests.cpp diff --git a/kapi/kapi/devices.hpp b/kapi/kapi/devices.hpp index b20e3546..0781357c 100644 --- a/kapi/kapi/devices.hpp +++ b/kapi/kapi/devices.hpp @@ -9,6 +9,7 @@ #include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export +#include // IWYU pragma: export #include // IWYU pragma: export #include diff --git a/kapi/kapi/devices/driver.hpp b/kapi/kapi/devices/driver.hpp index 20f66536..b46091fe 100644 --- a/kapi/kapi/devices/driver.hpp +++ b/kapi/kapi/devices/driver.hpp @@ -46,6 +46,21 @@ namespace kapi::devices //! @param device The device to release. virtual auto unbind(device & device) -> void = 0; + //! Suspend a device bound by this driver. + //! + //! This function will be invoked in in a depth-first manner, meaning any children of the bound device have already + //! been suspended. If suspension fails, the device must remain in, or be returned to, a non-suspended state by this + //! function. + //! + //! @param device The device to suspend. + //! @return Nothing on success, a error otherwise. + [[nodiscard]] virtual auto suspend(device & device) -> kstd::result; + + //! Resume a device bound by this driver. + //! + //! @param device The device to resume. + [[nodiscard]] virtual auto resume(device & device) -> kstd::result; + //! Retrieve this drivers implementation of a given capability facet. //! //! @return A pointer to this drivers implementation of the requested facet, nullptr if this driver does not diff --git a/kapi/kapi/devices/power.hpp b/kapi/kapi/devices/power.hpp new file mode 100644 index 00000000..44837a65 --- /dev/null +++ b/kapi/kapi/devices/power.hpp @@ -0,0 +1,38 @@ +#ifndef TEACHOS_KAPI_DEVICES_POWER_HPP +#define TEACHOS_KAPI_DEVICES_POWER_HPP + +// IWYU pragma: private, include + +#include + +#include + +namespace kapi::devices +{ + + //! @addtogroup kapi-devices-kernel-defined + //! @{ + + //! Suspend a device (sub-)tree rooted in a given bus. + //! + //! This function ensures a depth-first traversal and suspension of the given tree. In case suspension of a device + //! fails, a roll-back will be performed, resuming any previously suspended devices. Children will be suspended before + //! their parents are. + //! + //! @param root The root of the tree to suspend. + //! @return nothing on success, the first error to occur otherwise. + auto suspend_tree(bus & root) -> kstd::result; + + //! Resume a device (sub-)tree rooted in a given bus. + //! + //! This function resumes the tree in the reverse order of suspension performed by suspend_tree. On error, resumption + //! continues and error codes and messages are logged. + //! + //! @param root The root of the tree to resume. + auto resume_tree(bus & root) -> void; + + //! @} + +} // namespace kapi::devices + +#endif \ No newline at end of file diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt index dd72051d..d286b2aa 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources("kernel_lib" PRIVATE "kapi/devices/driver_registry.cpp" "kapi/devices/error.cpp" "kapi/devices/facet_registry.cpp" + "kapi/devices/power.cpp" "kapi/filesystem.cpp" "kapi/interrupts.cpp" "kapi/memory.cpp" diff --git a/kernel/kapi/devices/driver.cpp b/kernel/kapi/devices/driver.cpp index 4f0ce0e4..5ca92dc8 100644 --- a/kernel/kapi/devices/driver.cpp +++ b/kernel/kapi/devices/driver.cpp @@ -1,6 +1,8 @@ #include #include +#include + #include #include @@ -17,6 +19,16 @@ namespace kapi::devices return std::nullopt; } + auto driver::suspend(device &) -> kstd::result + { + return kstd::success(); + } + + auto driver::resume(device &) -> kstd::result + { + return kstd::success(); + } + auto driver::query_facet(kapi::capabilities::facet_id) -> void * { return nullptr; diff --git a/kernel/kapi/devices/power.cpp b/kernel/kapi/devices/power.cpp new file mode 100644 index 00000000..3bc66848 --- /dev/null +++ b/kernel/kapi/devices/power.cpp @@ -0,0 +1,93 @@ +#include + +#include + +#include +#include +#include +#include + +#include + +namespace kapi::devices +{ + + namespace + { + auto resume_subtree(device & node) -> void; + + // NOLINTNEXTLINE(misc-no-recursion) + auto suspend_subtree(device & node) -> kstd::result + { + auto suspended_children = kstd::vector>{}; + + if (auto as_bus = node.facet()) + { + for (auto const & child : as_bus->children()) + { + auto result = suspend_subtree(*child); + if (!result) + { + for (auto const & already_suspended : std::views::reverse(suspended_children)) + { + resume_subtree(*already_suspended); + } + + return result; + } + + suspended_children.push_back(child); + } + } + + if (auto driver = node.bound_driver()) + { + auto result = driver->suspend(node); + if (!result) + { + for (auto const & already_suspended : std::views::reverse(suspended_children)) + { + resume_subtree(*already_suspended); + } + + return result; + } + } + + return kstd::success(); + } + + // NOLINTNEXTLINE(misc-no-recursion) + auto resume_subtree(device & node) -> void + { + if (auto driver = node.bound_driver()) + { + auto result = driver->resume(node); + if (!result) + { + kstd::println(kstd::print_sink::stderr, "[OS:PM] failed to resume device '{}': {}", node.name(), + result.error().message()); + } + } + + if (auto as_bus = node.facet()) + { + for (auto const & child : as_bus->children()) + { + resume_subtree(*child); + } + } + } + } // namespace + + auto suspend_tree(bus & root) -> kstd::result + { + return suspend_subtree(root); + } + + auto resume_tree(bus & root) -> void + { + resume_subtree(root); + } + +} // namespace kapi::devices \ No newline at end of file diff --git a/kernel/kapi/devices/power.tests.cpp b/kernel/kapi/devices/power.tests.cpp new file mode 100644 index 00000000..8f522040 --- /dev/null +++ b/kernel/kapi/devices/power.tests.cpp @@ -0,0 +1,203 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace +{ + + struct test_device : kapi::devices::device + { + using kapi::devices::device::device; + }; + + struct recording_driver final : kapi::devices::driver + { + explicit recording_driver(kstd::vector & log, kstd::string fail_on_suspend = {}) + : m_log{&log} + , m_fail_on_suspend{fail_on_suspend} + {} + + [[nodiscard]] auto probe(kapi::devices::device &) -> kstd::result override + { + return kstd::success(); + } + + auto unbind(kapi::devices::device &) -> void override {} + + [[nodiscard]] auto suspend(kapi::devices::device & device) -> kstd::result override + { + if (!m_fail_on_suspend.empty() && device.name() == m_fail_on_suspend) + { + return kstd::failure(make_error_code(kstd::errc::io_error)); + } + + m_log->push_back(kstd::format("suspend:{}", device.name())); + return kstd::success(); + } + + [[nodiscard]] auto resume(kapi::devices::device & device) -> kstd::result override + { + m_log->push_back(kstd::format("resume:{}", device.name())); + return kstd::success(); + } + + [[nodiscard]] auto name() const noexcept -> std::string_view override + { + return "recording_driver"; + } + + private: + kstd::vector * m_log; + kstd::string m_fail_on_suspend; + }; + + auto index_of(kstd::vector const & log, std::string_view entry) -> std::size_t + { + auto found = std::ranges::find(log, entry); + REQUIRE(found != log.end()); + return static_cast(found - log.begin()); + } + +} // namespace + +SCENARIO("Suspension and resumption happens in the correct order", "[devices][power]") +{ + GIVEN("a bus with two bound lead devices, itself bound to its own driver, attached to a root") + { + auto log = kstd::vector{}; + + auto root = kstd::make_shared("power_test_root"); + auto middle = kstd::make_shared("power_test_middle"); + auto leaf_1 = kstd::make_shared("power_test_leaf_1"); + auto leaf_2 = kstd::make_shared("power_test_leaf_2"); + + auto middle_driver = kstd::make_shared(log); + auto leaf_1_driver = kstd::make_shared(log); + auto leaf_2_driver = kstd::make_shared(log); + + root->add_child(middle); + middle->add_child(leaf_1); + middle->add_child(leaf_2); + + middle->bind_driver(middle_driver); + middle->set_state(kapi::devices::state::bound); + + leaf_1->bind_driver(leaf_1_driver); + leaf_1->set_state(kapi::devices::state::bound); + + leaf_2->bind_driver(leaf_2_driver); + leaf_2->set_state(kapi::devices::state::bound); + + WHEN("the bus is suspended") + { + auto result = kapi::devices::suspend_tree(*root); + + THEN("it succeeds") + { + REQUIRE(result); + } + + THEN("the leaves were suspended before the middle bus") + { + REQUIRE(log.size() == 3); + + auto middle_index = index_of(log, "suspend:power_test_middle"); + auto leaf_1_index = index_of(log, "suspend:power_test_leaf_1"); + auto leaf_2_index = index_of(log, "suspend:power_test_leaf_2"); + + REQUIRE(leaf_1_index < middle_index); + REQUIRE(leaf_2_index < middle_index); + } + + AND_WHEN("the bus is resumed") + { + kapi::devices::resume_tree(*root); + + THEN("the leaves were resumed after the middle bus") + { + auto middle_index = index_of(log, "resume:power_test_middle"); + auto leaf_1_index = index_of(log, "resume:power_test_leaf_1"); + auto leaf_2_index = index_of(log, "resume:power_test_leaf_2"); + + REQUIRE(middle_index < leaf_1_index); + REQUIRE(middle_index < leaf_2_index); + } + } + } + } + + GIVEN("a subtree where one leaf will fail to suspend") + { + auto log = kstd::vector{}; + + auto root = kstd::make_shared("power_test_root_failing"); + auto middle = kstd::make_shared("power_test_middle_failing"); + auto leaf_1 = kstd::make_shared("power_test_leaf_1_failing"); + auto leaf_2 = kstd::make_shared("power_test_leaf_2_failing"); + + auto middle_driver = kstd::make_shared(log); + // leaf_1 suspends normally; leaf_2 is told to fail. + auto leaf_1_driver = kstd::make_shared(log); + auto leaf_2_driver = kstd::make_shared(log, kstd::string{"power_test_leaf_2_failing"}); + + root->add_child(middle); + middle->add_child(leaf_1); + middle->add_child(leaf_2); + + middle->bind_driver(middle_driver); + middle->set_state(kapi::devices::state::bound); + leaf_1->bind_driver(leaf_1_driver); + leaf_1->set_state(kapi::devices::state::bound); + leaf_2->bind_driver(leaf_2_driver); + leaf_2->set_state(kapi::devices::state::bound); + + WHEN("the subtree is suspended") + { + auto result = kapi::devices::suspend_tree(*root); + + THEN("the failure is reported") + { + REQUIRE_FALSE(result); + } + + THEN("the leaf that suspended successfully before the failure was resumed again") + { + REQUIRE(log.size() == 2); + REQUIRE(log[0] == "suspend:power_test_leaf_1_failing"); + REQUIRE(log[1] == "resume:power_test_leaf_1_failing"); + } + + THEN("the middle bus, whose child failed to suspend, was never suspended at all") + { + REQUIRE(std::ranges::find(log, "suspend:power_test_middle_failing") == log.end()); + } + } + } + + GIVEN("a bus with a child that has no bound driver at all") + { + auto root = kstd::make_shared("power_test_root_unbound"); + auto unbound_child = kstd::make_shared("power_test_unbound_child"); + + root->add_child(unbound_child); + + THEN("suspending and resuming it does not crash, and simply does nothing for that device") + { + REQUIRE(kapi::devices::suspend_tree(*root)); + REQUIRE_NOTHROW(kapi::devices::resume_tree(*root)); + } + } +} \ No newline at end of file -- cgit v1.2.3