单核调度会话示例
这个 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 和返回值都应被调用者检查。析构仅为忘记显式结束时提供兜底,无法向调用者汇报恢复失败。已失败的恢复不会在析构中反复重试;调用者应阻止下一项共享该线程的测试。异常路径也要先保存清理结果,不能仅因负载正常返回就接纳成绩。
文件
#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;
} #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