Thread

XWOS Threads

Overview

Thread is the basic scheduling unit of XWOS, which may be called a task in other RTOSes. XWOS threads support freeze, thaw, migration, and other operations in addition to basic run, sleep, and exit operations.

XWOS thread functions are designed to mimic pthread functions.

Thread States

XWOS Thread State Diagram
Photo: xwos.tech / CC-BY-SA-4.0

  • Standby: The thread object has been initialized but no main function has been assigned;
  • Ready: The thread has been added to the ready queue;
  • Running: The thread is running. At most one thread can be running on each CPU;
  • Sleeping: The thread is sleeping;
  • Blocked: The thread is waiting, can be combined with the Sleeping state;
  • Freezable: The thread can be frozen;
  • Frozen: The thread has been frozen;
  • Exiting: The thread is about to end;
  • Migrating: The thread is in the process of migrating to another CPU;
  • Detached: When a detached thread exits, the operating system automatically reclaims its memory resources;
  • Joined: A joinable thread that has been join()ed by another thread;
  • Uninterrupted: The thread’s blocked and sleeping states cannot be interrupted.

Detached and Joinable Threads

XWOS thread detached and joinable states are designed with reference to pthread:

  • A joinable thread requires another thread to call xwos_thd_join() or xwos_thd_stop() to reclaim its memory resources;
  • When a detached thread exits, the system automatically reclaims its resources.

Thread Object and Object Descriptor

The thread object is a derived class of XWOS Object struct xwos_object. Similarly, thread objects also use the thread object descriptor xwos_thd_d to solve the problems of validity and identity legitimacy.

The thread object descriptor is composed of a pointer to the thread object and a tag:

typedef struct {
        struct xwos_thd * thd; /**< Pointer to the thread object */
        xwsq_t tik; /**< Tag */
} xwos_thd_d;

When referencing an object through the object descriptor, first check the value of obj->magic to see if it is 0x58574F53U, which confirms that the pointer obj points to a valid XWOS object. Then compare the tags obj->tik and tik to see if they are equal, which confirms the object’s identity. Since the object’s tik is globally unique, when the object is released, its tik will be destructed to 0 by the destructor. When the memory address is reconstructed into a new object, its tik will definitely differ from the tik in the object descriptor.

Thread Initialization and Creation

Thread Attributes

When creating or initializing a thread, its attributes can be set via the parameter struct xwos_thd_attr. XWOS thread attributes are implemented with reference to pthread, and their structure definition is also similar to pthread_attr_t.

  • xwos_thd_attr::privileged: Indicates that the thread has system privileges.
    • On ARMv6m/ARMv7m, this is implemented via bit0(nPRIV) of the CONTROL register;
    • On Embedded PowerPC, this is implemented via bit17(PR) of the MSR register;
    • On RISCV32, this is implemented via bit28 and bit29(MPP) of the MCAUSE register.
  • xwos_thd_attr::detached: Indicates that the thread is detached, similar to the detached attribute of POSIX threads.
    • When a detached thread exits, the system automatically reclaims its resources;
    • A joinable thread requires another thread to call xwos_thd_join() or xwos_thd_stop() to reclaim its memory resources.
  • xwos_thd_attr::stack: Indicates the starting address of the thread’s stack.
  • xwos_thd_attr::stack_size: Indicates the size of the thread’s stack in bytes.
  • xwos_thd_attr::stack_guard_size: Indicates the guard line position of the thread.
    • When the stack pointer grows past the guard line position, a stack overflow warning is triggered, but this requires support from the SOC’s MPU or MMU. You can set up the MPU or MMU for different SOCs in the HOOK function board_thd_postinit_hook() after thread creation.
    • XWOS internally also provides an if...else... based detection logic, but after a stack overflow the program may run wild and may not have a chance to run the detection logic code.
  • xwos_thd_attr::name: Indicates the thread’s name, used for log output during debugging.
  • xwos_thd_attr::priority: Indicates the thread’s priority. In XWOS, a higher numerical value means higher priority.

Static Initialization

  • Static initialization: xwos_thd_init()
  • Static means the user predefines thread structure objects, which are allocated memory by the compiler at compile time.
  • Statically initialized threads also require a pre-defined stack array with global scope.
  • The starting address and size of the stack array must comply with the CPU’s ABI rules. For example, ARM requires 8-byte alignment, so when defining the stack array you need to use __xwcc__aligned(8) to modify it, and the size must be a multiple of 8.
  • If the CPU has L1Cache, you should use __xwcc_alignl1cache to modify the stack array, aligning it to the L1Cache cache line.

Example

#define THD_PRIORITY XWOS_SKD_PRIORITY_DROP(XWOS_SKD_PRIORITY_RT_MAX, 1)

struct xwos_thd static_thd;
xwos_thd_d static_thdd;
__xwcc_aligned(8) xwstk_t static_thd_stack[512];

