| // Copyright 2023 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use std::fmt::Display; | |
| use std::io; | |
| use std::os::fd::FromRawFd; | |
| use std::os::fd::OwnedFd; | |
| use std::path::Path; | |
| use std::sync::Arc; | |
| use dbus::MethodErr; | |
| use log::error; | |
| use log::info; | |
| use schedqos::cgroups::setup_cpu_cgroup; | |
| use schedqos::cgroups::setup_cpuset_cgroup; | |
| use schedqos::CgroupContext; | |
| use schedqos::Config; | |
| use schedqos::ProcessKey; | |
| use schedqos::ProcessState; | |
| use schedqos::ThreadState; | |
| use tokio::io::unix::AsyncFd; | |
| use tokio::io::Interest; | |
| use tokio::sync::Mutex; | |
| use tokio::task::JoinHandle; | |
| use crate::config::ConfigProvider; | |
| use crate::cpu_utils::Cpuset; | |
| use crate::proc::load_ruid; | |
| pub type SchedQosContext = schedqos::RestorableSchedQosContext; | |
| const STATE_FILE_PATH: &str = "/run/resourced/schedqos_states"; | |
| /// Error of parsing /proc/pid/status | |
| #[derive(Debug)] | |
| pub enum Error { | |
| ProcessForbidden, | |
| ProcessNotFound, | |
| InvalidState, | |
| SchedQoS(schedqos::Error), | |
| Pidfd(io::Error), | |
| Proc(crate::proc::Error), | |
| } | |
| impl Error { | |
| pub fn to_dbus_error(&self) -> MethodErr { | |
| match self { | |
| Self::ProcessForbidden => MethodErr::failed("process is not allowed"), | |
| Self::ProcessNotFound => MethodErr::failed("process not found"), | |
| Self::InvalidState => MethodErr::invalid_arg("invalid state"), | |
| Self::SchedQoS(e) => match e { | |
| schedqos::Error::ProcessNotRegistered => { | |
| MethodErr::failed("process not registered") | |
| } | |
| schedqos::Error::ThreadNotFound => MethodErr::failed("thread not found"), | |
| _ => MethodErr::failed("failed to set qos state"), | |
| }, | |
| Self::Pidfd(_) => MethodErr::failed("failed to create pidfd"), | |
| Self::Proc(_) => MethodErr::failed("failed to read /proc/pid/status"), | |
| } | |
| } | |
| } | |
| impl From<schedqos::Error> for Error { | |
| fn from(e: schedqos::Error) -> Self { | |
| match e { | |
| schedqos::Error::ProcessNotFound => Self::ProcessNotFound, | |
| _ => Self::SchedQoS(e), | |
| } | |
| } | |
| } | |
| impl From<crate::proc::Error> for Error { | |
| fn from(e: crate::proc::Error) -> Self { | |
| match e { | |
| crate::proc::Error::NotFound(_) => Self::ProcessNotFound, | |
| _ => Self::Proc(e), | |
| } | |
| } | |
| } | |
| impl std::error::Error for Error { | |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
| match self { | |
| Self::ProcessForbidden => None, | |
| Self::ProcessNotFound => None, | |
| Self::InvalidState => None, | |
| Self::SchedQoS(e) => Some(e), | |
| Self::Pidfd(e) => Some(e), | |
| Self::Proc(e) => Some(e), | |
| } | |
| } | |
| } | |
| impl Display for Error { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| match self { | |
| Self::ProcessForbidden => write!(f, "process forbidden"), | |
| Self::ProcessNotFound => write!(f, "process not found"), | |
| Self::InvalidState => write!(f, "invalid state"), | |
| Self::SchedQoS(e) => write!(f, "failed to set qos state: {:#}", e), | |
| Self::Pidfd(e) => write!(f, "failed to create pidfd: {:#}", e), | |
| Self::Proc(e) => write!(f, "failed to read /proc/pid/status: {:#}", e), | |
| } | |
| } | |
| } | |
| pub type Result<T> = std::result::Result<T, Error>; | |
| pub fn create_schedqos_context( | |
| root: &Path, | |
| config_provider: &ConfigProvider, | |
| ) -> anyhow::Result<SchedQosContext> { | |
| let mut normal_cpu_share = 1024; | |
| let mut background_cpu_share = 10; | |
| let mut thread_configs = Config::default_thread_config(); | |
| match config_provider.read_sched_qos_config("default") { | |
| Ok(Some(local_config)) => { | |
| if let Some(share) = local_config.normal_cpu_share { | |
| normal_cpu_share = share; | |
| } | |
| if let Some(share) = local_config.background_cpu_share { | |
| background_cpu_share = share; | |
| } | |
| local_config.merge_thread_configs_into(&mut thread_configs); | |
| } | |
| Ok(None) => { | |
| info!("no schedqos config to override."); | |
| } | |
| Err(e) => { | |
| error!("Failed to read sched qos config: {:?}", e); | |
| } | |
| } | |
| let cpu_normal = setup_cpu_cgroup("resourced/normal", normal_cpu_share)?; | |
| let cpu_background = setup_cpu_cgroup("resourced/background", background_cpu_share)?; | |
| let cpuset_all = Cpuset::all_cores(root)?; | |
| let cpuset_all = setup_cpuset_cgroup("resourced/all", &cpuset_all.to_string())?; | |
| let cpuset_efficient = Cpuset::little_cores(root)?; | |
| let cpuset_efficient = | |
| setup_cpuset_cgroup("resourced/efficient", &cpuset_efficient.to_string())?; | |
| let config = Config { | |
| cgroup_context: CgroupContext { | |
| cpu_normal, | |
| cpu_background, | |
| cpuset_all, | |
| cpuset_efficient, | |
| }, | |
| process_configs: Config::default_process_config(), | |
| thread_configs, | |
| }; | |
| let file_path = Path::new(STATE_FILE_PATH); | |
| let ctx = if file_path.exists() { | |
| info!("Loading schedqos state from {:?}", file_path); | |
| SchedQosContext::load_from_file(config, file_path)? | |
| } else { | |
| info!("Initialize schedqos state at {:?}", file_path); | |
| SchedQosContext::new_file(config, file_path)? | |
| }; | |
| Ok(ctx) | |
| } | |
| /// Validate the ruid of process_id is the same as the sender euid. | |
| /// | |
| /// Use ruid of the target process to compare because euid can be changed by the process itself | |
| /// without any capabilities. Sender should be aware of its euid when sending the QoS request. | |
| /// This can support the case a parent process forks a third-party process which change its euid and | |
| /// the parent process sends the QoS request for the third-party process.. | |
| fn validate_pid(process_id: u32, sender_euid: u32) -> Result<()> { | |
| let target_process_ruid = load_ruid(process_id)?; | |
| if target_process_ruid == sender_euid { | |
| Ok(()) | |
| } else { | |
| Err(Error::ProcessForbidden) | |
| } | |
| } | |
| pub async fn set_thread_state( | |
| sched_ctx: Arc<Mutex<SchedQosContext>>, | |
| process_id: u32, | |
| thread_id: u32, | |
| state: u8, | |
| sender_euid: u32, | |
| ) -> Result<()> { | |
| let state = ThreadState::try_from(state).map_err(|_| Error::InvalidState)?; | |
| validate_pid(process_id, sender_euid)?; | |
| let mut ctx = sched_ctx.lock().await; | |
| ctx.set_thread_state(process_id.into(), thread_id.into(), state)?; | |
| Ok(()) | |
| } | |
| /// The returned [JoinHandle] is used for testing purpose. | |
| pub async fn set_process_state( | |
| sched_ctx: Arc<Mutex<SchedQosContext>>, | |
| process_id: u32, | |
| state: u8, | |
| sender_euid: u32, | |
| ) -> Result<Option<JoinHandle<()>>> { | |
| let state = ProcessState::try_from(state).map_err(|_| Error::InvalidState)?; | |
| validate_pid(process_id, sender_euid)?; | |
| let mut ctx = sched_ctx.lock().await; | |
| if let Some(process_key) = ctx.set_process_state(process_id.into(), state)? { | |
| match create_async_pidfd(process_id) { | |
| Ok(pidfd) => Ok(Some(monitor_process(sched_ctx.clone(), pidfd, process_key))), | |
| Err(e) => { | |
| ctx.remove_process(process_key); | |
| if e.raw_os_error() == Some(libc::ESRCH) { | |
| Err(Error::ProcessNotFound) | |
| } else { | |
| Err(Error::Pidfd(e)) | |
| } | |
| } | |
| } | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| fn create_async_pidfd(pid: u32) -> std::io::Result<AsyncFd<OwnedFd>> { | |
| // SAFETY: pidfd_open(2) does not modify userspace memory. | |
| let res = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as libc::c_int; | |
| if res < 0 { | |
| return Err(io::Error::last_os_error()); | |
| } | |
| // SAFETY:: The new pidfd is not owned by anything. | |
| let pidfd = unsafe { OwnedFd::from_raw_fd(res) }; | |
| AsyncFd::with_interest(pidfd, Interest::READABLE) | |
| } | |
| fn monitor_process( | |
| sched_ctx: Arc<Mutex<SchedQosContext>>, | |
| pidfd: AsyncFd<OwnedFd>, | |
| process: ProcessKey, | |
| ) -> JoinHandle<()> { | |
| tokio::spawn(async move { | |
| match pidfd.readable().await { | |
| Ok(_guard) => {} | |
| Err(e) => { | |
| error!("pidfd readable fails: {:?}", e); | |
| } | |
| }; | |
| sched_ctx.lock().await.remove_process(process); | |
| }) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use std::time::Duration; | |
| use super::*; | |
| use crate::test_utils::*; | |
| fn create_schedqos_context_for_test() -> Arc<Mutex<SchedQosContext>> { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let file_path = dir.path().join("states"); | |
| let config = Config { | |
| cgroup_context: CgroupContext { | |
| cpu_normal: tempfile::tempfile().unwrap(), | |
| cpu_background: tempfile::tempfile().unwrap(), | |
| cpuset_all: tempfile::tempfile().unwrap(), | |
| cpuset_efficient: tempfile::tempfile().unwrap(), | |
| }, | |
| process_configs: Config::default_process_config(), | |
| thread_configs: Config::default_thread_config(), | |
| }; | |
| Arc::new(Mutex::new( | |
| SchedQosContext::new_file(config, &file_path).unwrap(), | |
| )) | |
| } | |
| #[tokio::test] | |
| async fn test_set_process_state() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| let result = set_process_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| ProcessState::Normal as u8, | |
| uid, | |
| ) | |
| .await; | |
| assert!(result.is_ok()); | |
| let result = result.unwrap(); | |
| assert!(result.is_some()); | |
| let join_handle = result.unwrap(); | |
| tokio::time::sleep(Duration::from_millis(1)).await; | |
| assert!(!join_handle.is_finished()); | |
| drop(process); | |
| // The remove_process() is executed. Otherwise this test times out. | |
| let _ = join_handle.await; | |
| } | |
| #[tokio::test] | |
| async fn test_set_process_state_invalid_state() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, _process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| let result = set_process_state(sched_ctx.clone(), process_id, 255, uid).await; | |
| assert!(matches!(result.err().unwrap(), Error::InvalidState)); | |
| } | |
| #[tokio::test] | |
| async fn test_set_process_state_invalid_pid() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| let result = set_process_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| ProcessState::Normal as u8, | |
| !uid, | |
| ) | |
| .await; | |
| assert!(matches!(result.err().unwrap(), Error::ProcessForbidden)); | |
| drop(process); | |
| let result = set_process_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| ProcessState::Normal as u8, | |
| uid, | |
| ) | |
| .await; | |
| assert!(matches!(result.err().unwrap(), Error::ProcessNotFound)); | |
| } | |
| #[tokio::test] | |
| async fn test_set_thread_state() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, _process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| set_process_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| ProcessState::Normal as u8, | |
| uid, | |
| ) | |
| .await | |
| .unwrap(); | |
| let result = set_thread_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| process_id, | |
| ThreadState::Balanced as u8, | |
| uid, | |
| ) | |
| .await; | |
| result.as_ref().unwrap(); | |
| assert!(result.is_ok()); | |
| } | |
| #[tokio::test] | |
| async fn test_set_thread_state_invalid_state() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, _process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| let result = set_thread_state(sched_ctx.clone(), process_id, process_id, 255, uid).await; | |
| assert!(matches!(result.err().unwrap(), Error::InvalidState)); | |
| } | |
| #[tokio::test] | |
| async fn test_set_thread_state_invalid_pid() { | |
| let sched_ctx = create_schedqos_context_for_test(); | |
| let (process_id, process) = fork_process_for_test(); | |
| let uid = load_ruid(process_id).unwrap(); | |
| let result = set_thread_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| process_id, | |
| ThreadState::Balanced as u8, | |
| !uid, | |
| ) | |
| .await; | |
| assert!(matches!(result.err().unwrap(), Error::ProcessForbidden)); | |
| drop(process); | |
| let result = set_thread_state( | |
| sched_ctx.clone(), | |
| process_id, | |
| process_id, | |
| ThreadState::Balanced as u8, | |
| uid, | |
| ) | |
| .await; | |
| assert!(matches!(result.err().unwrap(), Error::ProcessNotFound)); | |
| } | |
| #[tokio::test] | |
| async fn test_create_async_pidfd() { | |
| assert!(create_async_pidfd(std::process::id()).is_ok()); | |
| let (process_id, process) = fork_process_for_test(); | |
| assert!(create_async_pidfd(process_id).is_ok()); | |
| drop(process); | |
| assert_eq!( | |
| create_async_pidfd(process_id).err().unwrap().raw_os_error(), | |
| Some(libc::ESRCH) | |
| ); | |
| assert_eq!( | |
| create_async_pidfd(0).err().unwrap().raw_os_error(), | |
| Some(libc::EINVAL) | |
| ); | |
| } | |
| } |