← Single-core scheduling on HarmonyOS: making the core selection reach the compute workload

This article has no English version yet; the original Chinese text follows. Chart labels are in Chinese as well.

单核调度会话示例

这个 C++17 示例演示一条实际计算线程上的状态捕获、性能请求、目标 CPU 请求、回读及恢复。将 thread_session.h 放入鸿蒙 Native 模块,在 CMake 中链接 libqos.so。原生 QoS C API 从 API 12 起提供;本示例按 API 26 头文件核对。

target_compile_features(your_native_target PRIVATE cxx_std_17)
target_compile_definitions(your_native_target PRIVATE _GNU_SOURCE)
target_link_libraries(your_native_target PRIVATE libqos.so)

example.cpp 接收调用者自己的 Workload 与明确的 AcceptScheduling 准入函数。它创建计算线程,在该线程中构造会话,先检查调度是否满足此次实验的要求,再执行负载、显式 Finish(),最后 join。示例提供 RequireExactAtBoundary;SOFT 生产路径需自行定义目标组、有效属性与运行期观测规则。NAPI 调用者应使用异步任务承载这个过程,避免占用 ArkUI 线程。工作负载的计时、输出验证、取消及其子线程生命周期由调用者管理。

single_core::Options options{};
options.useQos = true;
options.requestRealtime = false;
single_core::ThreadSession session(targetCpu, options);

const bool schedulingAccepted = acceptScheduling(session.Report());
if (schedulingAccepted) runYourActualWorkload();
(void)session.ObserveNow();
bool cleanupPassed = session.Finish();
auto report = session.Report();

targetCpu 是从当前设备拓扑得到的逻辑 CPU,0 是有效值。sched_setaffinity 的第一个参数 0 表示调用线程;它不表示 CPU0。对象必须始终由创建它的线程使用,且不可复制或移动。

为什么先捕获状态:QoS 和调度属性可能影响线程的调度环境,因此示例先读取 affinity、完整 sched_attr 和 QoS。snapshotComplete 表示 affinity 与包含 uclamp 的 v1 属性均可读;未知属性布局会被拒绝。只有普通策略且保存 flags 在本示例支持范围内的线程才接受性能写入;无法读取 QoS 原值时,默认跳过 QoS 写入,qosSkippedUnknown 会明确记录。公共 Get 返回 -1 既可能是未注册,也可能是内部错误,不能直接当作默认等级。

性能请求采用教学用的 GB 式回退:可选 FIFO1+uclamp,随后 OTHER/nice−20+uclamp,最后保留请求发生时的当前策略和参数,仅请求 uclamp。requestRealtime 默认关闭。设备可能拒绝其中任何一步,应检查 fifoSet、normalSet、clampSet 的结果以及实际回读。SPEC 的生产策略包含自己的 FIFO-only 回退,不能将这段示例当成所有基准共用的完整实现。

Report().exactAtReadback 要求 affinity 请求成功、回读为目标单 CPU 掩码,且 sched_getcpu() 当时也等于目标。它只描述该次观测。effectiveQos、effectiveNice、effectiveUtilMin/Max 分别解释各自属性。Call.error 仅保存失败的 POSIX 调用 errno;QoS 只按 SDK 规定记录返回值。

workloadCompleted 只表示负载完成,cleanupPassed 只表示恢复通过;它们不能替代 schedulingAccepted。示例保存入口、负载后只读回读、恢复后的三份状态。ObserveNow() 不重新提交请求,适合在计时结束后观察当前状态。

Refresh() 在同一计算线程的非计时边界重申请求,成功返回只表示该过程执行,不表示精确绑核。示例没有计时器、信号处理器或辅助维护线程。需要持续维护时,应在了解负载线程生命周期后接入专门维护器,并在 Finish() 前停止、join 或排空它;不要从信号处理器调用这个类。

显式 Finish() 按 QoS→调度属性→affinity 恢复并回读,restoreSucceeded 和返回值都应被调用者检查。析构仅为忘记显式结束时提供兜底,无法向调用者汇报恢复失败。已失败的恢复不会在析构中反复重试;调用者应阻止下一项共享该线程的测试。异常路径也要先保存清理结果,不能仅因负载正常返回就接纳成绩。

Files

example.cpp 55 lines Download ↓
#include "thread_session.h"
#include <thread>

