blob: 44fa373942ea445b122b5588b1f57a00cb991aab [file]
// Copyright 2018 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
//! The state of a thread has two axes. One is its arc from birth to death, expressed via its
//! `Lifecycle` value. The other is its current exception handler state, expressed via its
//! `Exception` value.
//!
//! 1. Lifecycle: Initial -> Initialized -> Running <-> Suspended -> Dying -> Dead.
//! 2. Exception: Idle <-> Unprocessed.
//!
//! This module exists to encapsulate all legal state transitions, so generally other assertions
//! about the state of a thread prior to transitioning are not necessary.
//!
//! Only Running, Suspended, or Dying threads may participate in exception handling.
/// The lifecycle state of a thread.
///
/// The only legal transition that isn't from top-to-bottom occurs when a thread is resumed
/// after being suspended.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Lifecycle {
/// The ThreadDispatcher has been allocated, but not yet associated to a Thread or an aspace.
Initial = 0,
/// The ThreadDispatcher is now associated to its underlying Thread and the containing
/// process's address space, and is waiting to be run.
Initialized = 1,
/// The thread is running.
Running = 2,
/// The thread is currently suspended.
///
/// Note that suspension is orthogonal to being "in an exception". A thread may be both
/// suspended and in an exception, and the thread does not "resume" execution until it is
/// resumed from both the suspension and the exception.
Suspended = 3,
/// The thread is going to die. It may still be interacting with exception handling state.
Dying = 4,
/// The thread is being dissociated from all of its state, and no more interaction with
/// userspace (including exception handlers) is possible.
Dead = 5,
}
/// The exception handler state of a thread.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Exception {
/// There's no pending exception.
Idle = 0,
/// The thread is waiting for the pending exception to be processed.
Unprocessed = 1,
}
/// Internal representation of the combined lifecycle and exception state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Value {
InitialIdle,
InitializedIdle,
RunningIdle,
RunningUnprocessed,
SuspendedIdle,
SuspendedUnprocessed,
DyingIdle,
DyingUnprocessed,
DeadIdle,
}
/// Encapsulates legal thread state transitions along the Lifecycle and Exception axes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ThreadState {
value: Value,
}
impl Default for ThreadState {
fn default() -> Self {
Self::new()
}
}
impl ThreadState {
/// Creates a new `ThreadState` in the `Initial` lifecycle and `Idle` exception state.
pub const fn new() -> Self {
Self { value: Value::InitialIdle }
}
/// Returns the current lifecycle state.
pub fn lifecycle(&self) -> Lifecycle {
match self.value {
Value::InitialIdle => Lifecycle::Initial,
Value::InitializedIdle => Lifecycle::Initialized,
Value::RunningIdle | Value::RunningUnprocessed => Lifecycle::Running,
Value::SuspendedIdle | Value::SuspendedUnprocessed => Lifecycle::Suspended,
Value::DyingIdle | Value::DyingUnprocessed => Lifecycle::Dying,
Value::DeadIdle => Lifecycle::Dead,
}
}
/// Returns the current exception state.
///
/// Only Running, Suspended, and Dying threads have meaningful exception state.
pub fn exception(&self) -> Exception {
match self.value {
Value::RunningIdle | Value::SuspendedIdle | Value::DyingIdle => Exception::Idle,
Value::RunningUnprocessed | Value::SuspendedUnprocessed | Value::DyingUnprocessed => {
Exception::Unprocessed
}
Value::InitializedIdle | Value::DeadIdle => {
// Someone could have, for example, requested zx_info_thread_t.
Exception::Idle
}
Value::InitialIdle => {
panic!("invalid state: a thread in Initial has no meaningful exception state")
}
}
}
/// Transitions the lifecycle axis.
///
/// Illegal transitions `debug_assert!` and leave the state unchanged.
pub fn set_lifecycle(&mut self, lifecycle: Lifecycle) {
match lifecycle {
Lifecycle::Initial => {
debug_assert!(false);
}
Lifecycle::Initialized => match self.value {
Value::InitialIdle => self.value = Value::InitializedIdle,
_ => debug_assert!(false),
},
Lifecycle::Running => match self.value {
Value::InitializedIdle | Value::SuspendedIdle => self.value = Value::RunningIdle,
Value::SuspendedUnprocessed => self.value = Value::RunningUnprocessed,
_ => debug_assert!(false),
},
Lifecycle::Suspended => match self.value {
Value::RunningIdle => self.value = Value::SuspendedIdle,
Value::RunningUnprocessed => self.value = Value::SuspendedUnprocessed,
_ => debug_assert!(false),
},
Lifecycle::Dying => match self.value {
Value::RunningIdle | Value::SuspendedIdle | Value::DyingIdle => {
self.value = Value::DyingIdle
}
Value::RunningUnprocessed
| Value::SuspendedUnprocessed
| Value::DyingUnprocessed => self.value = Value::DyingUnprocessed,
_ => debug_assert!(false),
},
Lifecycle::Dead => match self.value {
Value::DyingIdle | Value::DyingUnprocessed => self.value = Value::DeadIdle,
_ => debug_assert!(false),
},
}
}
/// Transitions the exception axis.
///
/// Illegal transitions `debug_assert!` and leave the state unchanged.
pub fn set_exception(&mut self, exception: Exception) {
match exception {
Exception::Idle => match self.value {
Value::RunningUnprocessed => self.value = Value::RunningIdle,
Value::SuspendedUnprocessed => self.value = Value::SuspendedIdle,
Value::DyingUnprocessed => self.value = Value::DyingIdle,
_ => debug_assert!(false),
},
Exception::Unprocessed => match self.value {
Value::RunningIdle => self.value = Value::RunningUnprocessed,
Value::SuspendedIdle => self.value = Value::SuspendedUnprocessed,
Value::DyingIdle => self.value = Value::DyingUnprocessed,
_ => debug_assert!(false),
},
}
}
}
/// Converts a lifecycle enum value to its human-readable string representation.
pub fn lifecycle_to_string(lifecycle: Lifecycle) -> &'static str {
match lifecycle {
Lifecycle::Initial => "initial",
Lifecycle::Initialized => "initialized",
Lifecycle::Running => "running",
Lifecycle::Suspended => "suspended",
Lifecycle::Dying => "dying",
Lifecycle::Dead => "dead",
}
}
#[cfg(ktest)]
#[unittest::suite(name = "thread_state_rust")]
/// Unit tests for ThreadState.
mod tests {
use super::{Exception, Lifecycle, ThreadState, lifecycle_to_string};
use unittest::{expect_eq, expect_true};
/// Tests initial thread state.
#[test]
fn test_initial_state() {
let state = ThreadState::new();
expect_eq!(state.lifecycle(), Lifecycle::Initial);
// A thread in Initial has no meaningful exception state and `exception()` asserts, so it
// is deliberately not queried here.
}
/// Tests thread lifecycle state transitions.
#[test]
fn test_lifecycle_transitions() {
let mut state = ThreadState::new();
state.set_lifecycle(Lifecycle::Initialized);
expect_eq!(state.lifecycle(), Lifecycle::Initialized);
state.set_lifecycle(Lifecycle::Running);
expect_eq!(state.lifecycle(), Lifecycle::Running);
state.set_lifecycle(Lifecycle::Suspended);
expect_eq!(state.lifecycle(), Lifecycle::Suspended);
state.set_lifecycle(Lifecycle::Running);
expect_eq!(state.lifecycle(), Lifecycle::Running);
state.set_lifecycle(Lifecycle::Dying);
expect_eq!(state.lifecycle(), Lifecycle::Dying);
state.set_lifecycle(Lifecycle::Dead);
expect_eq!(state.lifecycle(), Lifecycle::Dead);
}
/// Tests that Dying accepts itself as a source, which is what makes a second kill() legal.
#[test]
fn test_dying_is_idempotent() {
let mut state = ThreadState::new();
state.set_lifecycle(Lifecycle::Initialized);
state.set_lifecycle(Lifecycle::Running);
state.set_lifecycle(Lifecycle::Dying);
expect_eq!(state.lifecycle(), Lifecycle::Dying);
state.set_lifecycle(Lifecycle::Dying);
expect_eq!(state.lifecycle(), Lifecycle::Dying);
// The exception axis is carried through the repeated transition unchanged.
state.set_exception(Exception::Unprocessed);
state.set_lifecycle(Lifecycle::Dying);
expect_eq!(state.lifecycle(), Lifecycle::Dying);
expect_eq!(state.exception(), Exception::Unprocessed);
}
/// Tests thread exception state transitions.
#[test]
fn test_exception_transitions() {
let mut state = ThreadState::new();
state.set_lifecycle(Lifecycle::Initialized);
state.set_lifecycle(Lifecycle::Running);
// Running: Idle -> Unprocessed -> Idle
state.set_exception(Exception::Unprocessed);
expect_eq!(state.exception(), Exception::Unprocessed);
state.set_exception(Exception::Idle);
expect_eq!(state.exception(), Exception::Idle);
// Transition with suspension. The exception axis is preserved across it.
state.set_exception(Exception::Unprocessed);
state.set_lifecycle(Lifecycle::Suspended);
expect_eq!(state.lifecycle(), Lifecycle::Suspended);
expect_eq!(state.exception(), Exception::Unprocessed);
state.set_exception(Exception::Idle);
expect_eq!(state.exception(), Exception::Idle);
}
/// Tests the exception state of threads that do not participate in exception handling.
#[test]
fn test_exception_of_non_participating_states() {
// Initialized and Dead are reachable by, for example, a request for `zx_info_thread_t`,
// even though a thread in either state never participates in exception handling.
let mut state = ThreadState::new();
state.set_lifecycle(Lifecycle::Initialized);
expect_eq!(state.exception(), Exception::Idle);
state.set_lifecycle(Lifecycle::Running);
state.set_lifecycle(Lifecycle::Dying);
state.set_lifecycle(Lifecycle::Dead);
expect_eq!(state.exception(), Exception::Idle);
}
/// Tests converting thread lifecycle to string representation.
#[test]
fn test_lifecycle_to_string() {
expect_true!(lifecycle_to_string(Lifecycle::Initial) == "initial");
expect_true!(lifecycle_to_string(Lifecycle::Initialized) == "initialized");
expect_true!(lifecycle_to_string(Lifecycle::Running) == "running");
expect_true!(lifecycle_to_string(Lifecycle::Suspended) == "suspended");
expect_true!(lifecycle_to_string(Lifecycle::Dying) == "dying");
expect_true!(lifecycle_to_string(Lifecycle::Dead) == "dead");
}
}