// Copyright (c) 2018 Levente Kurusa // Copyright (c) 2020 Ant Group // // SPDX-License-Identifier: Apache-2.0 or MIT // //! This module contains the implementation of the `memory` cgroup subsystem. //! //! See the Kernel's documentation for more information about this subsystem, found at: //! [Documentation/cgroup-v1/memory.txt](https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt) use log::warn; use std::collections::HashMap; use std::io::Write; use std::path::PathBuf; use std::sync::mpsc::Receiver; use crate::error::ErrorKind::*; use crate::error::*; use crate::events; use crate::{read_i64_from, read_string_from, read_u64_from}; use crate::flat_keyed_to_hashmap; use crate::{ ControllIdentifier, ControllerInternal, Controllers, CustomizedAttribute, MaxValue, MemoryResources, Resources, Subsystem, }; /// A controller that allows controlling the `memory` subsystem of a Cgroup. /// /// In essence, using the memory controller, the user can gather statistics about the memory usage /// of the tasks in the control group. Additonally, one can also set powerful limits on their /// memory usage. #[derive(Debug, Clone)] pub struct MemController { base: PathBuf, path: PathBuf, v2: bool, } #[derive(Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SetMemory { pub low: Option, pub high: Option, pub min: Option, pub max: Option, } /// Controls statistics and controls about the OOM killer operating in this control group. #[derive(Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct OomControl { /// If true, the OOM killer has been disabled for the tasks in this control group. pub oom_kill_disable: bool, /// Is the OOM killer currently running for the tasks in the control group? pub under_oom: bool, /// How many tasks were killed by the OOM killer so far. pub oom_kill: u64, } #[allow(clippy::unnecessary_wraps)] fn parse_oom_control(s: String) -> Result { let spl = s.split_whitespace().collect::>(); let oom_kill_disable = if spl.len() > 1 { spl[1].parse::().unwrap() == 1 } else { false }; let under_oom = if spl.len() > 3 { spl[3].parse::().unwrap() == 1 } else { false }; let oom_kill = if spl.len() > 5 { spl[5].parse::().unwrap() } else { 0 }; Ok(OomControl { oom_kill_disable, under_oom, oom_kill, }) } /// Contains statistics about the NUMA locality of the control group's tasks. #[derive(Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NumaStat { /// Total amount of pages used by the control group. pub total_pages: u64, /// Total amount of pages used by the control group, broken down by NUMA node. pub total_pages_per_node: Vec, /// Total amount of file pages used by the control group. pub file_pages: u64, /// Total amount of file pages used by the control group, broken down by NUMA node. pub file_pages_per_node: Vec, /// Total amount of anonymous pages used by the control group. pub anon_pages: u64, /// Total amount of anonymous pages used by the control group, broken down by NUMA node. pub anon_pages_per_node: Vec, /// Total amount of unevictable pages used by the control group. pub unevictable_pages: u64, /// Total amount of unevictable pages used by the control group, broken down by NUMA node. pub unevictable_pages_per_node: Vec, /// Same as `total_pages`, but includes the descedant control groups' number as well. pub hierarchical_total_pages: u64, /// Same as `total_pages_per_node`, but includes the descedant control groups' number as well. pub hierarchical_total_pages_per_node: Vec, /// Same as `file_pages`, but includes the descedant control groups' number as well. pub hierarchical_file_pages: u64, /// Same as `file_pages_per_node`, but includes the descedant control groups' number as well. pub hierarchical_file_pages_per_node: Vec, /// Same as `anon_pages`, but includes the descedant control groups' number as well. pub hierarchical_anon_pages: u64, /// Same as `anon_pages_per_node`, but includes the descedant control groups' number as well. pub hierarchical_anon_pages_per_node: Vec, /// Same as `unevictable`, but includes the descedant control groups' number as well. pub hierarchical_unevictable_pages: u64, /// Same as `unevictable_per_node`, but includes the descedant control groups' number as well. pub hierarchical_unevictable_pages_per_node: Vec, } #[allow(clippy::unnecessary_wraps)] fn parse_numa_stat(s: String) -> Result { // Parse the number of nodes let _nodes = (s.split_whitespace().count() - 8) / 8; let mut ls = s.lines(); let total_line = ls.next().unwrap(); let file_line = ls.next().unwrap(); let anon_line = ls.next().unwrap(); let unevict_line = ls.next().unwrap(); let hier_total_line = ls.next().unwrap_or_default(); let hier_file_line = ls.next().unwrap_or_default(); let hier_anon_line = ls.next().unwrap_or_default(); let hier_unevict_line = ls.next().unwrap_or_default(); Ok(NumaStat { total_pages: total_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0), total_pages_per_node: { let spl = &total_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() }, file_pages: file_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0), file_pages_per_node: { let spl = &file_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() }, anon_pages: anon_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0), anon_pages_per_node: { let spl = &anon_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() }, unevictable_pages: unevict_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0), unevictable_pages_per_node: { let spl = &unevict_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() }, hierarchical_total_pages: { if !hier_total_line.is_empty() { hier_total_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0) } else { 0 } }, hierarchical_total_pages_per_node: { if !hier_total_line.is_empty() { let spl = &hier_total_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() } else { Vec::new() } }, hierarchical_file_pages: { if !hier_file_line.is_empty() { hier_file_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0) } else { 0 } }, hierarchical_file_pages_per_node: { if !hier_file_line.is_empty() { let spl = &hier_file_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() } else { Vec::new() } }, hierarchical_anon_pages: { if !hier_anon_line.is_empty() { hier_anon_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0) } else { 0 } }, hierarchical_anon_pages_per_node: { if !hier_anon_line.is_empty() { let spl = &hier_anon_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() } else { Vec::new() } }, hierarchical_unevictable_pages: { if !hier_unevict_line.is_empty() { hier_unevict_line .split(|x| x == ' ' || x == '=') .collect::>()[1] .parse::() .unwrap_or(0) } else { 0 } }, hierarchical_unevictable_pages_per_node: { if !hier_unevict_line.is_empty() { let spl = &hier_unevict_line.split(' ').collect::>()[1..]; spl.iter() .map(|x| { x.split('=').collect::>()[1] .parse::() .unwrap_or(0) }) .collect() } else { Vec::new() } }, }) } #[derive(Default, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MemoryStat { pub cache: u64, pub rss: u64, pub rss_huge: u64, pub shmem: u64, pub mapped_file: u64, pub dirty: u64, pub writeback: u64, pub swap: u64, pub pgpgin: u64, pub pgpgout: u64, pub pgfault: u64, pub pgmajfault: u64, pub inactive_anon: u64, pub active_anon: u64, pub inactive_file: u64, pub active_file: u64, pub unevictable: u64, pub hierarchical_memory_limit: i64, pub hierarchical_memsw_limit: i64, pub total_cache: u64, pub total_rss: u64, pub total_rss_huge: u64, pub total_shmem: u64, pub total_mapped_file: u64, pub total_dirty: u64, pub total_writeback: u64, pub total_swap: u64, pub total_pgpgin: u64, pub total_pgpgout: u64, pub total_pgfault: u64, pub total_pgmajfault: u64, pub total_inactive_anon: u64, pub total_active_anon: u64, pub total_inactive_file: u64, pub total_active_file: u64, pub total_unevictable: u64, pub raw: HashMap, } #[allow(clippy::unnecessary_wraps)] fn parse_memory_stat(s: String) -> Result { let mut raw = HashMap::new(); for l in s.lines() { let t: Vec<&str> = l.split(' ').collect(); if t.len() != 2 { continue; } let n = t[1].trim().parse::(); if n.is_err() { continue; } raw.insert(t[0].to_string(), n.unwrap()); } Ok(MemoryStat { cache: *raw.get("cache").unwrap_or(&0), rss: *raw.get("rss").unwrap_or(&0), rss_huge: *raw.get("rss_huge").unwrap_or(&0), shmem: *raw.get("shmem").unwrap_or(&0), mapped_file: *raw.get("mapped_file").unwrap_or(&0), dirty: *raw.get("dirty").unwrap_or(&0), writeback: *raw.get("writeback").unwrap_or(&0), swap: *raw.get("swap").unwrap_or(&0), pgpgin: *raw.get("pgpgin").unwrap_or(&0), pgpgout: *raw.get("pgpgout").unwrap_or(&0), pgfault: *raw.get("pgfault").unwrap_or(&0), pgmajfault: *raw.get("pgmajfault").unwrap_or(&0), inactive_anon: *raw.get("inactive_anon").unwrap_or(&0), active_anon: *raw.get("active_anon").unwrap_or(&0), inactive_file: *raw.get("inactive_file").unwrap_or(&0), active_file: *raw.get("active_file").unwrap_or(&0), unevictable: *raw.get("unevictable").unwrap_or(&0), hierarchical_memory_limit: *raw.get("hierarchical_memory_limit").unwrap_or(&0) as i64, hierarchical_memsw_limit: *raw.get("hierarchical_memsw_limit").unwrap_or(&0) as i64, total_cache: *raw.get("total_cache").unwrap_or(&0), total_rss: *raw.get("total_rss").unwrap_or(&0), total_rss_huge: *raw.get("total_rss_huge").unwrap_or(&0), total_shmem: *raw.get("total_shmem").unwrap_or(&0), total_mapped_file: *raw.get("total_mapped_file").unwrap_or(&0), total_dirty: *raw.get("total_dirty").unwrap_or(&0), total_writeback: *raw.get("total_writeback").unwrap_or(&0), total_swap: *raw.get("total_swap").unwrap_or(&0), total_pgpgin: *raw.get("total_pgpgin").unwrap_or(&0), total_pgpgout: *raw.get("total_pgpgout").unwrap_or(&0), total_pgfault: *raw.get("total_pgfault").unwrap_or(&0), total_pgmajfault: *raw.get("total_pgmajfault").unwrap_or(&0), total_inactive_anon: *raw.get("total_inactive_anon").unwrap_or(&0), total_active_anon: *raw.get("total_active_anon").unwrap_or(&0), total_inactive_file: *raw.get("total_inactive_file").unwrap_or(&0), total_active_file: *raw.get("total_active_file").unwrap_or(&0), total_unevictable: *raw.get("total_unevictable").unwrap_or(&0), raw, }) } /// Contains statistics about the current usage of memory and swap (together, not seperately) by /// the control group's tasks. #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MemSwap { /// How many times the limit has been hit. pub fail_cnt: u64, /// Memory and swap usage limit in bytes. pub limit_in_bytes: i64, /// Current usage of memory and swap in bytes. pub usage_in_bytes: u64, /// The maximum observed usage of memory and swap in bytes. pub max_usage_in_bytes: u64, } /// State of and statistics gathered by the kernel about the memory usage of the control group's /// tasks. #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Memory { /// How many times the limit has been hit. pub fail_cnt: u64, /// The limit in bytes of the memory usage of the control group's tasks. pub limit_in_bytes: i64, /// The current usage of memory by the control group's tasks. pub usage_in_bytes: u64, /// The maximum observed usage of memory by the control group's tasks. pub max_usage_in_bytes: u64, /// Whether moving charges at immigrate is allowed. pub move_charge_at_immigrate: u64, /// Contains various statistics about the NUMA locality of the control group's tasks. /// /// The format of this field (as lifted from the kernel sources): /// ```text /// total= N0= N1= ... /// file= N0= N1= ... /// anon= N0= N1= ... /// unevictable= N0= N1= ... /// hierarchical_= N0= N1= ... /// ``` pub numa_stat: NumaStat, /// Various statistics and control information about the Out Of Memory killer. pub oom_control: OomControl, /// Allows setting a limit to memory usage which is enforced when the system (note, _not_ the /// control group) detects memory pressure. pub soft_limit_in_bytes: i64, /// Contains a wide array of statistics about the memory usage of the tasks in the control /// group. pub stat: MemoryStat, /// Set the tendency of the kernel to swap out parts of the address space consumed by the /// control group's tasks. /// /// Note that setting this to zero does *not* prevent swapping, use `mlock(2)` for that /// purpose. pub swappiness: u64, /// If set, then under OOM conditions, the kernel will try to reclaim memory from the children /// of the offending process too. By default, this is not allowed. pub use_hierarchy: u64, } /// The current state of and gathered statistics about the kernel's memory usage for TCP-related /// data structures. #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Tcp { /// How many times the limit has been hit. pub fail_cnt: u64, /// The limit in bytes of the memory usage of the kernel's TCP buffers by control group's /// tasks. pub limit_in_bytes: i64, /// The current memory used by the kernel's TCP buffers related to these tasks. pub usage_in_bytes: u64, /// The observed maximum usage of memory by the kernel's TCP buffers (that originated from /// these tasks). pub max_usage_in_bytes: u64, } /// Gathered statistics and the current state of limitation of the kernel's memory usage. Note that /// this is per-cgroup, so the kernel can of course use more memory, but it will fail operations by /// these tasks if it would think that the limits here would be violated. It's important to note /// that interrupts in particular might not be able to enforce these limits. #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Kmem { /// How many times the limit has been hit. pub fail_cnt: u64, /// The limit in bytes of the kernel memory used by the control group's tasks. pub limit_in_bytes: i64, /// The current usage of kernel memory used by the control group's tasks, in bytes. pub usage_in_bytes: u64, /// The maximum observed usage of kernel memory used by the control group's tasks, in bytes. pub max_usage_in_bytes: u64, /// Contains information about the memory usage of the kernel's caches, per control group. pub slabinfo: String, } impl ControllerInternal for MemController { fn control_type(&self) -> Controllers { Controllers::Mem } fn get_path(&self) -> &PathBuf { &self.path } fn get_path_mut(&mut self) -> &mut PathBuf { &mut self.path } fn get_base(&self) -> &PathBuf { &self.base } fn is_v2(&self) -> bool { self.v2 } fn apply(&self, res: &Resources) -> Result<()> { // get the resources that apply to this controller let memres: &MemoryResources = &res.memory; update!(self, set_limit, memres.memory_hard_limit); update!(self, set_soft_limit, memres.memory_soft_limit); update!(self, set_kmem_limit, memres.kernel_memory_limit); update!(self, set_memswap_limit, memres.memory_swap_limit); update!(self, set_tcp_limit, memres.kernel_tcp_memory_limit); update!(self, set_swappiness, memres.swappiness); memres.attrs.iter().for_each(|(k, v)| { let _ = self.set(k, v); }); Ok(()) } } impl MemController { /// Contructs a new `MemController` with `root` serving as the root of the control group. pub fn new(point: PathBuf, root: PathBuf, v2: bool) -> Self { Self { base: root, path: point, v2, } } // for v2 pub fn set_mem(&self, m: SetMemory) -> Result<()> { let values = vec![ (m.high, "memory.high"), (m.low, "memory.low"), (m.max, "memory.max"), (m.min, "memory.min"), ]; for value in values { let v = value.0; let f = value.1; if let Some(v) = v { let v = v.to_string(); self.open_path(f, true).and_then(|mut file| { file.write_all(v.as_ref()).map_err(|e| { Error::with_cause(WriteFailed(f.to_string(), format!("{:?}", v)), e) }) })?; } } Ok(()) } // for v2 pub fn get_mem(&self) -> Result { let m = SetMemory { high: self .get_max_value("memory.high") .map_or(Some(MaxValue::default()), Some), low: self .get_max_value("memory.low") .map_or(Some(MaxValue::Value(0)), Some), max: self .get_max_value("memory.max") .map_or(Some(MaxValue::default()), Some), min: self .get_max_value("memory.min") .map_or(Some(MaxValue::Value(0)), Some), }; Ok(m) } fn memory_stat_v2(&self) -> Memory { // NOTE: get_mem() always returns T, but let's // still do `unwrap_or` for safety. let set = self.get_mem().unwrap_or(SetMemory { low: Some(MaxValue::Value(0)), high: Some(MaxValue::default()), max: Some(MaxValue::default()), min: Some(MaxValue::Value(0)), }); Memory { fail_cnt: 0, limit_in_bytes: set.max.unwrap().to_i64(), usage_in_bytes: self .open_path("memory.current", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.peak", false) .and_then(read_u64_from) .unwrap_or(0), move_charge_at_immigrate: 0, numa_stat: NumaStat::default(), oom_control: OomControl::default(), soft_limit_in_bytes: set.low.unwrap().to_i64(), stat: self .open_path("memory.stat", false) .and_then(read_string_from) .and_then(parse_memory_stat) .unwrap_or_default(), swappiness: self .open_path("memory.swap.current", false) .and_then(read_u64_from) .unwrap_or(0), use_hierarchy: 0, } } /// Gathers overall statistics (and the current state of) about the memory usage of the control /// group's tasks. /// /// See the individual fields for more explanation, and as always, remember to consult the /// kernel Documentation and/or sources. pub fn memory_stat(&self) -> Memory { if self.v2 { return self.memory_stat_v2(); } Memory { fail_cnt: self .open_path("memory.failcnt", false) .and_then(read_u64_from) .unwrap_or(0), limit_in_bytes: self .open_path("memory.limit_in_bytes", false) .and_then(read_i64_from) .unwrap_or(0), usage_in_bytes: self .open_path("memory.usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.max_usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), move_charge_at_immigrate: self .open_path("memory.move_charge_at_immigrate", false) .and_then(read_u64_from) .unwrap_or(0), numa_stat: self .open_path("memory.numa_stat", false) .and_then(read_string_from) .and_then(parse_numa_stat) .unwrap_or_default(), oom_control: self .open_path("memory.oom_control", false) .and_then(read_string_from) .and_then(parse_oom_control) .unwrap_or_default(), soft_limit_in_bytes: self .open_path("memory.soft_limit_in_bytes", false) .and_then(read_i64_from) .unwrap_or(0), stat: self .open_path("memory.stat", false) .and_then(read_string_from) .and_then(parse_memory_stat) .unwrap_or_default(), swappiness: self .open_path("memory.swappiness", false) .and_then(read_u64_from) .unwrap_or(0), use_hierarchy: self .open_path("memory.use_hierarchy", false) .and_then(read_u64_from) .unwrap_or(0), } } /// Gathers information about the kernel memory usage of the control group's tasks. pub fn kmem_stat(&self) -> Kmem { Kmem { fail_cnt: self .open_path("memory.kmem.failcnt", false) .and_then(read_u64_from) .unwrap_or(0), limit_in_bytes: self .open_path("memory.kmem.limit_in_bytes", false) .and_then(read_i64_from) .unwrap_or(-1), usage_in_bytes: self .open_path("memory.kmem.usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.kmem.max_usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), slabinfo: self .open_path("memory.kmem.slabinfo", false) .and_then(read_string_from) .unwrap_or_default(), } } /// Gathers information about the control group's kernel memory usage where said memory is /// TCP-related. pub fn kmem_tcp_stat(&self) -> Tcp { Tcp { fail_cnt: self .open_path("memory.kmem.tcp.failcnt", false) .and_then(read_u64_from) .unwrap_or(0), limit_in_bytes: self .open_path("memory.kmem.tcp.limit_in_bytes", false) .and_then(read_i64_from) .unwrap_or(0), usage_in_bytes: self .open_path("memory.kmem.tcp.usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.kmem.tcp.max_usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), } } pub fn memswap_v2(&self) -> MemSwap { MemSwap { fail_cnt: self .open_path("memory.swap.events", false) .and_then(flat_keyed_to_hashmap) .map(|x| *x.get("fail").unwrap_or(&0) as u64) .unwrap_or(0), limit_in_bytes: self .open_path("memory.swap.max", false) .and_then(read_i64_from) .unwrap_or(0), usage_in_bytes: self .open_path("memory.swap.current", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.swap.peak", false) .and_then(read_u64_from) .unwrap_or(0), } } /// Gathers information about the memory usage of the control group including the swap usage /// (if any). pub fn memswap(&self) -> MemSwap { if self.v2 { return self.memswap_v2(); } MemSwap { fail_cnt: self .open_path("memory.memsw.failcnt", false) .and_then(read_u64_from) .unwrap_or(0), limit_in_bytes: self .open_path("memory.memsw.limit_in_bytes", false) .and_then(read_i64_from) .unwrap_or(0), usage_in_bytes: self .open_path("memory.memsw.usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), max_usage_in_bytes: self .open_path("memory.memsw.max_usage_in_bytes", false) .and_then(read_u64_from) .unwrap_or(0), } } /// Reset the fail counter pub fn reset_fail_count(&self) -> Result<()> { self.open_path("memory.failcnt", true).and_then(|mut file| { file.write_all("0".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.failcnt".to_string(), "0".to_string()), e, ) }) }) } /// Reset the kernel memory fail counter pub fn reset_kmem_fail_count(&self) -> Result<()> { // Ignore kmem because there is no kmem in cgroup v2 if self.v2 { return Ok(()); } self.open_path("memory.kmem.failcnt", true) .and_then(|mut file| { file.write_all("0".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.kmem.failcnt".to_string(), "0".to_string()), e, ) }) }) } /// Reset the TCP related fail counter pub fn reset_tcp_fail_count(&self) -> Result<()> { // Ignore kmem because there is no kmem in cgroup v2 if self.v2 { return Ok(()); } self.open_path("memory.kmem.tcp.failcnt", true) .and_then(|mut file| { file.write_all("0".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.kmem.tcp.failcnt".to_string(), "0".to_string()), e, ) }) }) } /// Reset the memory+swap fail counter pub fn reset_memswap_fail_count(&self) -> Result<()> { self.open_path("memory.memsw.failcnt", true) .and_then(|mut file| { file.write_all("0".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.memsw.failcnt".to_string(), "0".to_string()), e, ) }) }) } /// Reset the max memory usage recorded pub fn reset_max_usage(&self) -> Result<()> { self.open_path("memory.max_usage_in_bytes", true) .and_then(|mut file| { file.write_all("0".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.max_usage_in_bytes".to_string(), "0".to_string()), e, ) }) }) } /// Set the memory usage limit of the control group, in bytes. pub fn set_limit(&self, limit: i64) -> Result<()> { let mut file_name = "memory.limit_in_bytes"; let mut limit_str = limit.to_string(); if self.v2 { file_name = "memory.max"; if limit == -1 { limit_str = "max".to_string(); } } self.open_path(file_name, true).and_then(|mut file| { file.write_all(limit_str.as_ref()) .map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e)) }) } /// Set the kernel memory limit of the control group, in bytes. pub fn set_kmem_limit(&self, limit: i64) -> Result<()> { // Ignore kmem because there is no kmem in cgroup v2 if self.v2 { return Ok(()); } self.open_path("memory.kmem.limit_in_bytes", true) .and_then(|mut file| { let r = file.write_all(limit.to_string().as_ref()); match r { Ok(()) => Ok(()), Err(ref e) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => { warn!("memory.kmem.limit_in_bytes is unsupported by the kernel"); Ok(()) } Err(e) => Err(Error::with_cause( WriteFailed("memory.kmem.limit_in_bytes".to_string(), limit.to_string()), e, )), } }) } /// Set the memory+swap limit of the control group, in bytes. pub fn set_memswap_limit(&self, limit: i64) -> Result<()> { let mut file_name = "memory.memsw.limit_in_bytes"; let mut limit_str = limit.to_string(); if self.v2 { file_name = "memory.swap.max"; if limit == -1 { limit_str = "max".to_string(); } } self.open_path(file_name, true).and_then(|mut file| { file.write_all(limit_str.as_ref()) .map_err(|e| Error::with_cause(WriteFailed(file_name.to_string(), limit_str), e)) }) } /// Set how much kernel memory can be used for TCP-related buffers by the control group. pub fn set_tcp_limit(&self, limit: i64) -> Result<()> { // Ignore kmem because there is no kmem in cgroup v2 if self.v2 { return Ok(()); } self.open_path("memory.kmem.tcp.limit_in_bytes", true) .and_then(|mut file| { file.write_all(limit.to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed( "memory.kmem.tcp.limit_in_bytes".to_string(), limit.to_string(), ), e, ) }) }) } /// Set the soft limit of the control group, in bytes. /// /// This limit is enforced when the system is nearing OOM conditions. Contrast this with the /// hard limit, which is _always_ enforced. pub fn set_soft_limit(&self, limit: i64) -> Result<()> { let mut file_name = "memory.soft_limit_in_bytes"; if self.v2 { file_name = "memory.low" } self.open_path(file_name, true).and_then(|mut file| { file.write_all(limit.to_string().as_ref()).map_err(|e| { Error::with_cause(WriteFailed(file_name.to_string(), limit.to_string()), e) }) }) } /// Set how likely the kernel is to swap out parts of the address space used by the control /// group. /// /// Note that a value of zero does not imply that the process will not be swapped out. pub fn set_swappiness(&self, swp: u64) -> Result<()> { let mut file_name = "memory.swappiness"; if self.v2 { file_name = "memory.swap.max" } self.open_path(file_name, true).and_then(|mut file| { file.write_all(swp.to_string().as_ref()).map_err(|e| { Error::with_cause(WriteFailed(file_name.to_string(), swp.to_string()), e) }) }) } pub fn disable_oom_killer(&self) -> Result<()> { self.open_path("memory.oom_control", true) .and_then(|mut file| { file.write_all("1".to_string().as_ref()).map_err(|e| { Error::with_cause( WriteFailed("memory.oom_control".to_string(), "1".to_string()), e, ) }) }) } pub fn register_oom_event(&self, key: &str) -> Result> { if self.v2 { events::notify_on_oom_v2(key, self.get_path()) } else { events::notify_on_oom_v1(key, self.get_path()) } } } impl ControllIdentifier for MemController { fn controller_type() -> Controllers { Controllers::Mem } } impl CustomizedAttribute for MemController {} impl<'a> From<&'a Subsystem> for &'a MemController { fn from(sub: &'a Subsystem) -> &'a MemController { unsafe { match sub { Subsystem::Mem(c) => c, _ => { assert_eq!(1, 0); let v = std::mem::MaybeUninit::uninit(); v.assume_init() } } } } } #[cfg(test)] mod tests { use crate::memory::{ parse_memory_stat, parse_numa_stat, parse_oom_control, MemoryStat, NumaStat, OomControl, }; static GOOD_VALUE: &str = "\ total=51189 N0=51189 N1=123 file=50175 N0=50175 N1=123 anon=1014 N0=1014 N1=123 unevictable=0 N0=0 N1=123 hierarchical_total=1628573 N0=1628573 N1=123 hierarchical_file=858151 N0=858151 N1=123 hierarchical_anon=770402 N0=770402 N1=123 hierarchical_unevictable=20 N0=20 N1=123 "; static GOOD_VALUE_NON_HIERARCHICAL: &str = "\ total=51189 N0=51189 N1=123 file=50175 N0=50175 N1=123 anon=1014 N0=1014 N1=123 unevictable=0 N0=0 N1=123 "; static GOOD_OOMCONTROL_VAL_1: &str = "\ oom_kill_disable 0 oom_kill 1337 "; static GOOD_OOMCONTROL_VAL_2: &str = "\ oom_kill_disable 0 under_oom 1 "; static GOOD_OOMCONTROL_VAL_3: &str = "\ oom_kill_disable 0 under_oom 1 oom_kill 1337 "; static GOOD_MEMORYSTAT_VAL: &str = "\ cache 178880512 rss 4206592 rss_huge 0 shmem 106496 mapped_file 7491584 dirty 114688 writeback 49152 swap 0 pgpgin 213928 pgpgout 169220 pgfault 87064 pgmajfault 202 inactive_anon 0 active_anon 4153344 inactive_file 84779008 active_file 94273536 unevictable 0 hierarchical_memory_limit 9223372036854771712 hierarchical_memsw_limit 9223372036854771712 total_cache 4200333312 total_rss 2927677440 total_rss_huge 0 total_shmem 590061568 total_mapped_file 1086164992 total_dirty 1769472 total_writeback 602112 total_swap 0 total_pgpgin 5267326291 total_pgpgout 5265586647 total_pgfault 9947902469 total_pgmajfault 25132 total_inactive_anon 585981952 total_active_anon 2928996352 total_inactive_file 1272135680 total_active_file 2338816000 total_unevictable 81920 "; #[test] fn test_parse_numa_stat() { let ok = parse_numa_stat(GOOD_VALUE.to_string()).unwrap(); assert_eq!( ok, NumaStat { total_pages: 51189, total_pages_per_node: vec![51189, 123], file_pages: 50175, file_pages_per_node: vec![50175, 123], anon_pages: 1014, anon_pages_per_node: vec![1014, 123], unevictable_pages: 0, unevictable_pages_per_node: vec![0, 123], hierarchical_total_pages: 1628573, hierarchical_total_pages_per_node: vec![1628573, 123], hierarchical_file_pages: 858151, hierarchical_file_pages_per_node: vec![858151, 123], hierarchical_anon_pages: 770402, hierarchical_anon_pages_per_node: vec![770402, 123], hierarchical_unevictable_pages: 20, hierarchical_unevictable_pages_per_node: vec![20, 123], } ); let ok = parse_numa_stat(GOOD_VALUE_NON_HIERARCHICAL.to_string()).unwrap(); assert_eq!( ok, NumaStat { total_pages: 51189, total_pages_per_node: vec![51189, 123], file_pages: 50175, file_pages_per_node: vec![50175, 123], anon_pages: 1014, anon_pages_per_node: vec![1014, 123], unevictable_pages: 0, unevictable_pages_per_node: vec![0, 123], hierarchical_total_pages: 0, hierarchical_total_pages_per_node: vec![], hierarchical_file_pages: 0, hierarchical_file_pages_per_node: vec![], hierarchical_anon_pages: 0, hierarchical_anon_pages_per_node: vec![], hierarchical_unevictable_pages: 0, hierarchical_unevictable_pages_per_node: vec![], } ); } #[test] fn test_parse_oom_control() { let ok = parse_oom_control("".to_string()).unwrap(); assert_eq!( ok, OomControl { oom_kill_disable: false, under_oom: false, oom_kill: 0, } ); let ok = parse_oom_control(GOOD_OOMCONTROL_VAL_1.to_string()).unwrap(); assert_eq!( ok, OomControl { oom_kill_disable: false, under_oom: false, oom_kill: 0, } ); let ok = parse_oom_control(GOOD_OOMCONTROL_VAL_2.to_string()).unwrap(); assert_eq!( ok, OomControl { oom_kill_disable: false, under_oom: true, oom_kill: 0, } ); let ok = parse_oom_control(GOOD_OOMCONTROL_VAL_3.to_string()).unwrap(); assert_eq!( ok, OomControl { oom_kill_disable: false, under_oom: true, oom_kill: 1337, } ); } #[test] fn test_parse_memory_stat() { let ok = parse_memory_stat(GOOD_MEMORYSTAT_VAL.to_string()).unwrap(); let raw = ok.raw.clone(); assert_eq!( ok, MemoryStat { cache: 178880512, rss: 4206592, rss_huge: 0, shmem: 106496, mapped_file: 7491584, dirty: 114688, writeback: 49152, swap: 0, pgpgin: 213928, pgpgout: 169220, pgfault: 87064, pgmajfault: 202, inactive_anon: 0, active_anon: 4153344, inactive_file: 84779008, active_file: 94273536, unevictable: 0, hierarchical_memory_limit: 9223372036854771712, hierarchical_memsw_limit: 9223372036854771712, total_cache: 4200333312, total_rss: 2927677440, total_rss_huge: 0, total_shmem: 590061568, total_mapped_file: 1086164992, total_dirty: 1769472, total_writeback: 602112, total_swap: 0, total_pgpgin: 5267326291, total_pgpgout: 5265586647, total_pgfault: 9947902469, total_pgmajfault: 25132, total_inactive_anon: 585981952, total_active_anon: 2928996352, total_inactive_file: 1272135680, total_active_file: 2338816000, total_unevictable: 81920, raw, } ); } }