Implement On-Core session management, job submission and cancellation, isolated address space mapping helpers, and the round-robin time-sliced scheduling loop for preserved physical CPUs in kernel/liveupdate/oncore.c, and include the On-Core kernel-doc documentation in Documentation/liveupdate/cpu_preservation.rst.
Signed-off-by: Pasha Tatashin <[email protected]> --- Documentation/liveupdate/cpu_preservation.rst | 14 + kernel/liveupdate/Makefile | 12 + kernel/liveupdate/cpu_preserve.c | 15 + kernel/liveupdate/oncore.c | 947 ++++++++++++++++++ 4 files changed, 988 insertions(+) create mode 100644 kernel/liveupdate/oncore.c diff --git a/Documentation/liveupdate/cpu_preservation.rst b/Documentation/liveupdate/cpu_preservation.rst index a7a534188685..605d808e61c8 100644 --- a/Documentation/liveupdate/cpu_preservation.rst +++ b/Documentation/liveupdate/cpu_preservation.rst @@ -18,6 +18,20 @@ Architecture Backend Interface .. kernel-doc:: include/linux/cpu_preserve.h +On-Core Execution and Scheduling Framework +========================================== + +.. kernel-doc:: kernel/liveupdate/oncore.c + :doc: On-Core Execution and Scheduling Framework + +On-Core Session & Job API +========================= + +.. kernel-doc:: include/linux/oncore.h + +.. kernel-doc:: kernel/liveupdate/oncore.c + :identifiers: + CPU Preservation ABI ==================== diff --git a/kernel/liveupdate/Makefile b/kernel/liveupdate/Makefile index 486f7854bb75..ab0d44079fb2 100644 --- a/kernel/liveupdate/Makefile +++ b/kernel/liveupdate/Makefile @@ -1,6 +1,17 @@ # SPDX-License-Identifier: GPL-2.0 +KASAN_SANITIZE_cpu_preserve.o := n +KCSAN_SANITIZE_cpu_preserve.o := n +UBSAN_SANITIZE_cpu_preserve.o := n +KCOV_INSTRUMENT_cpu_preserve.o := n +KASAN_SANITIZE_oncore.o := n +KCSAN_SANITIZE_oncore.o := n +UBSAN_SANITIZE_oncore.o := n +KCOV_INSTRUMENT_oncore.o := n +CFLAGS_REMOVE_cpu_preserve.o = $(CC_FLAGS_FTRACE) +CFLAGS_REMOVE_oncore.o = $(CC_FLAGS_FTRACE) CFLAGS_cpu_preserve.o += $(call cc-option,-mbranch-protection=none) -fno-stack-protector $(call cc-option,-ftrivial-auto-var-init=uninitialized) $(call cc-option,-fno-jump-tables) +CFLAGS_oncore.o += $(call cc-option,-mbranch-protection=none) -fno-stack-protector $(call cc-option,-ftrivial-auto-var-init=uninitialized) $(call cc-option,-fno-jump-tables) luo-y := \ kho_block.o \ @@ -14,3 +25,4 @@ obj-$(CONFIG_KEXEC_HANDOVER_DEBUGFS) += kexec_handover_debugfs.o obj-$(CONFIG_LIVEUPDATE) += luo.o obj-$(CONFIG_LIVEUPDATE_CPU) += cpu_preserve.o +obj-$(CONFIG_LIVEUPDATE_ONCORE) += oncore.o diff --git a/kernel/liveupdate/cpu_preserve.c b/kernel/liveupdate/cpu_preserve.c index 9a039ba9e912..430afbca860d 100644 --- a/kernel/liveupdate/cpu_preserve.c +++ b/kernel/liveupdate/cpu_preserve.c @@ -178,6 +178,7 @@ #include <linux/liveupdate.h> #include <linux/mm.h> #include <linux/objtool.h> +#include <linux/oncore.h> #include <linux/reboot.h> #include <asm/sections.h> @@ -1552,14 +1553,22 @@ static int cpu_preserve_preserve(struct liveupdate_file_op_args *args) if (ret) return ret; + ret = oncore_session_add_cpu(args->session, cpu); + if (ret) { + cpu_unpreserve(cpu); + return ret; + } + fser = kho_alloc_preserve(sizeof(*fser)); if (IS_ERR(fser)) { cpu_unpreserve(cpu); + oncore_session_remove_cpu(args->session, cpu); return PTR_ERR(fser); } memset(fser, 0, sizeof(*fser)); fser->cpu = cpu; + KHOSER_STORE_PTR(fser->oncore, oncore_session_get_ser(args->session)); scoped_guard(mutex, &cpu_preserved_lock) fser->stack_pa = cpu_preserved_outgoing.pcpus[cpu].stack_pa; @@ -1580,6 +1589,7 @@ static void cpu_preserve_unpreserve(struct liveupdate_file_op_args *args) cpu = fser->cpu; cpu_unpreserve(cpu); + oncore_session_remove_cpu(args->session, cpu); kho_unpreserve_free(fser); } @@ -1587,8 +1597,12 @@ static void cpu_preserve_unpreserve(struct liveupdate_file_op_args *args) static void cpu_preserve_restore_incoming_cpu(struct liveupdate_session *session, struct cpu_preserved_file_ser *fser) { + struct oncore_session_ser *oncore = KHOSER_LOAD_PTR(fser->oncore); unsigned int cpu = fser->cpu; + if (oncore) + oncore_session_restore(session, oncore); + scoped_guard(mutex, &cpu_preserved_lock) { cpumask_set_cpu(cpu, &cpu_preserved_incoming.mask); cpumask_set_cpu(cpu, &cpu_preserved_mask); @@ -1646,6 +1660,7 @@ static void cpu_preserve_finish(struct liveupdate_file_op_args *args) cpu_preserve_restore_incoming_cpu(args->session, fser); cpu_unpreserve(fser->cpu); + oncore_session_remove_cpu(args->session, fser->cpu); kho_restore_free(fser); } diff --git a/kernel/liveupdate/oncore.c b/kernel/liveupdate/oncore.c new file mode 100644 index 000000000000..196ed0a67563 --- /dev/null +++ b/kernel/liveupdate/oncore.c @@ -0,0 +1,947 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2026, Google LLC. + * Pasha Tatashin <[email protected]> + * + * On-Core Session and Scheduler Framework for Live Update + */ + +/** + * DOC: On-Core Execution and Scheduling Framework + * + * The On-Core framework enables latency-sensitive workloads to continue + * executing directly on preserved physical CPUs (__cpu_preserved_text / + * __cpu_preserved_data) across a kexec Live Update (CONFIG_LIVEUPDATE_CPU) + * while the outgoing host kernel shuts down and the incoming kernel boots. + * + * Each Live Update session (struct liveupdate_session) can create an + * oncore_session that manages: + * + * 1. Isolated Address Space (struct cpu_preserved_as): + * Page tables containing only the preserved runtime text/data sections, + * the session and runqueue metadata, and workload-specific buffers + * explicitly mapped via oncore_session_map_range() or + * oncore_session_map_buffer(). + * + * 2. Preserved Physical CPU Pool: + * One or more physical CPUs isolated from Linux scheduling via + * cpu_preserve and attached to the session via oncore_session_add_cpu(). + * Each preserved CPU switches to the session's isolated page tables and + * executes oncore_cpu_schedule_loop(). + * + * 3. Cooperative Time-Sliced Runqueue (struct oncore_runqueue): + * A lockless/atomic round-robin FIFO scheduler supporting M jobs across N + * preserved physical CPUs (including M > N oversubscription). It provides + * initial-run starvation avoidance, least-loaded CPU assignment, work + * stealing, single-job fast-path continuation, and low-power backoff (WFE on + * arm64, PAUSE on x86) when the queue is idle or a job returns + * ONCORE_EXIT_STALL. + * + * Integration with KVM Caretaker and Future Kernel Caretaker Workloads + * -------------------------------------------------------------------- + * On-Core is workload- and hypervisor-agnostic: + * + * - KVM Caretaker: During LUO prepare/freeze, KVM detaches each vCPU into a + * self-contained KHO-preserved Caretaker page (caretaker_x86_page on x86, + * caretaker_arm64_page on arm64), maps the page and guest/virtualization + * structures into the session's address space, and submits an oncore_job + * whose callback enters the guest (VMLAUNCH/VMRESUME, VMRUN, or EL2 world + * switch) until the time-slice deadline (deadline_ticks) expires, the vCPU + * yields on HLT/WFI (ONCORE_EXIT_YIELD_IDLE), hits an exit requiring the + * incoming kernel (ONCORE_EXIT_STALL), or observes the incoming kernel's + * reclaim signal (ONCORE_EXIT_ATTACH_SIGNALED). + * + * - Future Kernel Caretaker Workloads: Any self-contained kernel subsystem + * compiled into the preserved runtime sections (such as hardware watchdog + * feeders, health/heartbeat responders, or zero-loss network/storage + * polling drivers) can submit an oncore_job_fn(void *data, u64 deadline) to + * share preserved physical cores alongside or independently of KVM vCPUs. + * + * Architecture Requirements + * ------------------------- + * To support CONFIG_LIVEUPDATE_ONCORE, an architecture must implement: + * + * - <asm/oncore.h>: + * - arch_oncore_read_counter(): Read a monotonic hardware counter directly + * from __cpu_preserved_text without relying on kernel timekeeping + * (e.g., rdtsc() on x86, __arch_counter_get_cntpct() on arm64). + * - arch_oncore_counter_freq_hz(): Return the hardware counter frequency in + * Hz used to convert the time quantum (oncore.quantum_ms) into ticks. + * + * - <asm/cpu_preserve.h> (from CONFIG_LIVEUPDATE_CPU): + * - arch_cpu_preserved_switch_pgd(): Switch to the session's isolated PGD. + * - arch_cpu_preserved_park_wait() / arch_cpu_preserved_kick(): Low-power + * wait instruction and cross-CPU wakeup mechanism. + * + * Public Interfaces (<linux/oncore.h>) + * ------------------------------------ + * - Session & CPU Lifecycle: + * oncore_session_add_cpu(), oncore_session_remove_cpu(), + * oncore_session_get_ser(), oncore_session_restore(). + * - Session Address Space Mapping: + * oncore_session_map_range(), oncore_session_map_buffer(), + * oncore_session_get_pgd_pa(). + * - Job Submission & Control: + * oncore_session_submit_job(), oncore_job_set_data(), + * oncore_job_cpu(), oncore_job_session(), + * oncore_session_activate_job(), oncore_session_cancel_job(). + */ + +#define pr_fmt(fmt) "oncore: " fmt + +#include <linux/cpu_preserve.h> +#include <linux/cpumask.h> +#include <linux/delay.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/kexec.h> +#include <linux/kexec_handover.h> +#include <linux/list.h> +#include <linux/liveupdate.h> +#include <linux/mm.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/oncore.h> +#include <linux/overflow.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/types.h> + +#define ONCORE_DEFAULT_QUANTUM_MS 10 +#define ONCORE_CANCEL_TIMEOUT_US 1000000 +#define ONCORE_CANCEL_STEP_US 100 + +enum oncore_job_state { + ONCORE_JOB_NEW = 0, + ONCORE_JOB_RUNNABLE, + ONCORE_JOB_RUNNING, + ONCORE_JOB_CANCELING, + ONCORE_JOB_DEAD, +}; + +struct oncore_job { + struct list_head node; + struct list_head sess_node; + phys_addr_t next_job_pa; + struct oncore_session *session; + enum oncore_job_state state; + oncore_job_fn run_fn; + void *data; + int assigned_cpu; + int last_cpu; + u64 total_runs; +}; + +struct oncore_sched_config { + u32 quantum_ms; + u64 quantum_ticks; + u64 counter_freq_hz; +}; + +struct oncore_runqueue { + atomic_t lock; + struct list_head runnable; + unsigned int nr_runnable; +}; + +struct oncore_session { + struct list_head node; + /* Protects session state and job list */ + struct mutex lock; + struct list_head jobs; + phys_addr_t first_job_pa; + struct oncore_runqueue rq; + struct oncore_sched_config sched_config; + struct cpu_preserved_as *as; + struct oncore_session_ser *ser; + bool is_incoming; +}; + +static struct oncore_sched_config global_oncore_sched_config __cpu_preserved_data; +static DEFINE_MUTEX(oncore_sessions_lock); +static LIST_HEAD(oncore_sessions); + +static inline struct cpumask *oncore_session_cpus(struct oncore_session *sess) +{ + return to_cpumask((unsigned long *)sess->ser->cpus_bitmap); +} + +static inline void oncore_rq_lock(struct oncore_runqueue *rq) +{ + while (atomic_cmpxchg_acquire(&rq->lock, 0, 1) != 0) { + while (atomic_read(&rq->lock) != 0) + cpu_relax(); + } +} + +static inline void oncore_rq_unlock(struct oncore_runqueue *rq) +{ + atomic_set_release(&rq->lock, 0); +} + +static int __init parse_oncore_quantum(char *arg) +{ + u32 val; + + if (kstrtou32(arg, 0, &val) == 0 && val >= 1 && val <= 1000) + global_oncore_sched_config.quantum_ms = val; + return 0; +} +early_param("oncore.quantum_ms", parse_oncore_quantum); + +static void oncore_sched_update_ticks(void) +{ + u32 ms = global_oncore_sched_config.quantum_ms ? : ONCORE_DEFAULT_QUANTUM_MS; + u64 freq = arch_oncore_counter_freq_hz(); + + global_oncore_sched_config.counter_freq_hz = freq; + global_oncore_sched_config.quantum_ticks = (freq * ms) / 1000ULL; +} + +static void oncore_runqueue_init(struct oncore_runqueue *rq) +{ + atomic_set(&rq->lock, 0); + INIT_LIST_HEAD(&rq->runnable); + rq->nr_runnable = 0; +} + +static int oncore_sched_enqueue(struct oncore_runqueue *rq, + struct oncore_job *job) +{ + int cpu; + + oncore_rq_lock(rq); + job->state = ONCORE_JOB_RUNNABLE; + list_add_tail(&job->node, &rq->runnable); + rq->nr_runnable++; + oncore_rq_unlock(rq); + + for_each_cpu(cpu, oncore_session_cpus(job->session)) { + if (cpu_is_preserved(cpu)) + arch_cpu_preserved_kick(cpu); + } + + return 0; +} + +static int oncore_sched_dequeue(struct oncore_runqueue *rq, + struct oncore_job *job) +{ + oncore_rq_lock(rq); + if (!list_empty(&job->node)) { + list_del_init(&job->node); + rq->nr_runnable--; + } + if (job->state == ONCORE_JOB_RUNNING) + job->state = ONCORE_JOB_CANCELING; + else + job->state = ONCORE_JOB_DEAD; + oncore_rq_unlock(rq); + return 0; +} + +static struct oncore_job *__cpu_preserved_text +oncore_sched_pick_next(struct oncore_runqueue *rq, int cpu) +{ + struct oncore_job *job = NULL, *iter; + + if (!rq || READ_ONCE(rq->nr_runnable) == 0) + return NULL; + + oncore_rq_lock(rq); + + /* 1. Starvation avoidance: if head job has NEVER run, take it immediately */ + if (!list_empty(&rq->runnable)) { + iter = list_first_entry(&rq->runnable, struct oncore_job, node); + if (iter->total_runs == 0) { + job = iter; + goto found; + } + } + + /* 2. Prefer job affine to this core */ + list_for_each_entry(iter, &rq->runnable, node) { + if (iter->assigned_cpu == cpu) { + job = iter; + goto found; + } + } + + /* 3. Work-stealing fallback: take oldest job from head of queue */ + if (!list_empty(&rq->runnable)) + job = list_first_entry(&rq->runnable, struct oncore_job, node); + +found: + if (job) { + list_del_init(&job->node); + rq->nr_runnable--; + } + + oncore_rq_unlock(rq); + return job; +} + +static void __cpu_preserved_text +oncore_sched_put_prev(struct oncore_runqueue *rq, + struct oncore_job *job) +{ + oncore_rq_lock(rq); + job->state = ONCORE_JOB_RUNNABLE; + list_add_tail(&job->node, &rq->runnable); + rq->nr_runnable++; + oncore_rq_unlock(rq); +} + +static void __cpu_preserved_text +oncore_cpu_schedule_loop(int cpu, struct oncore_runqueue *rq, + struct oncore_sched_config *cfg) +{ + enum oncore_exit_reason reason; + struct oncore_job *curr = NULL; + u64 deadline; + + if (!rq || !cfg) + return; + + while (!cpu_preserved_should_exit(cpu)) { + /* 1. Pick the next runnable job from the FIFO queue */ + if (!curr) { + curr = oncore_sched_pick_next(rq, cpu); + if (!curr) { + /* No runnable jobs; execute low-power park wait */ + arch_cpu_preserved_park_wait(); + continue; + } + } + + /* 2. Compute quantum deadline */ + deadline = arch_oncore_read_counter() + cfg->quantum_ticks; + + /* 3. Execute workload on physical silicon */ + curr->last_cpu = cpu; + curr->state = ONCORE_JOB_RUNNING; + reason = curr->run_fn(curr->data, deadline); + + /* 4. Update telemetry and accounting */ + curr->total_runs++; + + /* + * A stalled job cannot make progress until the incoming kernel + * reclaims it, so re-running it immediately would just repeat + * the same unhandled exit as fast as the hardware allows. Back + * off in the architecture's low-power wait (WFE on arm64, PAUSE + * on x86) first. arch_cpu_preserved_kick() wakes it, so the + * attach signal is still observed promptly by the checks below + * and at the top of the loop. + */ + if (reason == ONCORE_EXIT_STALL) + arch_cpu_preserved_park_wait(); + + /* Fast-path: single runnable job continues uninterrupted */ + if (READ_ONCE(rq->nr_runnable) == 0 && + READ_ONCE(curr->state) == ONCORE_JOB_RUNNING && + reason != ONCORE_EXIT_ATTACH_SIGNALED && + reason != ONCORE_EXIT_ERROR && + !cpu_preserved_should_exit(cpu)) { + continue; + } + + /* 5. Handle exit and return job to queue */ + oncore_rq_lock(rq); + if (curr->state == ONCORE_JOB_CANCELING || + curr->state == ONCORE_JOB_DEAD || + reason == ONCORE_EXIT_ATTACH_SIGNALED || + reason == ONCORE_EXIT_ERROR) { + WRITE_ONCE(curr->state, ONCORE_JOB_DEAD); + oncore_rq_unlock(rq); + curr = NULL; + continue; + } + oncore_rq_unlock(rq); + + oncore_sched_put_prev(rq, curr); + curr = NULL; + } +} + +static void __cpu_preserved_text oncore_sched_cpu_worker(void *data) +{ + struct cpu_preserved_stack_context *sctx = cpu_preserved_get_stack_context(); + struct oncore_session *sess; + int cpu; + + if (!sctx) + return; + + sess = sctx->workload_context ? + (struct oncore_session *)(uintptr_t)sctx->workload_context : + data; + if (!sess) + return; + cpu = sctx->cpu; + + if (sctx->session_pgd_pa) + arch_cpu_preserved_switch_pgd(sctx->session_pgd_pa); + + oncore_cpu_schedule_loop(cpu, &sess->rq, &sess->sched_config); +} + +static int __init oncore_sched_init(void) +{ + if (!arch_oncore_counter_freq_hz()) { + pr_err("Counter frequency is unknown; on-core scheduler disabled\n"); + return -ENODEV; + } + if (!global_oncore_sched_config.quantum_ms) + global_oncore_sched_config.quantum_ms = ONCORE_DEFAULT_QUANTUM_MS; + oncore_sched_update_ticks(); + pr_info("Round-Robin scheduler initialized (quantum=%u ms, ticks=%llu)\n", + global_oncore_sched_config.quantum_ms, + global_oncore_sched_config.quantum_ticks); + return 0; +} +early_initcall(oncore_sched_init); + +/** + * oncore_session_map_range - Map a physical memory range into the session's address space + * @sess: On-Core session (may be %NULL to map into the global preserved address space). + * @pa: Start physical address of the range. + * @va: Target virtual address in the isolated page tables. + * @size: Size of the mapping in bytes. + * @prot: Page protection flags. + * + * Maps `[pa, pa + size)` at `@va` in the isolated page tables used by + * preserved CPUs executing workloads in @sess across kexec. + * + * Return: 0 on success, or a negative errno on failure. + */ +int oncore_session_map_range(struct oncore_session *sess, phys_addr_t pa, + unsigned long va, size_t size, pgprot_t prot) +{ + return cpu_preserved_map_range(pa, va, size, prot); +} +EXPORT_SYMBOL_GPL(oncore_session_map_range); + +/** + * oncore_session_map_buffer - Map a direct-map kernel buffer into the session's address space + * @sess: On-Core session. + * @va: Kernel direct-map virtual address of the buffer (no-op if %NULL). + * @size: Size of the buffer in bytes (no-op if 0). + * + * Convenience helper that resolves `virt_to_phys(@va)` and maps the buffer at + * its existing kernel virtual address `@va` with %PAGE_KERNEL permissions in + * the session's isolated page tables. + * + * Return: 0 on success, or a negative errno on failure. + */ +int oncore_session_map_buffer(struct oncore_session *sess, void *va, + size_t size) +{ + if (!va || !size) + return 0; + return oncore_session_map_range(sess, virt_to_phys(va), + (unsigned long)va, size, PAGE_KERNEL); +} +EXPORT_SYMBOL_GPL(oncore_session_map_buffer); + +/** + * oncore_session_get_pgd_pa - Return the root page table physical address of a session + * @sess: On-Core session. + * + * Return: Physical address of the session's isolated PGD, or 0 if @sess or its + * address space is %NULL. + */ +phys_addr_t oncore_session_get_pgd_pa(struct oncore_session *sess) +{ + return (sess && sess->as) ? sess->as->pgd_pa : 0; +} +EXPORT_SYMBOL_GPL(oncore_session_get_pgd_pa); + +static struct oncore_session *oncore_find_session_locked(const char *sname) +{ + struct oncore_session *sess; + + if (!sname || !sname[0]) + return NULL; + + list_for_each_entry(sess, &oncore_sessions, node) { + if (strcmp(sess->ser->session_name, sname) == 0) + return sess; + } + + return NULL; +} + +static struct oncore_session *oncore_find_session(struct liveupdate_session *s) +{ + guard(mutex)(&oncore_sessions_lock); + + return oncore_find_session_locked(liveupdate_session_name(s)); +} + +static struct oncore_session *oncore_create_session_locked(const char *sname) +{ + unsigned int nr_ser_words = BITS_TO_U64(nr_cpu_ids); + struct oncore_session *sess; + size_t ser_sz; + + ser_sz = struct_size(sess->ser, cpus_bitmap, nr_ser_words); + + sess = kho_alloc_preserve(sizeof(*sess)); + if (IS_ERR(sess)) + return NULL; + + memset(sess, 0, sizeof(*sess)); + mutex_init(&sess->lock); + INIT_LIST_HEAD(&sess->jobs); + oncore_runqueue_init(&sess->rq); + oncore_sched_update_ticks(); + sess->sched_config = global_oncore_sched_config; + + sess->as = cpu_preserved_as_create(); + if (IS_ERR(sess->as)) + sess->as = NULL; + + oncore_session_map_range(sess, virt_to_phys(sess), + (unsigned long)sess, sizeof(*sess), + PAGE_KERNEL); + + sess->ser = kho_alloc_preserve(ser_sz); + if (IS_ERR(sess->ser)) { + cpu_preserved_as_destroy(sess->as); + kho_unpreserve_free(sess); + return NULL; + } + + memset(sess->ser, 0, ser_sz); + sess->ser->nr_cpu_words = nr_ser_words; + strscpy(sess->ser->session_name, sname, sizeof(sess->ser->session_name)); + sess->ser->sess_pa = virt_to_phys(sess); + KHOSER_STORE_PTR(sess->ser->as, sess->as ? sess->as->ser : NULL); + + list_add_tail(&sess->node, &oncore_sessions); + return sess; +} + +static struct oncore_session *oncore_get_or_create_session(struct liveupdate_session *s) +{ + const char *sname = liveupdate_session_name(s); + struct oncore_session *sess; + + if (!sname || !sname[0]) + return NULL; + + guard(mutex)(&oncore_sessions_lock); + + sess = oncore_find_session_locked(sname); + if (sess) + return sess; + + return oncore_create_session_locked(sname); +} + +/** + * oncore_session_add_cpu - Attach a preserved physical CPU to an On-Core session + * @s: Live Update session handle. + * @cpu: Logical ID of the preserved physical CPU to add. + * + * Creates the On-Core session for @s if it does not yet exist, adds @cpu to + * the session's CPU mask, configures @cpu's preserved stack context with the + * session pointer and isolated PGD physical address, and attaches + * oncore_sched_cpu_worker() so @cpu begins servicing the session's runqueue. + * + * Return: 0 on success, or a negative errno on failure. + */ +int oncore_session_add_cpu(struct liveupdate_session *s, int cpu) +{ + struct oncore_session *sess; + int ret = 0; + + if (!arch_oncore_counter_freq_hz()) { + pr_err("On-core counter frequency is unknown; refusing to add CPU\n"); + return -ENODEV; + } + + sess = oncore_get_or_create_session(s); + if (!sess || cpu < 0 || cpu >= nr_cpu_ids) + return -EINVAL; + + guard(mutex)(&sess->lock); + cpumask_set_cpu(cpu, oncore_session_cpus(sess)); + + cpu_preserved_set_workload_context(cpu, sess, + oncore_session_get_pgd_pa(sess)); + + ret = cpu_preserved_attach_workload(cpu, + oncore_sched_cpu_worker, + sess); + if (ret) { + cpu_preserved_set_workload_context(cpu, NULL, 0); + cpumask_clear_cpu(cpu, oncore_session_cpus(sess)); + return ret; + } + + return 0; +} + +static void oncore_sync_jobs_pa(struct oncore_session *sess) +{ + phys_addr_t *tail = &sess->first_job_pa; + struct oncore_job *j; + + list_for_each_entry(j, &sess->jobs, sess_node) { + j->next_job_pa = 0; + *tail = virt_to_phys(j); + tail = &j->next_job_pa; + } + *tail = 0; +} + +static void oncore_session_destroy_incoming(struct oncore_session *sess) +{ + struct oncore_session *old_sess; + struct oncore_job *job; + phys_addr_t job_pa; + + if (!sess->ser) { + kfree(sess); + return; + } + + if (sess->ser->sess_pa) { + old_sess = phys_to_virt(sess->ser->sess_pa); + job_pa = old_sess->first_job_pa; + while (job_pa) { + job = phys_to_virt(job_pa); + job_pa = job->next_job_pa; + kho_restore_free(job); + } + kho_restore_free(old_sess); + } + + kho_restore_free(sess->ser); + kfree(sess); +} + +static void oncore_session_destroy_outgoing(struct oncore_session *sess) +{ + struct oncore_job *job, *tmp; + + list_for_each_entry_safe(job, tmp, &sess->jobs, sess_node) { + list_del_init(&job->sess_node); + kho_unpreserve_free(job); + } + + if (sess->ser) + kho_unpreserve_free(sess->ser); + kho_unpreserve_free(sess); +} + +static void oncore_session_destroy(struct oncore_session *sess) +{ + scoped_guard(mutex, &oncore_sessions_lock) + list_del_init(&sess->node); + + cpu_preserved_as_destroy(sess->as); + + if (sess->is_incoming) + oncore_session_destroy_incoming(sess); + else + oncore_session_destroy_outgoing(sess); +} + +/** + * oncore_session_remove_cpu - Detach a preserved physical CPU from an On-Core session + * @s: Live Update session handle. + * @cpu: Logical ID of the preserved physical CPU to remove. + * + * Detaches the On-Core scheduler worker from @cpu and clears @cpu from the + * session's CPU mask. When the last CPU in the session is removed, destroys + * the session's isolated address space and reclaims all KHO-preserved job and + * session structures (via kho_restore_free() in the incoming kernel after + * kexec, or kho_unpreserve_free() in the outgoing kernel on cancellation). + */ +void oncore_session_remove_cpu(struct liveupdate_session *s, int cpu) +{ + struct oncore_session *sess = oncore_find_session(s); + + if (!sess || cpu < 0 || cpu >= nr_cpu_ids) + return; + + scoped_guard(mutex, &sess->lock) + cpumask_clear_cpu(cpu, oncore_session_cpus(sess)); + + cpu_preserved_detach_workload(cpu); + cpu_preserved_set_workload_context(cpu, NULL, 0); + + if (cpumask_empty(oncore_session_cpus(sess))) + oncore_session_destroy(sess); +} + +/** + * oncore_session_get_ser - Retrieve the KHO serialization block for an On-Core session + * @s: Live Update session handle. + * + * Called by the preserved-CPU LUO file handler (cpu_preserve) when serializing + * a preserved CPU file descriptor so it can store a KHO pointer + * (&cpu_preserved_file_ser.oncore) to the session's metadata across kexec. + * + * Return: Pointer to the KHO-preserved &struct oncore_session_ser for @s, or + * %NULL if @s has no On-Core session. + */ +struct oncore_session_ser *oncore_session_get_ser(struct liveupdate_session *s) +{ + struct oncore_session *sess = oncore_find_session(s); + + return sess ? sess->ser : NULL; +} + +/** + * oncore_session_restore - Reconstruct an On-Core session in the incoming kernel + * @s: Incoming Live Update session handle. + * @ser: KHO-preserved session metadata handed over from the outgoing kernel. + * + * Adopts the KHO-preserved session metadata and isolated page tables (@ser->as) + * in the incoming kernel so that subsequent calls to oncore_session_remove_cpu() + * as preserved CPUs reattach to Linux can cleanly release the preserved page + * tables, jobs, and session structures. Idempotent if @s has already been + * restored. + */ +void oncore_session_restore(struct liveupdate_session *s, + struct oncore_session_ser *ser) +{ + struct cpu_preserved_as_ser *as_ser; + struct oncore_session *sess; + const char *sname; + + if (!ser) + return; + + sname = liveupdate_session_name(s); + if (!sname || !sname[0]) + sname = ser->session_name; + + guard(mutex)(&oncore_sessions_lock); + + if (oncore_find_session_locked(sname)) + return; + + sess = kzalloc_obj(*sess, GFP_KERNEL); + if (!sess) + return; + + mutex_init(&sess->lock); + INIT_LIST_HEAD(&sess->jobs); + oncore_runqueue_init(&sess->rq); + sess->is_incoming = true; + sess->ser = ser; + + as_ser = KHOSER_LOAD_PTR(ser->as); + if (as_ser) + sess->as = cpu_preserved_as_adopt(as_ser); + + list_add_tail(&sess->node, &oncore_sessions); +} + +static unsigned int oncore_session_cpu_job_count(struct oncore_session *sess, + int cpu) +{ + struct oncore_job *j; + unsigned int count = 0; + + list_for_each_entry(j, &sess->jobs, sess_node) { + if (j->assigned_cpu == cpu) + count++; + } + + return count; +} + +static int oncore_select_job_cpu(struct oncore_session *sess) +{ + struct cpumask *cpus = oncore_session_cpus(sess); + unsigned int min_count = UINT_MAX, count; + int cpu, min_cpu = -1; + + for_each_cpu(cpu, cpus) { + count = oncore_session_cpu_job_count(sess, cpu); + if (count < min_count) { + min_count = count; + min_cpu = cpu; + } + } + + return min_cpu; +} + +/** + * oncore_session_submit_job - Allocate and register a workload job in an On-Core session + * @s: Live Update session handle. + * @run_fn: Workload callback executed on a preserved physical CPU. + * @data: Opaque context pointer passed to @run_fn (may be %NULL and set + * later via oncore_job_set_data() before activation). + * + * Allocates a KHO-preserved &struct oncore_job, maps it into the session's + * isolated address space, assigns it to the least-loaded preserved CPU in the + * session, and links it into @s. The job is not placed on the runqueue until + * oncore_session_activate_job() is called. + * + * Return: Pointer to the allocated &struct oncore_job on success, %NULL if @s + * has no preserved CPUs, or an ERR_PTR() on failure. + */ +struct oncore_job *oncore_session_submit_job(struct liveupdate_session *s, + oncore_job_fn run_fn, + void *data) +{ + struct oncore_session *sess = oncore_find_session(s); + struct oncore_job *job; + + if (!run_fn) + return ERR_PTR(-EINVAL); + if (!sess) + return NULL; + + guard(mutex)(&sess->lock); + if (cpumask_empty(oncore_session_cpus(sess))) + return NULL; + + job = kho_alloc_preserve(sizeof(*job)); + if (IS_ERR(job)) + return job; + + memset(job, 0, sizeof(*job)); + oncore_session_map_buffer(sess, job, sizeof(*job)); + INIT_LIST_HEAD(&job->node); + INIT_LIST_HEAD(&job->sess_node); + job->session = sess; + job->state = ONCORE_JOB_NEW; + job->run_fn = run_fn; + job->data = data; + job->last_cpu = -1; + job->assigned_cpu = oncore_select_job_cpu(sess); + + list_add_tail(&job->sess_node, &sess->jobs); + oncore_sync_jobs_pa(sess); + + return job; +} +EXPORT_SYMBOL_GPL(oncore_session_submit_job); + +/** + * oncore_session_activate_job - Map a submitted job's data and enqueue it for execution + * @s: Live Update session handle. + * @job: Job previously returned by oncore_session_submit_job(). + * + * Maps the first page of @job->data (if non-%NULL) into the session's isolated + * address space, places @job onto the session's round-robin FIFO runqueue, and + * kicks the assigned preserved physical CPU so it wakes from low-power park + * wait and begins executing @job. + * + * Return: 0 on success, or -EINVAL if @s or @job is invalid. + */ +int oncore_session_activate_job(struct liveupdate_session *s, + struct oncore_job *job) +{ + struct oncore_session *sess = oncore_find_session(s); + + if (!sess || !job) + return -EINVAL; + + guard(mutex)(&sess->lock); + if (job->data) + oncore_session_map_buffer(sess, job->data, PAGE_SIZE); + oncore_sched_enqueue(&sess->rq, job); + if (cpu_is_preserved(job->assigned_cpu)) + arch_cpu_preserved_kick(job->assigned_cpu); + + return 0; +} +EXPORT_SYMBOL_GPL(oncore_session_activate_job); + +/** + * oncore_job_set_data - Set the opaque argument passed to a job's run callback + * @job: Job to update. + * @data: Pointer handed to @job's run_fn. May be %NULL. + * + * Callers that cannot determine @data at submission time submit with %NULL and + * call this once the object exists. It must be called before + * oncore_session_activate_job(), which is what maps @data into the session's + * address space. + */ +void oncore_job_set_data(struct oncore_job *job, void *data) +{ + if (job) + job->data = data; +} +EXPORT_SYMBOL_GPL(oncore_job_set_data); + +/** + * oncore_job_cpu - Return the preserved physical CPU assigned to a job + * @job: Job to query. + * + * Return: Logical CPU ID assigned to @job, or -1 if @job is %NULL. + */ +int oncore_job_cpu(const struct oncore_job *job) +{ + return job ? job->assigned_cpu : -1; +} +EXPORT_SYMBOL_GPL(oncore_job_cpu); + +/** + * oncore_job_session - Return the On-Core session that owns a job + * @job: Job to query. + * + * Return: Pointer to the owning &struct oncore_session, or %NULL if @job is %NULL. + */ +struct oncore_session *oncore_job_session(const struct oncore_job *job) +{ + return job ? job->session : NULL; +} +EXPORT_SYMBOL_GPL(oncore_job_session); + +/** + * oncore_session_cancel_job - Stop and free a submitted or running On-Core job + * @s: Live Update session handle. + * @job: Job to cancel. + * + * Removes @job from the session and runqueue. If @job is currently executing + * on a preserved physical CPU (%ONCORE_JOB_CANCELING), kicks that CPU and + * waits for the current scheduling quantum to finish before unpreserving and + * freeing @job. + * + * Return: 0 on success, or -EINVAL if @s or @job is invalid. + */ +int oncore_session_cancel_job(struct liveupdate_session *s, + struct oncore_job *job) +{ + struct oncore_session *sess = oncore_find_session(s); + int cpu, retries; + + if (!sess || !job) + return -EINVAL; + + guard(mutex)(&sess->lock); + list_del_init(&job->sess_node); + oncore_sync_jobs_pa(sess); + if (job->assigned_cpu >= 0) + job->assigned_cpu = -1; + oncore_sched_dequeue(&sess->rq, job); + + if (READ_ONCE(job->state) == ONCORE_JOB_CANCELING) { + cpu = READ_ONCE(job->last_cpu); + retries = 0; + + while (READ_ONCE(job->state) == ONCORE_JOB_CANCELING && + retries < (ONCORE_CANCEL_TIMEOUT_US / ONCORE_CANCEL_STEP_US)) { + if ((retries % 50) == 0 && cpu >= 0) + arch_cpu_preserved_kick(cpu); + udelay(ONCORE_CANCEL_STEP_US); + retries++; + } + } + + kho_unpreserve_free(job); + + return 0; +} +EXPORT_SYMBOL_GPL(oncore_session_cancel_job); -- 2.55.0.1082.g2b9226bbc0-goog