xwer_t thd_func(void * arg)
{
        /* ... thread function ... */
}

void some_function(void)
{
        xwos_thd_attr_init(&attr);
        attr.name = "static.thd";
        attr.stack = static_thd_stack;
        attr.stack_size = sizeof(static_thd_stack);
        attr.priority = THD_PRIORITY;
        attr.detached = false;
        attr.privileged = true;
        rc = xwos_thd_init(&static_thd, &static_thdd, &attr, thd_func, NULL);
}

Dynamic Creation

  • Dynamic creation: xwos_thd_create()
  • Dynamic means the program allocates memory through memory allocation functions at runtime and constructs the object on the allocated memory.
  • For dynamically created threads, the stack memory can also be dynamically allocated, and its address alignment is handled by the operating system kernel.
  • Dynamically created threads also support using statically defined arrays for stack memory. The starting address and size of the stack array must comply with the CPU’s ABI rules. For example, ARM requires 8-byte alignment, so when defining the stack array you need to use __xwcc__aligned(8) to modify it, and the size must be a multiple of 8.
  • If the CPU has L1Cache, you should use __xwcc_alignl1cache to modify the statically defined stack array, aligning it to the L1Cache cache line.
#define THD_PRIORITY XWOS_SKD_PRIORITY_DROP(XWOS_SKD_PRIORITY_RT_MAX, 1)

xwos_thd_d dynamic_thdd;

xwer_t thd_func(void * arg)
{
        /* ... thread function ... */
}

void some_function(void)
{
        struct xwos_thd_attr attr;
        xwer_t rc;

        xwos_thd_attr_init(&attr);
        attr.name = "dynamic.thd";
        attr.stack = NULL;
        attr.stack_size = 2048;
        attr.priority = THD_PRIORITY;
        attr.detached = false;
        attr.privileged = true;
        rc = xwos_thd_create(&dynamic_thdd, &attr, thd_func, NULL);
}

Interrupting a Thread’s Blocked and Sleeping States

When a thread calls a function that causes blocking or sleeping and enters the blocked state or sleeping state, it yields the CPU and the scheduler reschedules. Other threads or contexts can interrupt its blocked state or sleeping state via xwos_thd_intr(). The blocking or sleeping function will return with the error code -EINTR (-4).

Thread Exit and Return Value

Thread Exit

There are typically two ways for a thread to exit:

  • The main function directly returns
xwer_t thd_func(void * arg)
{
        /* ... omitted ... */
        return rc;
}

This CAPI is similar to the POSIX function pthread_exit(). The calling thread immediately terminates and throws a return value.

xwer_t thd_func(void * arg)
{
        /* ... omitted ... */
        xwos_cthd_exit(XWOK); /* The thread ends here, throwing the return value */
        /* Subsequent code is not executed ... */
}

Thread Detachment

The behavior of thread exit is related to the attribute xwos_thd_attr::detached:

  • When a detached thread exits, the system automatically reclaims its resources;
  • A joinable thread requires another thread to call xwos_thd_join() or xwos_thd_stop() to reclaim its memory resources. If forgotten, the resources will not be automatically reclaimed.

Notifying a Thread to Exit

xwos_thd_quit() can be used to notify a thread to exit. Calling this CAPI sets the exit state for the thread and interrupts the thread’s blocked state and sleeping state.

The blocking and sleeping CAPI being called by the notified thread will return with the value -EINTR, unless the notified thread is uninterruptible.

The thread itself can check its exit state via xwos_cthd_shld_stop() or xwos_cthd_frz_shld_stop().

Waiting for Thread Exit

If the thread is joinable, other threads can wait for its end and retrieve its return value via xwos_thd_join(). After this CAPI is called, the operating system also reclaims the thread’s resources.

Terminating a Thread

xwos_thd_stop() can terminate a thread and wait for it to exit. This CAPI is equivalent to xwos_thd_quit() + xwos_thd_join().

Thread Self-Checking Exit State

The thread itself can check its exit state via xwos_cthd_shld_stop(). It can be used as the termination condition of a thread loop:

xwer_t thd_func(void * arg)
{
        while (!xwos_cthd_shld_stop()) {
            /* ... thread loop ... */ ;
        }
}

Thread Self-Sleep

XWOS kernel provides multiple thread sleep methods:

  • xwos_cthd_sleep(): The starting point of sleep time is obtained by this CAPI itself. This method only requires telling the CAPI how long to sleep; it is simple to use but has lower precision.
  • xwos_cthd_sleep_to(): Specifies a future point in time to be awakened, with higher precision.
  • xwos_cthd_sleep_from(): The starting point and duration of sleep are provided by the caller. The starting point can be a past point in time.

If a thread just wants the scheduler to reschedule within the ready queue of the same priority, it can call xwos_cthd_sleep().