using Workload = void (*)(void *context);
using AcceptScheduling = bool (*)(const single_core::SessionReport &) noexcept;
struct Measurement {
    single_core::SessionReport scheduling;
    single_core::SessionReport schedulingAtEntry;
    single_core::SessionReport schedulingAfterWork;
    bool schedulingAccepted = false;
    bool workloadCompleted = false;
    bool workloadThrew = false;
    bool cleanupPassed = false;
};

// Call and join this from a native background runner, never an ArkUI thread.
// The caller owns the actual workload, its timer, output checks and cancellation.
Measurement ExecuteOnComputeThread(int cpu, Workload workload, void *context,
                                   AcceptScheduling accept)
{
    Measurement result{};
    if (workload == nullptr || accept == nullptr) return result;
    std::thread compute([&] {
        single_core::Options options{};
        options.useQos = true;
        options.requestRealtime = false;
        single_core::ThreadSession session(cpu, options);
        result.schedulingAtEntry = session.Report();
        result.schedulingAccepted = accept(result.schedulingAtEntry);
        if (result.schedulingAccepted) {
            try {
                workload(context); // Put the real timer and compute loop here.
                result.workloadCompleted = true;
            } catch (...) {
                result.workloadThrew = true;
            }
        }
        (void)session.ObserveNow();
        result.schedulingAfterWork = session.Report();
        // Stop and join any workload-owned observers before this boundary.
        result.cleanupPassed = session.Finish();
        result.scheduling = session.Report();
    });
    compute.join();
    return result;
}

// One explicit policy for a strict boundary check. A SOFT production policy
// must supply its own target-group/attribute criteria and runtime observation.
bool RequireExactAtBoundary(const single_core::SessionReport &report) noexcept
{
    return report.snapshotComplete && report.validTarget &&
        !report.wrongThread && report.exactAtReadback;
}
thread_session.h 229 lines Download ↓
#pragma once
// C++17 / HarmonyOS native QoS (API 12+). Construct, use and finish on the
// actual native compute thread. This teaching scope does not create a timer,
// observer thread, signal handler or process-wide CPUFreq/cpuset request.
#include <qos/qos.h>
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <cerrno>
#include <cstdint>

namespace single_core {
struct Options {
    bool useQos = true;
    bool requestRealtime = false;
};
struct Call {
    bool attempted = false;
    int result = -1;
    int error = 0; // POSIX errno only. QoS documents its return, not errno.
};
struct SessionReport {
    int ownerTid = -1, requestedCpu = -1, actualCpu = -1, effectiveQos = -1;
    bool validTarget = false, affinityCaptured = false, attrsCaptured = false;
    bool qosCaptured = false, qosSkippedUnknown = false, snapshotComplete = false;
    bool performanceEligible = false, targetInOriginalMask = false;
    bool exactAtReadback = false, wrongThread = false, finished = false;
    bool restoreSucceeded = false;
    unsigned refreshCount = 0;
    // 0 = none, 1 = FIFO, 2 = OTHER/nice -20, 3 = current policy + clamp.
    int selectedRequest = 0;
    uint32_t effectivePolicy = 0, effectiveUtilMin = 0, effectiveUtilMax = 0;
    int32_t effectiveNice = 0;
    Call captureAffinity, captureAttr, captureQos;
    Call qosSet, fifoSet, normalSet, clampSet, affinitySet;
    Call affinityRead, attrRead, qosRead;
    Call qosRestore, attrRestore, affinityRestore;
    Call qosVerify, attrVerify, affinityVerify;
};

class ThreadSession {
    // Local Linux UAPI sched_attr v1 avoids the sched_param collision between
    // POSIX sched.h and linux/sched/types.h. v0 has no uclamp fields.
    struct SchedAttr {
        uint32_t size, policy;
        uint64_t flags;
        int32_t nice;
        uint32_t priority;
        uint64_t runtime, deadline, period;
        uint32_t utilMin, utilMax;
    };
    static_assert(sizeof(SchedAttr) == 56, "sched_attr v1 ABI size");
    static constexpr uint64_t kResetOnFork = 0x01;
    static constexpr uint64_t kKeepPolicyAndParams = 0x18;
    static constexpr uint64_t kClamp = 0x60;
    Options options_;
    SessionReport report_;
    cpu_set_t originalMask_{};
    SchedAttr originalAttr_{};
    QoS_Level originalQos_ = QOS_DEFAULT;
    bool qosTouched_ = false, attrTouched_ = false, affinityTouched_ = false;

