mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
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:
@@ -214,11 +214,12 @@ struct MemoryZoneConfig {
|
||||
hotplug_size: Option<u64>,
|
||||
hotplugged_size: Option<u64>,
|
||||
prefault: bool,
|
||||
mergeable: bool,
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
--memory-zone <memory-zone> User defined memory zone parameters "size=<guest_memory_region_size>,file=<backing_file>,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,host_numa_node=<node_id>,id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off"
|
||||
--memory-zone <memory-zone> User defined memory zone parameters "size=<guest_memory_region_size>,file=<backing_file>,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,host_numa_node=<node_id>,id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,mergeable=on|off"
|
||||
```
|
||||
|
||||
This parameter expects one or more occurrences, allowing for a list of memory
|
||||
@@ -422,6 +423,34 @@ _Example_
|
||||
--memory-zone id=mem0,size=1G,prefault=on
|
||||
```
|
||||
|
||||
### `mergeable`
|
||||
|
||||
Specifies if the pages from this memory zone must be marked as _mergeable_,
|
||||
enabling Kernel Same-page Merging (KSM) for this zone.
|
||||
|
||||
This is the per-zone equivalent of the top-level `--memory mergeable=on` option.
|
||||
It allows KSM to be enabled selectively — for example, enabling it only on a
|
||||
hotplug zone while leaving boot memory unaffected:
|
||||
|
||||
```
|
||||
--memory size=2G,mergeable=off
|
||||
--memory-zone id=hotplug,size=0,hotplug_size=8G,mergeable=on
|
||||
```
|
||||
|
||||
For KSM to have any effect, the host kernel must have KSM enabled:
|
||||
```
|
||||
echo 1 > /sys/kernel/mm/ksm/run
|
||||
```
|
||||
|
||||
By default this option is turned off.
|
||||
|
||||
_Example_
|
||||
|
||||
```
|
||||
--memory size=0
|
||||
--memory-zone id=mem0,size=1G,mergeable=on
|
||||
```
|
||||
|
||||
## NUMA settings
|
||||
|
||||
`NumaConfig` or what is known as `--numa` from the CLI perspective has been
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -138,16 +138,18 @@ pub struct MemoryZone {
|
||||
shared: bool,
|
||||
hugepages: bool,
|
||||
backing_page_size: u64,
|
||||
mergeable: bool,
|
||||
}
|
||||
|
||||
impl MemoryZone {
|
||||
fn new(shared: bool, hugepages: bool, backing_page_size: u64) -> Self {
|
||||
fn new(shared: bool, hugepages: bool, backing_page_size: u64, mergeable: bool) -> Self {
|
||||
Self {
|
||||
regions: Vec::new(),
|
||||
virtio_mem_zone: None,
|
||||
shared,
|
||||
hugepages,
|
||||
backing_page_size,
|
||||
mergeable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,7 +609,7 @@ impl MemoryManager {
|
||||
// Add zone id to the list of memory zones.
|
||||
memory_zones.insert(
|
||||
zone.id.clone(),
|
||||
MemoryZone::new(zone.shared, zone.hugepages, zone_align_size),
|
||||
MemoryZone::new(zone.shared, zone.hugepages, zone_align_size, zone.mergeable),
|
||||
);
|
||||
|
||||
for ram_region in ram_regions.iter() {
|
||||
@@ -701,7 +703,12 @@ impl MemoryManager {
|
||||
}
|
||||
memory_zones.insert(
|
||||
zone.id.clone(),
|
||||
MemoryZone::new(zone.shared, zone.hugepages, zone_align_size),
|
||||
MemoryZone::new(
|
||||
zone.shared,
|
||||
zone.hugepages,
|
||||
zone_align_size,
|
||||
zone.mergeable,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -733,7 +740,12 @@ impl MemoryManager {
|
||||
let zone_page_size = memory_zone_get_align_size(zone_config)?;
|
||||
memory_zones.insert(
|
||||
zone_config.id.clone(),
|
||||
MemoryZone::new(zone_config.shared, zone_config.hugepages, zone_page_size),
|
||||
MemoryZone::new(
|
||||
zone_config.shared,
|
||||
zone_config.hugepages,
|
||||
zone_page_size,
|
||||
zone_config.mergeable,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1295,6 +1307,7 @@ impl MemoryManager {
|
||||
hotplug_size: config.hotplug_size,
|
||||
hotplugged_size: config.hotplugged_size,
|
||||
prefault: config.prefault,
|
||||
mergeable: config.mergeable,
|
||||
}];
|
||||
|
||||
Ok((config.size, zones, allow_mem_hotplug))
|
||||
@@ -1316,10 +1329,10 @@ impl MemoryManager {
|
||||
regions.push((virtio_mem_zone.region().clone(), true));
|
||||
}
|
||||
|
||||
list.push((zone_id.clone(), regions));
|
||||
list.push((zone_id.clone(), regions, memory_zone.mergeable));
|
||||
}
|
||||
|
||||
for (zone_id, regions) in list {
|
||||
for (zone_id, regions, zone_mergeable) in list {
|
||||
for (region, virtio_mem) in regions {
|
||||
// SAFETY: guaranteed by GuestRegionMmap invariants
|
||||
let slot = unsafe {
|
||||
@@ -1327,7 +1340,7 @@ impl MemoryManager {
|
||||
region.start_addr().raw_value(),
|
||||
region.len().try_into().unwrap(),
|
||||
region.as_ptr(),
|
||||
self.mergeable,
|
||||
zone_mergeable,
|
||||
false,
|
||||
self.log_dirty,
|
||||
)
|
||||
@@ -2087,7 +2100,9 @@ impl MemoryManager {
|
||||
region.start_addr().0,
|
||||
region.len().try_into().unwrap(),
|
||||
region.as_ptr(),
|
||||
self.mergeable,
|
||||
self.memory_zones
|
||||
.get(DEFAULT_MEMORY_ZONE)
|
||||
.map_or(self.mergeable, |z| z.mergeable),
|
||||
false,
|
||||
self.log_dirty,
|
||||
)
|
||||
|
||||
@@ -159,7 +159,7 @@ pub struct PciSegmentConfig {
|
||||
pub mmio64_aperture_weight: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct MemoryZoneConfig {
|
||||
pub id: String,
|
||||
pub size: u64,
|
||||
@@ -179,6 +179,8 @@ pub struct MemoryZoneConfig {
|
||||
pub hotplugged_size: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub prefault: bool,
|
||||
#[serde(default)]
|
||||
pub mergeable: bool,
|
||||
}
|
||||
|
||||
impl ApplyLandlock for MemoryZoneConfig {
|
||||
|
||||
Reference in New Issue
Block a user