vmm: add per-zone mergeable option to --memory-zone

Add a `mergeable` field to `MemoryZoneConfig` so that KSM page merging
can be enabled selectively per memory zone rather than globally for all
guest RAM.

Previously, `MADV_MERGEABLE` was only controllable via the top-level
`--memory mergeable=on` flag, which applied uniformly to all regions.
With this change, users can leave boot memory unmerged while enabling
KSM only on hotplug zones:

  --memory size=0,hotplug_method=virtio-mem
  --memory-zone id=boot,size=512M,shared=on,mergeable=off
  --memory-zone id=hotplug,size=256M,hotplug_size=1G,shared=off,mergeable=on

The `MemoryZone` runtime struct now carries the `mergeable` flag so
that both `allocate_address_space` and `add_ram_region` can apply
per-zone `MADV_MERGEABLE` instead of the global `self.mergeable`.
The top-level `--memory mergeable=on` path continues to work unchanged:
the default zone is synthesised from `MemoryConfig` and inherits its
`mergeable` value.

AI/LLM disclosure: this patch was co-authored with
GitHub Copilot and Claude Code (Opus 4.6).

Signed-off-by: JP Kobryn <inwardvessel@gmail.com>
This commit is contained in:
JP Kobryn
2026-04-08 18:52:01 -07:00
committed by Rob Bradford
parent 3a23e2f841
commit bdc7a6947d
4 changed files with 145 additions and 11 deletions
+89 -1
View File
@@ -1006,7 +1006,8 @@ impl MemoryConfig {
.add("host_numa_node")
.add("hotplug_size")
.add("hotplugged_size")
.add("prefault");
.add("prefault")
.add("mergeable");
parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?;
let id = parser.get("id").ok_or(Error::ParseMemoryZoneIdMissing)?;
@@ -1047,6 +1048,11 @@ impl MemoryConfig {
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(false))
.0;
let mergeable = parser
.convert::<Toggle>("mergeable")
.map_err(Error::ParseMemoryZone)?
.unwrap_or(Toggle(mergeable))
.0;
zones.push(MemoryZoneConfig {
id,
@@ -1059,6 +1065,7 @@ impl MemoryConfig {
hotplug_size,
hotplugged_size,
prefault,
mergeable,
});
}
Some(zones)
@@ -3750,6 +3757,87 @@ mod unit_tests {
Ok(())
}
#[test]
fn test_mem_zone_parsing() -> Result<()> {
// mergeable defaults to false
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
..Default::default()
}]),
..Default::default()
}
);
// mergeable=on
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=on"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
// mergeable=off is explicit false
assert_eq!(
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=off"]))?,
MemoryConfig {
size: 0,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: false,
..Default::default()
}]),
..Default::default()
}
);
// per-zone mergeable independent of global mergeable
assert_eq!(
MemoryConfig::parse(
"size=1G,mergeable=off",
Some(vec!["id=hotplug,size=0,hotplug_size=4G,mergeable=on"])
)?,
MemoryConfig {
size: 1 << 30,
mergeable: false,
hotplug_method: HotplugMethod::Acpi,
zones: Some(vec![MemoryZoneConfig {
id: "hotplug".to_string(),
size: 0,
hotplug_size: Some(4 << 30),
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
// global mergeable=on inherited by zone with no explicit mergeable
assert_eq!(
MemoryConfig::parse("size=0,mergeable=on", Some(vec!["id=mem0,size=1G"]))?,
MemoryConfig {
size: 0,
mergeable: true,
zones: Some(vec![MemoryZoneConfig {
id: "mem0".to_string(),
size: 1 << 30,
mergeable: true,
..Default::default()
}]),
..Default::default()
}
);
Ok(())
}
#[test]
fn test_mem_parsing() -> Result<()> {
assert_eq!(MemoryConfig::parse("", None)?, MemoryConfig::default());