    static int Tid() noexcept { return static_cast<int>(::syscall(SYS_gettid)); }
    static int Posix(Call &call, int result) noexcept {
        call = {true, result, result == 0 ? 0 : errno};
        return result;
    }
    static int Qos(Call &call, int result) noexcept {
        call = {true, result, 0};
        return result;
    }
    static int GetAttr(SchedAttr &attr, Call &call) noexcept {
#if defined(SYS_sched_getattr)
        attr = {}; attr.size = sizeof(attr);
        const int rc = static_cast<int>(::syscall(SYS_sched_getattr, 0, &attr, sizeof(attr), 0));
        if (rc == 0 && attr.size != sizeof(attr)) {
            call = {true, -1, ENOTSUP}; // Only the captured v1 layout is supported.
            return -1;
        }
        return Posix(call, rc);
#else
        (void)attr; call = {true, -1, ENOSYS}; return -1;
#endif
    }
    static int SetAttr(SchedAttr &attr, Call &call) noexcept {
#if defined(SYS_sched_setattr)
        return Posix(call, static_cast<int>(::syscall(SYS_sched_setattr, 0, &attr, 0)));
#else
        (void)attr; call = {true, -1, ENOSYS}; return -1;
#endif
    }
    bool OnOwner() noexcept {
        if (Tid() == report_.ownerTid) return true;
        report_.wrongThread = true; return false;
    }
    static bool SameMask(const cpu_set_t &a, const cpu_set_t &b) noexcept {
        for (int cpu = 0; cpu < CPU_SETSIZE; ++cpu)
            if (CPU_ISSET(cpu, &a) != CPU_ISSET(cpu, &b)) return false;
        return true;
    }
    static bool SameAttr(const SchedAttr &a, const SchedAttr &b) noexcept {
        return a.policy == b.policy && a.nice == b.nice && a.priority == b.priority &&
            a.runtime == b.runtime && a.deadline == b.deadline && a.period == b.period &&
            a.utilMin == b.utilMin && a.utilMax == b.utilMax &&
            (a.flags & kResetOnFork) == (b.flags & kResetOnFork);
    }
    void Capture() noexcept {
        report_.affinityCaptured = Posix(report_.captureAffinity,
            ::sched_getaffinity(0, sizeof(originalMask_), &originalMask_)) == 0;
        report_.attrsCaptured = GetAttr(originalAttr_, report_.captureAttr) == 0;
        // The minimal example only modifies ordinary-policy threads. Capturing
        // a pre-existing realtime/deadline policy is not proof we can restore it.
        report_.performanceEligible = report_.attrsCaptured &&
            (originalAttr_.flags & ~kResetOnFork) == 0 &&
            (originalAttr_.policy == SCHED_OTHER || originalAttr_.policy == SCHED_BATCH ||
             originalAttr_.policy == SCHED_IDLE);
        report_.qosCaptured = Qos(report_.captureQos, OH_QoS_GetThreadQoS(&originalQos_)) == 0 &&
            originalQos_ >= QOS_BACKGROUND && originalQos_ <= QOS_USER_INTERACTIVE;
        report_.qosSkippedUnknown = options_.useQos && !report_.qosCaptured;
        report_.snapshotComplete = report_.affinityCaptured && report_.attrsCaptured;
        report_.targetInOriginalMask = report_.validTarget && report_.affinityCaptured &&
            CPU_ISSET(report_.requestedCpu, &originalMask_);
    }
    void ApplyPerformance() noexcept {
        // QoS can also affect scheduling state; capture both attr and mask first.
        if (!report_.snapshotComplete || !report_.performanceEligible) return;
        if (options_.useQos && report_.qosCaptured) {
            qosTouched_ = true;
            Qos(report_.qosSet, OH_QoS_SetThreadQoS(QOS_USER_INTERACTIVE));
        }
        SchedAttr request{}; request.size = sizeof(request);
        request.flags = (originalAttr_.flags & kResetOnFork) | kClamp;
        request.utilMin = request.utilMax = 1024;
        attrTouched_ = true;
        report_.selectedRequest = 0;
        if (options_.requestRealtime) {
            request.policy = SCHED_FIFO; request.priority = 1;
            if (SetAttr(request, report_.fifoSet) == 0) report_.selectedRequest = 1;
        }
        if (report_.selectedRequest == 0) {
            request.policy = SCHED_OTHER; request.priority = 0; request.nice = -20;
            if (SetAttr(request, report_.normalSet) == 0) report_.selectedRequest = 2;
        }
        if (report_.selectedRequest == 0) {
            request = originalAttr_; request.size = sizeof(request);
            request.flags = (originalAttr_.flags & kResetOnFork) | kClamp | kKeepPolicyAndParams;
            request.utilMin = request.utilMax = 1024;
            if (SetAttr(request, report_.clampSet) == 0) report_.selectedRequest = 3;
        }
    }
    void Observe() noexcept {
        cpu_set_t effective{};
        const bool maskRead = Posix(report_.affinityRead,
            ::sched_getaffinity(0, sizeof(effective), &effective)) == 0;
        report_.actualCpu = ::sched_getcpu();
        report_.exactAtReadback = report_.affinitySet.result == 0 && maskRead &&
            CPU_COUNT(&effective) == 1 && CPU_ISSET(report_.requestedCpu, &effective) &&
            report_.actualCpu == report_.requestedCpu;
        SchedAttr actual{};
        if (GetAttr(actual, report_.attrRead) == 0) {
            report_.effectivePolicy = actual.policy; report_.effectiveNice = actual.nice;
            report_.effectiveUtilMin = actual.utilMin; report_.effectiveUtilMax = actual.utilMax;
        }
        QoS_Level qos = QOS_DEFAULT;
        report_.effectiveQos = Qos(report_.qosRead, OH_QoS_GetThreadQoS(&qos)) == 0 ? qos : -1;
    }
public:
    explicit ThreadSession(int cpu, Options options = {}) noexcept : options_(options) {
        report_.ownerTid = Tid(); report_.requestedCpu = cpu;
        report_.validTarget = cpu >= 0 && cpu < CPU_SETSIZE;
        Capture();
        if (report_.validTarget) Refresh();
    }
    ThreadSession(const ThreadSession &) = delete;
    ThreadSession &operator=(const ThreadSession &) = delete;
    ThreadSession(ThreadSession &&) = delete;
    ThreadSession &operator=(ThreadSession &&) = delete;
    ~ThreadSession() noexcept { if (!report_.finished) (void)Finish(); }
    const SessionReport &Report() const noexcept { return report_; }
    // Read only: inspect the owner again outside the timed workload.
    bool ObserveNow() noexcept {
        if (report_.finished || !OnOwner() || !report_.validTarget) return false;
        Observe(); return true;
    }
    // Use at a chosen non-timed boundary on the owner thread. A true return
    // means this operation ran; inspect exactAtReadback and each request result.
    bool Refresh() noexcept {
        if (report_.finished || !OnOwner() || !report_.validTarget || !report_.affinityCaptured) return false;
        ++report_.refreshCount; ApplyPerformance();
        cpu_set_t target{}; CPU_ZERO(&target); CPU_SET(report_.requestedCpu, &target);
        affinityTouched_ = true;
        Posix(report_.affinitySet, ::sched_setaffinity(0, sizeof(target), &target));
        Observe(); return true;
    }
    bool Finish() noexcept {
        if (report_.finished) return report_.restoreSucceeded;
        if (!OnOwner()) return false;
        bool okay = true;
        // Stop/join any external maintenance before calling Finish().
        if (qosTouched_) {
            const bool set = Qos(report_.qosRestore, OH_QoS_SetThreadQoS(originalQos_)) == 0;
            QoS_Level qos = QOS_DEFAULT;
            const bool get = Qos(report_.qosVerify, OH_QoS_GetThreadQoS(&qos)) == 0;
            okay = set && get && qos == originalQos_ && okay;
        }
        if (attrTouched_ || qosTouched_) {
            SchedAttr saved = originalAttr_; saved.size = sizeof(saved);
            // KEEP_* are commands, not saved policy. Explicitly request clamp
            // restoration; getters need not return UTIL_CLAMP_* command bits.
            saved.flags = (originalAttr_.flags & kResetOnFork) | kClamp;
            const bool set = SetAttr(saved, report_.attrRestore) == 0;
            SchedAttr actual{};
            const bool get = GetAttr(actual, report_.attrVerify) == 0;
            okay = set && get && SameAttr(actual, originalAttr_) && okay;
        }
        if (affinityTouched_ || attrTouched_ || qosTouched_) {
            const bool set = Posix(report_.affinityRestore,
                ::sched_setaffinity(0, sizeof(originalMask_), &originalMask_)) == 0;
            cpu_set_t actual{};
            const bool get = Posix(report_.affinityVerify,
                ::sched_getaffinity(0, sizeof(actual), &actual)) == 0;
            okay = set && get && SameMask(actual, originalMask_) && okay;
        }
        report_.finished = true; report_.restoreSucceeded = okay;
        return okay;
    }
};
} // namespace single_core