mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: add memory reserve option to opt out of MAP_NORESERVE
Cloud Hypervisor maps guest RAM with MAP_NORESERVE, so the kernel never reserves the backing pages at mmap time. On a host whose hugepage pool cannot satisfy every guest, a VM is created successfully and then takes a SIGBUS when the guest faults a page the pool can no longer back. This is the failure mode reported in #5730 and #7387. As noted on #5730, checking free pool headroom up front is not a reliable fix: another process can consume pages between the check and the fault. Add a reserve=on parameter to --memory and --memory-zone (default off, preserving the current MAP_NORESERVE behaviour). When set, the region is mapped without MAP_NORESERVE, so the kernel reserves the backing pages (swap, or huge pages for hugepage-backed memory) at mmap time, atomically with the mapping. An over-committed configuration then fails cleanly at VM creation with an mmap ENOMEM instead of crashing the guest later. Unlike prefault it does not fault the memory in, so it does not slow down boot. This mirrors QEMU's memory-backend reserve property, which has the same name and meaning (reserve=off maps with MAP_NORESERVE). reserve is threaded through the same mmap paths as the existing prefault option, and is exposed in the OpenAPI schema, CLI help and docs. The top-level --memory reserve=on path is unchanged: the default zone is synthesised from MemoryConfig and inherits its reserve value. Assisted-by: Claude Code (Opus 4.8) Signed-off-by: Ian Klemm <hi@ianklemm.de>
This commit is contained in:
@@ -321,7 +321,7 @@ fn get_cli_options_sorted(
|
||||
hotplug_method=acpi|virtio-mem,\
|
||||
hotplug_size=<hotpluggable_memory_size>,\
|
||||
hotplugged_size=<hotplugged_memory_size>,\
|
||||
prefault=on|off,thp=on|off\"",
|
||||
prefault=on|off,reserve=on|off,thp=on|off\"",
|
||||
)
|
||||
.default_value(default_memory)
|
||||
.group("vm-config"),
|
||||
@@ -335,7 +335,7 @@ fn get_cli_options_sorted(
|
||||
host_numa_node=<node_id>,\
|
||||
id=<zone_identifier>,hotplug_size=<hotpluggable_memory_size>,\
|
||||
hotplugged_size=<hotplugged_memory_size>,\
|
||||
prefault=on|off\"",
|
||||
prefault=on|off,reserve=on|off\"",
|
||||
)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Append)
|
||||
@@ -1003,6 +1003,7 @@ mod unit_tests {
|
||||
hugepages: false,
|
||||
hugepage_size: None,
|
||||
prefault: false,
|
||||
reserve: false,
|
||||
zones: None,
|
||||
thp: true,
|
||||
},
|
||||
|
||||
@@ -20,13 +20,14 @@ struct MemoryConfig {
|
||||
hugepages: bool,
|
||||
hugepage_size: Option<u64>,
|
||||
prefault: bool,
|
||||
reserve: bool,
|
||||
thp: bool,
|
||||
zones: Option<Vec<MemoryZoneConfig>>,
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
--memory <memory> Memory parameters "size=<guest_memory_size>,mergeable=on|off,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,hotplug_method=acpi|virtio-mem,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,thp=on|off" [default: size=512M,thp=on]
|
||||
--memory <memory> Memory parameters "size=<guest_memory_size>,mergeable=on|off,shared=on|off,hugepages=on|off,hugepage_size=<hugepage_size>,hotplug_method=acpi|virtio-mem,hotplug_size=<hotpluggable_memory_size>,hotplugged_size=<hotplugged_memory_size>,prefault=on|off,reserve=on|off,thp=on|off" [default: size=512M,thp=on]
|
||||
```
|
||||
|
||||
### `size`
|
||||
@@ -177,6 +178,32 @@ _Example_
|
||||
--memory size=1G,prefault=on
|
||||
```
|
||||
|
||||
### `reserve`
|
||||
|
||||
Specifies if guest memory should be `mmap(2)`-ed _without_ the `MAP_NORESERVE`
|
||||
flag, asking the kernel to reserve the backing pages (swap space, or huge pages
|
||||
for hugepage-backed memory) for the whole region up front at `mmap` time.
|
||||
|
||||
By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM
|
||||
creation succeeds even when the backing pool cannot satisfy the full guest size.
|
||||
The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when
|
||||
it faults a page the pool cannot back. With `reserve=on` the reservation is made
|
||||
when the memory is mapped, so an over-committed configuration fails cleanly at VM
|
||||
creation with an out-of-memory error instead of crashing the guest at run time.
|
||||
This is most useful for hugepage-backed memory, where the huge pages are reserved
|
||||
from the pool.
|
||||
|
||||
Unlike `prefault`, this does not populate or fault in the memory, so it does not
|
||||
slow down boot; it only reserves it.
|
||||
|
||||
By default this option is turned off.
|
||||
|
||||
_Example_
|
||||
|
||||
```
|
||||
--memory size=1G,hugepages=on,reserve=on
|
||||
```
|
||||
|
||||
### `thp`
|
||||
|
||||
Specifies if private anonymous memory for the guest (i.e. `shared=off` and no
|
||||
@@ -214,12 +241,13 @@ struct MemoryZoneConfig {
|
||||
hotplug_size: Option<u64>,
|
||||
hotplugged_size: Option<u64>,
|
||||
prefault: bool,
|
||||
reserve: 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,mergeable=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,reserve=on|off,mergeable=on|off"
|
||||
```
|
||||
|
||||
This parameter expects one or more occurrences, allowing for a list of memory
|
||||
@@ -423,6 +451,34 @@ _Example_
|
||||
--memory-zone id=mem0,size=1G,prefault=on
|
||||
```
|
||||
|
||||
### `reserve`
|
||||
|
||||
Specifies if the memory for this zone should be `mmap(2)`-ed _without_ the
|
||||
`MAP_NORESERVE` flag, asking the kernel to reserve the backing pages (swap space,
|
||||
or huge pages for hugepage-backed memory) for the whole zone up front at `mmap`
|
||||
time.
|
||||
|
||||
By default Cloud Hypervisor maps guest memory with `MAP_NORESERVE`, so VM
|
||||
creation succeeds even when the backing pool cannot satisfy the full zone size.
|
||||
The shortfall then surfaces only later, as a `SIGBUS` delivered to the guest when
|
||||
it faults a page the pool cannot back. With `reserve=on` the reservation is made
|
||||
when the memory is mapped, so an over-committed configuration fails cleanly at VM
|
||||
creation with an out-of-memory error instead of crashing the guest at run time.
|
||||
This is most useful for hugepage-backed zones, where the huge pages are reserved
|
||||
from the pool.
|
||||
|
||||
Unlike `prefault`, this does not populate or fault in the memory, so it does not
|
||||
slow down boot; it only reserves it.
|
||||
|
||||
By default this option is turned off.
|
||||
|
||||
_Example_
|
||||
|
||||
```
|
||||
--memory size=0
|
||||
--memory-zone id=mem0,size=1G,hugepages=on,reserve=on
|
||||
```
|
||||
|
||||
### `mergeable`
|
||||
|
||||
Specifies if the pages from this memory zone must be marked as _mergeable_,
|
||||
|
||||
@@ -150,6 +150,7 @@ impl RequestHandler for StubApiRequestHandler {
|
||||
hugepages: false,
|
||||
hugepage_size: None,
|
||||
prefault: false,
|
||||
reserve: false,
|
||||
zones: None,
|
||||
thp: true,
|
||||
},
|
||||
|
||||
@@ -148,6 +148,7 @@ fn create_dummy_virtio_mem(bytes: &[u8; VIRTIO_MEM_DATA_SIZE]) -> (Mem, Arc<Gues
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
numa_id,
|
||||
None,
|
||||
|
||||
@@ -862,6 +862,9 @@ components:
|
||||
prefault:
|
||||
type: boolean
|
||||
default: false
|
||||
reserve:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
MemoryConfig:
|
||||
required:
|
||||
@@ -895,6 +898,9 @@ components:
|
||||
prefault:
|
||||
type: boolean
|
||||
default: false
|
||||
reserve:
|
||||
type: boolean
|
||||
default: false
|
||||
thp:
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
@@ -1060,6 +1060,7 @@ impl MemoryConfig {
|
||||
.add("hugepages")
|
||||
.add("hugepage_size")
|
||||
.add("prefault")
|
||||
.add("reserve")
|
||||
.add("thp");
|
||||
parser.parse(memory).map_err(Error::ParseMemory)?;
|
||||
|
||||
@@ -1104,6 +1105,11 @@ impl MemoryConfig {
|
||||
.map_err(Error::ParseMemory)?
|
||||
.unwrap_or(Toggle(false))
|
||||
.0;
|
||||
let reserve = parser
|
||||
.convert::<Toggle>("reserve")
|
||||
.map_err(Error::ParseMemory)?
|
||||
.unwrap_or(Toggle(false))
|
||||
.0;
|
||||
let thp = parser
|
||||
.convert::<Toggle>("thp")
|
||||
.map_err(Error::ParseMemory)?
|
||||
@@ -1125,6 +1131,7 @@ impl MemoryConfig {
|
||||
.add("hotplug_size")
|
||||
.add("hotplugged_size")
|
||||
.add("prefault")
|
||||
.add("reserve")
|
||||
.add("mergeable");
|
||||
parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?;
|
||||
|
||||
@@ -1166,6 +1173,11 @@ impl MemoryConfig {
|
||||
.map_err(Error::ParseMemoryZone)?
|
||||
.unwrap_or(Toggle(false))
|
||||
.0;
|
||||
let reserve = parser
|
||||
.convert::<Toggle>("reserve")
|
||||
.map_err(Error::ParseMemoryZone)?
|
||||
.unwrap_or(Toggle(false))
|
||||
.0;
|
||||
let mergeable = parser
|
||||
.convert::<Toggle>("mergeable")
|
||||
.map_err(Error::ParseMemoryZone)?
|
||||
@@ -1183,6 +1195,7 @@ impl MemoryConfig {
|
||||
hotplug_size,
|
||||
hotplugged_size,
|
||||
prefault,
|
||||
reserve,
|
||||
mergeable,
|
||||
});
|
||||
}
|
||||
@@ -1201,6 +1214,7 @@ impl MemoryConfig {
|
||||
hugepages,
|
||||
hugepage_size,
|
||||
prefault,
|
||||
reserve,
|
||||
zones,
|
||||
thp,
|
||||
})
|
||||
@@ -3974,6 +3988,20 @@ mod unit_tests {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
// reserve=on on a zone
|
||||
assert_eq!(
|
||||
MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,reserve=on"]))?,
|
||||
MemoryConfig {
|
||||
size: 0,
|
||||
zones: Some(vec![MemoryZoneConfig {
|
||||
id: "mem0".to_string(),
|
||||
size: 1 << 30,
|
||||
reserve: true,
|
||||
..Default::default()
|
||||
}]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4038,6 +4066,16 @@ mod unit_tests {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
// reserve=on opts out of MAP_NORESERVE
|
||||
assert_eq!(
|
||||
MemoryConfig::parse("size=1G,hugepages=on,reserve=on", None)?,
|
||||
MemoryConfig {
|
||||
size: 1 << 30,
|
||||
hugepages: true,
|
||||
reserve: true,
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5227,6 +5265,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
|
||||
hugepages: false,
|
||||
hugepage_size: None,
|
||||
prefault: false,
|
||||
reserve: false,
|
||||
zones: None,
|
||||
thp: true,
|
||||
},
|
||||
|
||||
@@ -5387,6 +5387,7 @@ impl IvshmemOps for IvshmemHandler {
|
||||
0,
|
||||
size,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -2731,6 +2731,7 @@ mod unit_tests {
|
||||
hugepages: false,
|
||||
hugepage_size: None,
|
||||
prefault: false,
|
||||
reserve: false,
|
||||
zones: None,
|
||||
thp: true,
|
||||
},
|
||||
|
||||
@@ -278,6 +278,7 @@ pub struct MemoryManager {
|
||||
hugepages: bool,
|
||||
hugepage_size: Option<u64>,
|
||||
prefault: bool,
|
||||
reserve: bool,
|
||||
thp: bool,
|
||||
user_provided_zones: bool,
|
||||
snapshot_memory_ranges: MemoryRangeTable,
|
||||
@@ -727,6 +728,7 @@ impl MemoryManager {
|
||||
region_start,
|
||||
region_size as usize,
|
||||
prefault.unwrap_or(zone.prefault),
|
||||
zone.reserve,
|
||||
zone.shared,
|
||||
zone.hugepages,
|
||||
zone.hugepage_size,
|
||||
@@ -829,6 +831,7 @@ impl MemoryManager {
|
||||
GuestAddress(guest_ram_mapping.gpa),
|
||||
guest_ram_mapping.size as usize,
|
||||
prefault.unwrap_or(zone_config.prefault),
|
||||
zone_config.reserve,
|
||||
zone_config.shared,
|
||||
zone_config.hugepages,
|
||||
zone_config.hugepage_size,
|
||||
@@ -1528,6 +1531,7 @@ impl MemoryManager {
|
||||
hotplug_size: config.hotplug_size,
|
||||
hotplugged_size: config.hotplugged_size,
|
||||
prefault: config.prefault,
|
||||
reserve: config.reserve,
|
||||
mergeable: config.mergeable,
|
||||
}];
|
||||
|
||||
@@ -1762,6 +1766,7 @@ impl MemoryManager {
|
||||
start_addr,
|
||||
hotplug_size as usize,
|
||||
prefault.unwrap_or(zone.prefault),
|
||||
zone.reserve,
|
||||
zone.shared,
|
||||
zone.hugepages,
|
||||
zone.hugepage_size,
|
||||
@@ -1875,6 +1880,7 @@ impl MemoryManager {
|
||||
hugepages: config.hugepages,
|
||||
hugepage_size: config.hugepage_size,
|
||||
prefault: config.prefault,
|
||||
reserve: config.reserve,
|
||||
user_provided_zones,
|
||||
snapshot_memory_ranges: MemoryRangeTable::default(),
|
||||
memory_zones,
|
||||
@@ -2042,6 +2048,7 @@ impl MemoryManager {
|
||||
file_offset: u64,
|
||||
size: usize,
|
||||
prefault: bool,
|
||||
reserve: bool,
|
||||
shared: bool,
|
||||
hugepages: bool,
|
||||
hugepage_size: Option<u64>,
|
||||
@@ -2049,7 +2056,7 @@ impl MemoryManager {
|
||||
existing_memory_file: Option<File>,
|
||||
thp: bool,
|
||||
) -> Result<MmapRegion<AtomicBitmap>, Error> {
|
||||
let mut mmap_flags = libc::MAP_NORESERVE;
|
||||
let mut mmap_flags = if reserve { 0 } else { libc::MAP_NORESERVE };
|
||||
|
||||
// The duplication of mmap_flags ORing here is unfortunate but it also makes
|
||||
// the complexity of the handling clear.
|
||||
@@ -2179,6 +2186,7 @@ impl MemoryManager {
|
||||
start_addr: GuestAddress,
|
||||
size: usize,
|
||||
prefault: bool,
|
||||
reserve: bool,
|
||||
shared: bool,
|
||||
hugepages: bool,
|
||||
hugepage_size: Option<u64>,
|
||||
@@ -2191,6 +2199,7 @@ impl MemoryManager {
|
||||
file_offset,
|
||||
size,
|
||||
prefault,
|
||||
reserve,
|
||||
shared,
|
||||
hugepages,
|
||||
hugepage_size,
|
||||
@@ -2306,6 +2315,7 @@ impl MemoryManager {
|
||||
start_addr,
|
||||
size,
|
||||
self.prefault,
|
||||
self.reserve,
|
||||
self.shared,
|
||||
self.hugepages,
|
||||
self.hugepage_size,
|
||||
|
||||
@@ -245,6 +245,8 @@ pub struct MemoryZoneConfig {
|
||||
#[serde(default)]
|
||||
pub prefault: bool,
|
||||
#[serde(default)]
|
||||
pub reserve: bool,
|
||||
#[serde(default)]
|
||||
pub mergeable: bool,
|
||||
}
|
||||
|
||||
@@ -293,6 +295,8 @@ pub struct MemoryConfig {
|
||||
#[serde(default)]
|
||||
pub prefault: bool,
|
||||
#[serde(default)]
|
||||
pub reserve: bool,
|
||||
#[serde(default)]
|
||||
pub zones: Option<Vec<MemoryZoneConfig>>,
|
||||
#[serde(default = "default_memoryconfig_thp")]
|
||||
pub thp: bool,
|
||||
@@ -312,6 +316,7 @@ impl Default for MemoryConfig {
|
||||
hugepages: false,
|
||||
hugepage_size: None,
|
||||
prefault: false,
|
||||
reserve: false,
|
||||
zones: None,
|
||||
thp: true,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user