The full register set is often saved on the thread's stack, which means that one stack pointer may be all you need to store the program counter, status registers, and any other registers which need to be context-switched.
Here is a real-world example TCB/PCB from an RTOS I open-sourced a few months ago (Atomthreads):
typedef struct atom_tcb
{
/* Thread's current stack pointer */
POINTER sp_save_ptr;
/* Thread priority (0-255) */
uint8_t priority;
/* Thread entry point and parameter */
void (*entry_point)(uint32_t);
uint32_t entry_param;
/* Queue pointers */
struct atom_tcb *prev_tcb; /* Previous TCB in doubly-linked TCB list */
struct atom_tcb *next_tcb; /* Next TCB in doubly-linked list */
/* Suspension data */
uint8_t suspended; /* TRUE if task is currently suspended */
uint8_t suspend_wake_status; /* Status returned to woken suspend calls */
ATOM_TIMER *suspend_timo_cb; /* Callback registered for suspension timeouts */
} ATOM_TCB;
Apart from the stack pointer, the key elements I needed were as follows:
- Priority
- Linked list pointers: To manage threads in the ready queue for simple queue-based schedulers, or handle queues of threads waiting on a particular semaphore etc.
- Suspension status: For handling actions like pending on a semaphore. These are used to register the callback function to be called if a suspension times out (which could be the timeout handler in the queue library for example) and to pass status codes back to the woken thread.
This is not the only way to do it. You will find that your own particular requirements become clear as you set out on the design of the RTOS, and implement the various OS primitives (semaphores, queues etc).