CPU and Cores
CPU and Cores

What is a Thread?

A thread is an independent path of execution within a process. A process can contain multiple threads, all sharing the same address space, file descriptors, and heap — but each with its own stack and register state.

Examples: a browser runs one thread per tab; a text editor runs one thread for rendering and another for handling keystrokes. On Linux, threads and processes are both represented as tasks — the kernel makes no fundamental distinction between them.

Linux does not distinguish between processes and threads — it uses the generic term task. clone() with CLONE_THREAD is what pthread_create calls under the hood.

Thread within a program
Threads share heap and globals but each has its own stack

Thread Lifecycle — Joinable vs Detached

Every POSIX thread is created in one of two modes:

ModeHow to setWhat happens when thread exits
Joinable (default)pthread_join()Resources held until another thread calls pthread_join()
Detachedpthread_detach() or attrResources released immediately on exit
Memory leak risk: if you never call pthread_join() on a joinable thread and never detach it, the thread's stack and metadata are never freed — even after the thread function returns.
#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("Thread %d running\n", id);
    return NULL;
}

int main(void) {
    pthread_t tid;
    int id = 42;

    /* Create joinable thread (default) */
    if (pthread_create(&tid, NULL, worker, &id) != 0) {
        perror("pthread_create");
        return 1;
    }

    /* Block until thread exits — also frees its resources */
    pthread_join(tid, NULL);
    return 0;
}

Compile with: gcc thread.c -o thread -lpthread

pthread_create — Full Signature

int pthread_create(
    pthread_t        *thread,      /* OUT: handle to new thread        */
    const pthread_attr_t *attr,    /* thread attributes (NULL = default)*/
    void *(*start_routine)(void*), /* function to run                  */
    void             *arg          /* argument passed to function       */
);
/* Returns 0 on success, errno value on failure */

Pass NULL as attr to accept all defaults: scheduling policy SCHED_OTHER, priority 0, all CPU cores allowed, joinable mode.

Setting Thread Attributes

To control scheduling policy, priority, and CPU affinity you need a pthread_attr_t. Always initialise and destroy it:

pthread_attr_t attr;
pthread_attr_init(&attr);          /* initialise with defaults */

/* ... set fields ... */

pthread_create(&tid, &attr, fn, arg);
pthread_attr_destroy(&attr);       /* free internal resources  */

Scheduling Policy

Linux provides three scheduling policies relevant to real-time and embedded work:

PolicyTypePriority rangeUse case
SCHED_OTHERNormal (CFS)0 (fixed)Default — general-purpose tasks
SCHED_FIFOReal-time1–99Hard RT — runs until it blocks or yields, no time-slicing
SCHED_RRReal-time1–99Soft RT — like FIFO but with a round-robin timeslice between equal-priority threads
Rule of thumb: use SCHED_FIFO for hard real-time tasks (sensor interrupt handler, PWM generator). Use SCHED_RR when multiple RT threads share a core and must share time fairly. Use SCHED_OTHER for everything else.
Root required: setting SCHED_FIFO or SCHED_RR requires CAP_SYS_NICE — run as root or grant the capability with setcap cap_sys_nice+ep ./binary.
/* Set SCHED_FIFO on the attribute */
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

/* Must also set this flag — otherwise the attr policy is ignored */
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

The PTHREAD_EXPLICIT_SCHED flag is critical — without it, the thread inherits the creating thread's policy regardless of what you set in the attribute.

Thread Priority

Real-time threads use priority 1 (lowest RT) to 99 (highest RT). A higher-priority RT thread preempts any lower-priority thread immediately.

struct sched_param param;

/* Query the maximum allowed priority for this policy */
param.sched_priority = sched_get_priority_max(SCHED_FIFO);

pthread_attr_setschedparam(&attr, &param);
Don't always use max priority. Priority 99 means this thread preempts everything including kernel threads handling IRQs. Typical embedded RT tasks use priority 80–90, leaving room above for watchdog or panic handlers.

CPU Affinity — Pinning to a Core