Thread Freeze and Thaw

Thread Self-Freeze

Thread freezing is used to support some special features of the kernel. Users should not freeze threads arbitrarily. In the following situations, the XWOS kernel requires the thread to enter the frozen state:

  • When the system is preparing to enter low-power mode. If the thread is still running, it is very likely to be accessing hardware resources or holding locks, causing exceptions when the system shuts down hardware or cleans up resources. Therefore, the thread needs to run to a special point before freezing; this point is called the freeze point. Before entering the freeze point, the thread needs to return to the outermost main function and release all locks and hardware resources.
  • When a thread migrates to another CPU. During thread migration, it also needs to return to the outermost freeze point to ensure that no resources of the current CPU are occupied.

The thread can check the freezable state via xwos_cthd_shld_frz(). Once the freezable state is detected, it needs to call xwos_cthd_freeze() to freeze itself.

Example:

xwer_t thd_func(void * arg)
{
        /* ... omitted ... */
        while (!xwos_cthd_shld_stop()) { /* Check if the thread needs to exit */
                rc = do_sth(/* ... */); /* The thread internally blocks on some synchronization object or lock */
                if (-EINTR == rc) { /* When the thread needs to freeze, blocking/sleeping will be interrupted and return -EINTR */
                        if (xwos_cthd_shld_frz()) { /* Check if freezing is needed */
                                release_resource(); /* Release resources ... */
                                xwos_cthd_freeze(); /* Freeze */
                                /* After the thread is thawed, execution continues from here. */
                                /* If migration occurred, the thread also starts running from here on another CPU. */
                                acquire_resource(); /* Reacquire resources ... */
                        } else {
                                /* Handle interrupts caused by other reasons ... */
                        }
                }
        }
        /* ... omitted ... */
}

If the thread does not need to release any resources before freezing, you can use xwos_cthd_frz_shld_stop(). This CAPI is equivalent to xwos_cthd_shld_frz() + xwos_cthd_freeze() + xwos_cthd_shld_stop()

Thread loop:

xwer_t thd_func(void * arg)
{
        bool wasfrz;
        /* ... omitted ... */
        while (!xwos_cthd_frz_shld_stop(&wasfrz)) { /* You can know whether the thread was frozen via wasfrz */
                /* ... thread loop ... */;
        }
        /* ... omitted ... */
}

Thaw

Thread thawing is not controlled by the user. The system automatically thaws the thread after completing special functions:

  • When the system exits low-power mode
  • When the thread migration operation has completed

Thread Migration

In a multi-core system, an XWOS thread is only scheduled on a specific CPU. The XWOS kernel does not automatically balance threads but supports migrating a thread to another CPU.

Migration Process

  • Assumption: The thread is currently on CPU-A and is to be migrated to CPU-B
  • Process:
    • The user calls the CAPI xwos_thd_migrate() in any context on any CPU;
      • The system sends a scheduler service interrupt to CPU-A, requesting migration out;
      • CPU-A switches to the scheduler service interrupt, sets the freeze flag on the thread, interrupts the thread’s blocked and sleeping states, then exits the interrupt context;
      • The thread on CPU-A is rescheduled and runs to the freeze point;
      • The thread at the freeze point sends a scheduler service interrupt to CPU-A, performing the freeze operation;
      • After the thread is frozen, CPU-A requests a scheduler service interrupt from CPU-B, requesting migration in;
      • CPU-B switches to the scheduler service interrupt, adds the thread to its own scheduler, removes the thread’s frozen state, and adds it to the ready list;
      • Migration completes and the thread begins scheduling on CPU-B.

Thread-Local Storage

After the C11 standard, Thread-Local Storage (TLS) was introduced. XWOS supports the keywords _Thread_local (C99), thread_local (C2X), as well as gcc and clang compiler extension keyword __thread.

If using a standard prior to C99, users can use:

Thread Object Lifecycle Management

The base class of the thread object is XWOS Object struct xwos_object. The thread object also has two sets of lifecycle management CAPI:

  • Access lifecycle management CAPI using object pointer: requires ensuring that the object is definitely valid when calling the CAPI, and there is no case of released-then-reallocated as another object.

    • xwos_thd_grab(): Increase reference count.
    • xwos_thd_put(): Decrease reference count. When the reference count drops to 0, the garbage collection function is called to release the object.
  • Access lifecycle management CAPI using object descriptor: used when the user cannot guarantee that the object is definitely valid or cannot guarantee that the object has not become another object.

    • xwos_thd_acquire(): Determine that the object is valid and legitimate through the object descriptor, then increase the reference count.
    • xwos_thd_release(): Determine that the object is valid and legitimate through the object descriptor, then decrease the reference count. When the reference count drops to 0, the garbage collection function is called to release the object.

CAPI Reference