Upstream: https://gitlab.com/qemu-project/qemu.git Upstream-Commit: d2e570cc0f97b936902a5b1b86b73c0f5998b475
613 lines
18 KiB
C
613 lines
18 KiB
C
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
/*
|
|
* RAM Discard Manager
|
|
*
|
|
* Copyright Red Hat, Inc. 2026
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/error-report.h"
|
|
#include "qemu/queue.h"
|
|
#include "system/memory.h"
|
|
|
|
static uint64_t ram_discard_source_get_min_granularity(const RamDiscardSource *rds,
|
|
const MemoryRegion *mr)
|
|
{
|
|
RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_GET_CLASS(rds);
|
|
|
|
g_assert(rdsc->get_min_granularity);
|
|
return rdsc->get_min_granularity(rds, mr);
|
|
}
|
|
|
|
static bool ram_discard_source_is_populated(const RamDiscardSource *rds,
|
|
const MemoryRegionSection *section)
|
|
{
|
|
RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_GET_CLASS(rds);
|
|
|
|
g_assert(rdsc->is_populated);
|
|
return rdsc->is_populated(rds, section);
|
|
}
|
|
|
|
/*
|
|
* Iterate a single source's populated or discarded regions and call
|
|
* replay_fn for each contiguous run.
|
|
*/
|
|
static int replay_source_by_state(const RamDiscardSource *source,
|
|
const MemoryRegion *mr,
|
|
const MemoryRegionSection *section,
|
|
bool replay_populated,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
uint64_t granularity, offset, size, end, pos, run_start = 0;
|
|
bool in_run = false;
|
|
int ret = 0;
|
|
|
|
granularity = ram_discard_source_get_min_granularity(source, mr);
|
|
offset = section->offset_within_region;
|
|
size = int128_get64(section->size);
|
|
end = offset + size;
|
|
|
|
/* Align iteration to granularity boundaries */
|
|
pos = QEMU_ALIGN_DOWN(offset, granularity);
|
|
|
|
for (; pos < end; pos += granularity) {
|
|
MemoryRegionSection chunk = {
|
|
.mr = section->mr,
|
|
.offset_within_region = pos,
|
|
.size = int128_make64(granularity),
|
|
};
|
|
bool populated = ram_discard_source_is_populated(source, &chunk);
|
|
|
|
if (populated == replay_populated) {
|
|
if (!in_run) {
|
|
run_start = pos;
|
|
in_run = true;
|
|
}
|
|
} else if (in_run) {
|
|
MemoryRegionSection tmp = *section;
|
|
|
|
if (memory_region_section_intersect_range(&tmp, run_start,
|
|
pos - run_start)) {
|
|
ret = replay_fn(&tmp, opaque);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
}
|
|
in_run = false;
|
|
}
|
|
}
|
|
|
|
if (in_run) {
|
|
MemoryRegionSection tmp = *section;
|
|
|
|
if (memory_region_section_intersect_range(&tmp, run_start,
|
|
pos - run_start)) {
|
|
ret = replay_fn(&tmp, opaque);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
RamDiscardManager *ram_discard_manager_new(MemoryRegion *mr)
|
|
{
|
|
RamDiscardManager *rdm;
|
|
|
|
rdm = RAM_DISCARD_MANAGER(object_new(TYPE_RAM_DISCARD_MANAGER));
|
|
rdm->mr = mr;
|
|
return rdm;
|
|
}
|
|
|
|
static void ram_discard_manager_update_granularity(RamDiscardManager *rdm)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
uint64_t granularity = 0;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
uint64_t src_granularity;
|
|
|
|
src_granularity =
|
|
ram_discard_source_get_min_granularity(entry->rds, rdm->mr);
|
|
g_assert(src_granularity != 0);
|
|
if (granularity == 0) {
|
|
granularity = src_granularity;
|
|
} else {
|
|
granularity = MIN(granularity, src_granularity);
|
|
}
|
|
}
|
|
rdm->min_granularity = granularity;
|
|
}
|
|
|
|
static RamDiscardSourceEntry *
|
|
ram_discard_manager_find_source(RamDiscardManager *rdm, RamDiscardSource *rds)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (entry->rds == rds) {
|
|
return entry;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static int rdl_populate_cb(const MemoryRegionSection *section, void *opaque)
|
|
{
|
|
RamDiscardListener *rdl = opaque;
|
|
MemoryRegionSection tmp = *rdl->section;
|
|
|
|
g_assert(section->mr == rdl->section->mr);
|
|
|
|
if (!memory_region_section_intersect_range(&tmp,
|
|
section->offset_within_region,
|
|
int128_get64(section->size))) {
|
|
return 0;
|
|
}
|
|
|
|
return rdl->notify_populate(rdl, &tmp);
|
|
}
|
|
|
|
static int rdl_discard_cb(const MemoryRegionSection *section, void *opaque)
|
|
{
|
|
RamDiscardListener *rdl = opaque;
|
|
MemoryRegionSection tmp = *rdl->section;
|
|
|
|
g_assert(section->mr == rdl->section->mr);
|
|
|
|
if (!memory_region_section_intersect_range(&tmp,
|
|
section->offset_within_region,
|
|
int128_get64(section->size))) {
|
|
return 0;
|
|
}
|
|
|
|
rdl->notify_discard(rdl, &tmp);
|
|
return 0;
|
|
}
|
|
|
|
static bool rdm_is_all_populated_skip(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
const RamDiscardSource *skip_source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (skip_source && entry->rds == skip_source) {
|
|
continue;
|
|
}
|
|
if (!ram_discard_source_is_populated(entry->rds, section)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
typedef struct SourceNotifyCtx {
|
|
RamDiscardManager *rdm;
|
|
RamDiscardListener *rdl;
|
|
RamDiscardSource *source; /* added or removed */
|
|
} SourceNotifyCtx;
|
|
|
|
/*
|
|
* Unified helper to replay regions based on populated state.
|
|
* If replay_populated is true: replay regions where ALL sources are populated.
|
|
* If replay_populated is false: replay regions where ANY source is discarded.
|
|
*/
|
|
static int replay_by_populated_state(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
const RamDiscardSource *skip_source,
|
|
bool replay_populated,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *user_opaque)
|
|
{
|
|
uint64_t granularity = rdm->min_granularity;
|
|
uint64_t offset, end_offset;
|
|
uint64_t run_start = 0;
|
|
bool in_run = false;
|
|
int ret = 0;
|
|
|
|
if (QLIST_EMPTY(&rdm->source_list)) {
|
|
if (replay_populated) {
|
|
return replay_fn(section, user_opaque);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
g_assert(granularity != 0);
|
|
|
|
offset = section->offset_within_region;
|
|
end_offset = offset + int128_get64(section->size);
|
|
|
|
while (offset < end_offset) {
|
|
MemoryRegionSection subsection = {
|
|
.mr = section->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(MIN(granularity, end_offset - offset)),
|
|
};
|
|
bool all_populated;
|
|
bool included;
|
|
|
|
all_populated = rdm_is_all_populated_skip(rdm, &subsection,
|
|
skip_source);
|
|
included = replay_populated ? all_populated : !all_populated;
|
|
|
|
if (included) {
|
|
if (!in_run) {
|
|
run_start = offset;
|
|
in_run = true;
|
|
}
|
|
} else {
|
|
if (in_run) {
|
|
MemoryRegionSection run_section = {
|
|
.mr = section->mr,
|
|
.offset_within_region = run_start,
|
|
.size = int128_make64(offset - run_start),
|
|
};
|
|
ret = replay_fn(&run_section, user_opaque);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
in_run = false;
|
|
}
|
|
}
|
|
if (granularity > end_offset - offset) {
|
|
break;
|
|
}
|
|
offset += granularity;
|
|
}
|
|
|
|
if (in_run) {
|
|
MemoryRegionSection run_section = {
|
|
.mr = section->mr,
|
|
.offset_within_region = run_start,
|
|
.size = int128_make64(end_offset - run_start),
|
|
};
|
|
ret = replay_fn(&run_section, user_opaque);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int add_source_check_discard_cb(const MemoryRegionSection *section,
|
|
void *opaque)
|
|
{
|
|
SourceNotifyCtx *ctx = opaque;
|
|
|
|
return replay_by_populated_state(ctx->rdm, section, ctx->source, true,
|
|
rdl_discard_cb, ctx->rdl);
|
|
}
|
|
|
|
static int del_source_check_populate_cb(const MemoryRegionSection *section,
|
|
void *opaque)
|
|
{
|
|
SourceNotifyCtx *ctx = opaque;
|
|
|
|
return replay_by_populated_state(ctx->rdm, section, ctx->source, true,
|
|
rdl_populate_cb, ctx->rdl);
|
|
}
|
|
|
|
int ram_discard_manager_add_source(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
RamDiscardListener *rdl, *rdl2;
|
|
int ret = 0;
|
|
|
|
if (ram_discard_manager_find_source(rdm, source)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
/*
|
|
* If there are existing listeners, notify them about regions that
|
|
* become discarded due to adding this source. Only notify for regions
|
|
* that were previously populated (all other sources agreed).
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl,
|
|
/* no need to set source */
|
|
};
|
|
ret = replay_source_by_state(source, rdm->mr, rdl->section,
|
|
false,
|
|
add_source_check_discard_cb, &ctx);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
if (ret) {
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl2,
|
|
};
|
|
replay_source_by_state(source, rdm->mr, rdl2->section,
|
|
false,
|
|
del_source_check_populate_cb,
|
|
&ctx);
|
|
if (rdl == rdl2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
entry = g_new0(RamDiscardSourceEntry, 1);
|
|
entry->rds = source;
|
|
QLIST_INSERT_HEAD(&rdm->source_list, entry, next);
|
|
|
|
ram_discard_manager_update_granularity(rdm);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int ram_discard_manager_del_source(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
RamDiscardListener *rdl, *rdl2;
|
|
int ret = 0;
|
|
|
|
entry = ram_discard_manager_find_source(rdm, source);
|
|
if (!entry) {
|
|
return -ENOENT;
|
|
}
|
|
|
|
/*
|
|
* If there are existing listeners, check if any regions become
|
|
* populated due to removing this source.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl,
|
|
.source = source,
|
|
};
|
|
/*
|
|
* From the previously discarded regions, check if any
|
|
* regions become populated.
|
|
*/
|
|
ret = replay_source_by_state(source, rdm->mr, rdl->section,
|
|
false,
|
|
del_source_check_populate_cb,
|
|
&ctx);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
if (ret) {
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
SourceNotifyCtx ctx = {
|
|
.rdm = rdm,
|
|
.rdl = rdl2,
|
|
.source = source,
|
|
};
|
|
replay_source_by_state(source, rdm->mr, rdl2->section,
|
|
false,
|
|
add_source_check_discard_cb,
|
|
&ctx);
|
|
if (rdl == rdl2) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
QLIST_REMOVE(entry, next);
|
|
g_free(entry);
|
|
ram_discard_manager_update_granularity(rdm);
|
|
return ret;
|
|
}
|
|
|
|
uint64_t ram_discard_manager_get_min_granularity(const RamDiscardManager *rdm,
|
|
const MemoryRegion *mr)
|
|
{
|
|
g_assert(mr == rdm->mr);
|
|
return rdm->min_granularity;
|
|
}
|
|
|
|
/*
|
|
* Aggregated query: returns true only if ALL sources report populated (AND).
|
|
*/
|
|
bool ram_discard_manager_is_populated(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section)
|
|
{
|
|
RamDiscardSourceEntry *entry;
|
|
|
|
QLIST_FOREACH(entry, &rdm->source_list, next) {
|
|
if (!ram_discard_source_is_populated(entry->rds, section)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int ram_discard_manager_replay_populated(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
return replay_by_populated_state(rdm, section, NULL, true,
|
|
replay_fn, opaque);
|
|
}
|
|
|
|
int ram_discard_manager_replay_discarded(const RamDiscardManager *rdm,
|
|
const MemoryRegionSection *section,
|
|
ReplayRamDiscardState replay_fn,
|
|
void *opaque)
|
|
{
|
|
return replay_by_populated_state(rdm, section, NULL, false,
|
|
replay_fn, opaque);
|
|
}
|
|
|
|
static void ram_discard_manager_initfn(Object *obj)
|
|
{
|
|
RamDiscardManager *rdm = RAM_DISCARD_MANAGER(obj);
|
|
|
|
QLIST_INIT(&rdm->source_list);
|
|
QLIST_INIT(&rdm->rdl_list);
|
|
rdm->min_granularity = 0;
|
|
}
|
|
|
|
static void ram_discard_manager_finalize(Object *obj)
|
|
{
|
|
RamDiscardManager *rdm = RAM_DISCARD_MANAGER(obj);
|
|
|
|
g_assert(QLIST_EMPTY(&rdm->rdl_list));
|
|
g_assert(QLIST_EMPTY(&rdm->source_list));
|
|
}
|
|
|
|
int ram_discard_manager_notify_populate(RamDiscardManager *rdm,
|
|
RamDiscardSource *source,
|
|
uint64_t offset, uint64_t size)
|
|
{
|
|
RamDiscardListener *rdl, *rdl2;
|
|
MemoryRegionSection section = {
|
|
.mr = rdm->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(size),
|
|
};
|
|
int ret = 0;
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
/*
|
|
* Only notify about regions that are populated in ALL sources.
|
|
* Skip the calling source: it has implicitly declared itself populated
|
|
* for this range but may not have updated its bitmap yet.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
ret = replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ret) {
|
|
/*
|
|
* Rollback: notify discard for listeners we already notified,
|
|
* including the failing listener which may have been partially
|
|
* notified. Listeners must handle discard notifications for
|
|
* regions they didn't receive populate notifications for.
|
|
*/
|
|
QLIST_FOREACH(rdl2, &rdm->rdl_list, next) {
|
|
replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_discard_cb, rdl2);
|
|
if (rdl2 == rdl) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void ram_discard_manager_notify_discard(RamDiscardManager *rdm,
|
|
RamDiscardSource *source,
|
|
uint64_t offset, uint64_t size)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
MemoryRegionSection section = {
|
|
.mr = rdm->mr,
|
|
.offset_within_region = offset,
|
|
.size = int128_make64(size),
|
|
};
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
/*
|
|
* Only notify about ranges that were aggregately populated before this
|
|
* source's discard. Since the source has already updated its state,
|
|
* we use replay_by_populated_state with this source skipped - it will
|
|
* replay only the ranges where all OTHER sources are populated.
|
|
*/
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
replay_by_populated_state(rdm, §ion, source, true,
|
|
rdl_discard_cb, rdl);
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_notify_discard_all(RamDiscardManager *rdm,
|
|
RamDiscardSource *source)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
|
|
g_assert(ram_discard_manager_find_source(rdm, source));
|
|
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
rdl->notify_discard(rdl, rdl->section);
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_register_listener(RamDiscardManager *rdm,
|
|
RamDiscardListener *rdl,
|
|
MemoryRegionSection *section)
|
|
{
|
|
int ret;
|
|
|
|
g_assert(section->mr == rdm->mr);
|
|
|
|
object_ref(rdm);
|
|
rdl->section = memory_region_section_new_copy(section);
|
|
QLIST_INSERT_HEAD(&rdm->rdl_list, rdl, next);
|
|
|
|
ret = ram_discard_manager_replay_populated(rdm, rdl->section,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
error_report("%s: Replaying populated ranges failed: %s", __func__,
|
|
strerror(-ret));
|
|
}
|
|
}
|
|
|
|
void ram_discard_manager_unregister_listener(RamDiscardManager *rdm,
|
|
RamDiscardListener *rdl)
|
|
{
|
|
g_assert(rdl->section);
|
|
g_assert(rdl->section->mr == rdm->mr);
|
|
|
|
rdl->notify_discard(rdl, rdl->section);
|
|
memory_region_section_free_copy(rdl->section);
|
|
rdl->section = NULL;
|
|
QLIST_REMOVE(rdl, next);
|
|
object_unref(rdm);
|
|
}
|
|
|
|
int ram_discard_manager_replay_populated_to_listeners(RamDiscardManager *rdm)
|
|
{
|
|
RamDiscardListener *rdl;
|
|
int ret = 0;
|
|
|
|
QLIST_FOREACH(rdl, &rdm->rdl_list, next) {
|
|
ret = ram_discard_manager_replay_populated(rdm, rdl->section,
|
|
rdl_populate_cb, rdl);
|
|
if (ret) {
|
|
break;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static const TypeInfo ram_discard_manager_info = {
|
|
.parent = TYPE_OBJECT,
|
|
.name = TYPE_RAM_DISCARD_MANAGER,
|
|
.instance_size = sizeof(RamDiscardManager),
|
|
.instance_init = ram_discard_manager_initfn,
|
|
.instance_finalize = ram_discard_manager_finalize,
|
|
};
|
|
|
|
static const TypeInfo ram_discard_source_info = {
|
|
.parent = TYPE_INTERFACE,
|
|
.name = TYPE_RAM_DISCARD_SOURCE,
|
|
.class_size = sizeof(RamDiscardSourceClass),
|
|
};
|
|
|
|
static void ram_discard_manager_register_types(void)
|
|
{
|
|
type_register_static(&ram_discard_manager_info);
|
|
type_register_static(&ram_discard_source_info);
|
|
}
|
|
|
|
type_init(ram_discard_manager_register_types)
|