cpu_set_t is a bitmask — one bit per logical CPU. You manipulate it with four macros:

cpu_set_t cpuset;

CPU_ZERO(&cpuset);           /* clear all bits            */
CPU_SET(2, &cpuset);         /* allow core 2              */
CPU_CLR(2, &cpuset);         /* remove core 2             */
CPU_ISSET(2, &cpuset);       /* check if core 2 is set    */

Apply the mask to the attribute before creating the thread:

pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);

To verify after creation — read back the actual affinity the thread is running with:

cpu_set_t actual;
pthread_getaffinity_np(tid, sizeof(cpu_set_t), &actual);

for (int c = 0; c < CPU_SETSIZE; c++) {
    if (CPU_ISSET(c, &actual))
        printf("Thread running on core %d\n", c);
}

Why Pin a Thread to a Core?

Three reasons this matters in embedded and real-time systems:

1. Cache locality — a thread pinned to one core keeps its working set in that core's L1/L2 cache. Migrating between cores causes cold cache misses, increasing latency unpredictably.

2. Interrupt affinity — on multi-core SoCs, IRQs are often distributed across cores. Pin your RT processing thread to the same core that handles the hardware interrupt to eliminate cross-core wakeup latency.

3. Determinism — in hard RT systems, you need to reason about worst-case execution time (WCET). An unbound thread can be migrated at any scheduler tick, making WCET analysis impossible. A pinned thread on a dedicated core eliminates that variable.

Complete Working Example

Creates a SCHED_FIFO RT thread pinned to core 1, verifies its affinity, then joins it:

#define _GNU_SOURCE
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

void *rt_worker(void *arg) {
    /* Confirm which core we're on */
    cpu_set_t actual;
    pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &actual);
    for (int c = 0; c < CPU_SETSIZE; c++) {
        if (CPU_ISSET(c, &actual))
            printf("[worker] running on core %d\n", c);
    }

    /* Real work goes here */
    printf("[worker] doing real-time work...\n");
    return NULL;
}

int main(void) {
    pthread_t tid;
    pthread_attr_t attr;
    struct sched_param param;
    cpu_set_t cpuset;
    int ret;

    /* 1. Initialise attribute */
    pthread_attr_init(&attr);

    /* 2. Set SCHED_FIFO + explicit scheduling */
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    /* 3. Set priority to 80 (leave room above for panic handlers) */
    param.sched_priority = 80;
    pthread_attr_setschedparam(&attr, &param);

    /* 4. Pin to core 1 */
    CPU_ZERO(&cpuset);
    CPU_SET(1, &cpuset);
    pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);

    /* 5. Create thread */
    ret = pthread_create(&tid, &attr, rt_worker, NULL);
    if (ret != 0) {
        fprintf(stderr, "pthread_create: %s\n", strerror(ret));
        return 1;
    }
    pthread_attr_destroy(&attr);

    /* 6. Join — wait for thread to finish and free its resources */
    pthread_join(tid, NULL);
    printf("[main] thread done\n");
    return 0;
}

/* Build: gcc -O2 rt_thread.c -o rt_thread -lpthread
   Run:   sudo ./rt_thread          (SCHED_FIFO needs root) */

Common Pitfalls

MistakeEffectFix
Forgetting -lpthreadLinker error: undefined reference to pthread_createAdd -lpthread to gcc command
Missing PTHREAD_EXPLICIT_SCHEDPolicy/priority in attr silently ignoredAlways set pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED)
Not joining or detachingThread stack never freed — memory leakAlways pthread_join() or pthread_detach()
Using priority 99Preempts kernel IRQ threads — system may hangUse 70–90 for application RT; reserve 99 for watchdog only
SCHED_FIFO without rootpthread_create returns EPERMRun as root or use setcap cap_sys_nice+ep
Passing stack variable as thread argThread reads freed stack memoryPass heap-allocated arg or ensure lifetime outlasts thread

For the full Linux scheduler reference see sched(7) — Linux man pages.


📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.