Tasks? Processes? Threads?

Tasks

  • In userspace, there is no such thing as a task

  • Only processes and threads

  • Kernel/scheduler is different

    • Process is-a task

    • Thread is-a task

  • Tasks are scheduled

A Typical Bare Metal Application

Back in Time, Back in Technology

  • One timer interrupt with two responsibilities

    • Update status

    • Show status

// g++ -o /tmp/embedded-app-1-irq embedded-app-1-irq.cpp -lrt

#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

enum State
{
    INITIAL,
    RISING,
    FALLING
};


struct app_status {
    int state;
    int tick_counter;
    char display[12];
};
struct app_status status;

static void update_status(int)
{
    switch (status.state) {
        case INITIAL:
            if (status.tick_counter == 9)
                status.state = RISING;
            break;
        case RISING:
            status.display[status.tick_counter] = '/';
            if (status.tick_counter == 9)
                status.state = FALLING;
            break;
        case FALLING:
            status.display[status.tick_counter] = '\\';
            if (status.tick_counter == 9)
                status.state = RISING;
            break;
    }

    if (status.tick_counter < 9)
        status.tick_counter++;
    else 
        status.tick_counter = 0;

    write(STDOUT_FILENO, status.display, sizeof(status.display));
}

int main()
{
    // initialize application
    {
        status.state = INITIAL;
        status.tick_counter = 0;
        strcpy(status.display, "----------\n");
    }

    // establish timer handler
    {
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = update_status;

        int error = sigaction(SIGRTMIN, &sa, NULL);
        if (error) {
            perror("sigaction");
            return 1;
        }
    }

    // create and start timer
    {
        struct sigevent sev;
        memset(&sev, 0, sizeof(sev));
        sev.sigev_notify = SIGEV_SIGNAL;
        sev.sigev_signo = SIGRTMIN;

        timer_t timer;
        int error = timer_create(CLOCK_MONOTONIC, &sev, &timer);
        if (error) {
            perror("timer_create");
            return 1;
        }

        itimerspec tspec = {
            /*interval*/
            {
                /*sec*/ 0, 
                /*nsec*/ 1000*1000*1000 / 2 // half a second
            },
            /*initial expiration in a second*/
            {
                /*sec*/ 1,
                /*nsec*/ 0
            }
        };

        error = timer_settime(timer, 0, &tspec, NULL);
        if (error) {
            perror("timer_settime");
            return 1;
        }
    }

    while (true)
        pause();  // or do power management, on a real bare metal
                  // platform

    return 0;
}
$ g++ -o /tmp/embedded-app-1-irq embedded-app-1-irq.cpp -lrt
$ /tmp/embedded-app-1-irq
----------
----------
----------
----------
----------
----------
----------
----------
----------
----------
/---------        <---- rising
//--------
///-------
////------
/////-----
//////----
^C

Hm. Need Another Timer Interrupt!

  • Update rates and display rates are the same currently

  • Want them to diverge for whatever reason

  • ⟶ “update” timer (0.5s, and a separate “show” timer (1s)

// g++ -o /tmp/embedded-app-2-irq embedded-app-2-irq.cpp -lrt

#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

enum State
{
    INITIAL,
    RISING,
    FALLING
};


struct app_status {
    int state;
    int tick_counter;
    char display[12];
};
struct app_status status;

static void update_status(int)
{
    switch (status.state) {
        case INITIAL:
            if (status.tick_counter == 9)
                status.state = RISING;
            break;
        case RISING:
            status.display[status.tick_counter] = '/';
            if (status.tick_counter == 9)
                status.state = FALLING;
            break;
        case FALLING:
            status.display[status.tick_counter] = '\\';
            if (status.tick_counter == 9)
                status.state = RISING;
            break;
    }

    if (status.tick_counter < 9)
        status.tick_counter++;
    else 
        status.tick_counter = 0;
}

static void show_status(int)
{
    write(STDOUT_FILENO, status.display, sizeof(status.display));
}

int main()
{
    // initialize application
    {
        status.state = INITIAL;
        status.tick_counter = 0;
        strcpy(status.display, "----------\n");
    }

    // establish update timer handler
    {
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = update_status;

        int error = sigaction(SIGRTMIN, &sa, NULL);
        if (error) {
            perror("sigaction");
            return 1;
        }
    }

    // create and start update timer
    {
        struct sigevent sev;
        memset(&sev, 0, sizeof(sev));
        sev.sigev_notify = SIGEV_SIGNAL;
        sev.sigev_signo = SIGRTMIN;

        timer_t timer;
        int error = timer_create(CLOCK_MONOTONIC, &sev, &timer);
        if (error) {
            perror("timer_create");
            return 1;
        }

        itimerspec tspec = {
            /*interval*/
            {
                /*sec*/ 0, 
                /*nsec*/ 1000*1000*1000 / 2 // half a second
            },
            /*initial expiration in a second*/
            {
                /*sec*/ 1,
                /*nsec*/ 0
            }
        };

        error = timer_settime(timer, 0, &tspec, NULL);
        if (error) {
            perror("timer_settime");
            return 1;
        }
    }

    // establish show timer handler
    {
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = show_status;

        int error = sigaction(SIGRTMIN+1, &sa, NULL);
        if (error) {
            perror("sigaction");
            return 1;
        }
    }

    // create and start show timer
    {
        struct sigevent sev;
        memset(&sev, 0, sizeof(sev));
        sev.sigev_notify = SIGEV_SIGNAL;
        sev.sigev_signo = SIGRTMIN+1;

        timer_t timer;
        int error = timer_create(CLOCK_MONOTONIC, &sev, &timer);
        if (error) {
            perror("timer_create");
            return 1;
        }

        itimerspec tspec = {
            /*interval*/
            {
                /*sec*/ 1, 
                /*nsec*/ 0
            },
            /*initial expiration in a second*/
            {
                /*sec*/ 1,
                /*nsec*/ 0
            }
        };

        error = timer_settime(timer, 0, &tspec, NULL);
        if (error) {
            perror("timer_settime");
            return 1;
        }
    }

    while (true)
        pause();  // or do power management, on a real bare metal
                  // platform

    return 0;
}
$ g++ -o /tmp/embedded-app-1-irq embedded-app-2-irq.cpp -lrt
$ /tmp/embedded-app-2-irq
----------
----------
----------
----------
----------
----------
//--------
////------
//////----
////////--
//////////
\\////////
^C

(Missing every second status update, roughly)

Away From Interrupts: Use An Operating System

  • I want to do periodic actions

  • Number of actions will soon exceed number of timer chips

  • ⟶ need something more capable

  • ⟶ an Embedded OS

    • FreeRTOS is a popular choice these days

  • Embedded OSen typically come with

    • Tasks and a task scheduler

    • Syncronous ways of sleeping

    • Arbitrary number of virtual timers, multiplexed on top of physical timer chips

  • ⟶ replace timer interrupts with synchronous loops in tasks

// g++ -o /tmp/embedded-app-tasks embedded-app-tasks.cpp -lrt -lpthread

#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>

enum State
{
    INITIAL,
    RISING,
    FALLING
};


struct app_status {
    int state;
    int tick_counter;
    char display[12];
};
struct app_status status;


static void* update_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        switch (status.state) {
            case INITIAL:
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
            case RISING:
                status.display[status.tick_counter] = '/';
                if (status.tick_counter == 9)
                    status.state = FALLING;
                break;
            case FALLING:
                status.display[status.tick_counter] = '\\';
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
        }

        if (status.tick_counter < 9)
            status.tick_counter++;
        else 
            status.tick_counter = 0;

        // interval
        struct timespec interval_ts = {
            /*sec*/ 0,
            /*nsec*/ 1000*1000*1000 / 2 // half a second
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

static void* show_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        write(STDOUT_FILENO, status.display, sizeof(status.display));

        struct timespec interval_ts = {
            /*sec*/ 1, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

int main()
{
    // initialize application
    {
        status.state = INITIAL;
        status.tick_counter = 0;
        strcpy(status.display, "----------\n");
    }

    // start update task
    {
        pthread_t update_task;
        int error = pthread_create(&update_task, NULL, update_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    // start show task
    {
        pthread_t show_task;
        int error = pthread_create(&show_task, NULL, show_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    while (true)
        pause();  // or do power management, on a real bare metal
                  // platform

    return 0;
}
$ g++ -o /tmp/embedded-app-tasks embedded-app-tasks.cpp -lrt -lpthread
$ /tmp/embedded-app-tasks
...

Tasks?

  • Great win: one process with two tasks (threads)

  • Lets do some introspection

$ ps -efl|grep embedded-app-tasks
0 S jfasch    231765  225819  0  80   0 -  5635 -      17:47 pts/4    00:00:00 /tmp/embedded-app-tasks
$ ps -L 231765
    PID     LWP TTY      STAT   TIME COMMAND
 231765  231765 pts/4    Sl+    0:00 /tmp/embedded-app-tasks
 231765  231766 pts/4    Sl+    0:00 /tmp/embedded-app-tasks
 231765  231767 pts/4    Sl+    0:00 /tmp/embedded-app-tasks
main thread
$ strace -p 231765
strace: Process 231765 attached
pause(
update thread
$ strace -p 231766
strace: Process 231766 attached
restart_syscall(<... resuming interrupted read ...>) = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=0, tv_nsec=500000000}, NULL) = 0
...
show thread
$ strace -p 231767
strace: Process 231767 attached
restart_syscall(<... resuming interrupted read ...>) = 0
write(1, "\\\\\\\\\\\\////\n\0", 12)    = 12
...

Threads Are Great: More Functionality

  • Not being interrupt driven anymore

  • ⟶ good

  • ⟶ flexible

  • lets add more functionality!

Nonsense: switch terminal between reverse and normal

  • Reverse: "\033[7m"

  • Normal: "\033[0m"

// g++ -o /tmp/embedded-app-more-tasks embedded-app-more-tasks.cpp -lrt -lpthread

#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>

enum State
{
    INITIAL,
    RISING,
    FALLING
};


struct app_status {
    int state;
    int tick_counter;
    char display[12];
};
struct app_status status;


static void* update_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        switch (status.state) {
            case INITIAL:
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
            case RISING:
                status.display[status.tick_counter] = '/';
                if (status.tick_counter == 9)
                    status.state = FALLING;
                break;
            case FALLING:
                status.display[status.tick_counter] = '\\';
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
        }

        if (status.tick_counter < 9)
            status.tick_counter++;
        else 
            status.tick_counter = 0;

        // interval
        struct timespec interval_ts = {
            /*sec*/ 0,
            /*nsec*/ 1000*1000*1000 / 2 // half a second
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

static void* show_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        write(STDOUT_FILENO, status.display, sizeof(status.display));

        struct timespec interval_ts = {
            /*sec*/ 1, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

static void* flash_func(void*)
{
    bool is_reverse = false;
    static char reverse[] = "\033[7m";
    static char normal[] = "\033[0m";

    while (true) {
        if (is_reverse)
            write(STDOUT_FILENO, reverse, sizeof(reverse));
        else
            write(STDOUT_FILENO, normal, sizeof(normal));

        is_reverse = !is_reverse;
        
        struct timespec interval_ts = {
            /*sec*/ 2, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);
    }
}


int main()
{
    // initialize application
    {
        status.state = INITIAL;
        status.tick_counter = 0;
        strcpy(status.display, "----------\n");
    }

    // start update task
    {
        pthread_t update_task;
        int error = pthread_create(&update_task, NULL, update_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    // start show task
    {
        pthread_t show_task;
        int error = pthread_create(&show_task, NULL, show_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    // start reverse/normal task
    {
        pthread_t flash_task;
        int error = pthread_create(&flash_task, NULL, flash_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    while (true)
        pause();  // or do power management, on a real bare metal
                  // platform

    return 0;
}
$ g++ -o /tmp/embedded-app-more-tasks embedded-app-more-tasks.cpp -lrt -lpthread
$ /tmp/embedded-app-more-tasks
...

Are Threads Great?

  • Cramming tons of functionality into a single program requires some programming skills

    • Program = Microcontroller

  • Our program approaches 200 lines which is still manageable (functionality is trivial though)

  • Need internet?

  • Need filesystem?

  • Need <insert favorite feature>?

Worst case: one thread causes a bug … (hint: it’s the unrelated terminal flasher)

// g++ -o /tmp/embedded-app-more-tasks-buggy embedded-app-more-tasks-buggy.cpp -lrt

#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>

enum State
{
    INITIAL,
    RISING,
    FALLING
};


struct app_status {
    int state;
    int tick_counter;
    char display[12];
};
struct app_status status;


static void* update_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        switch (status.state) {
            case INITIAL:
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
            case RISING:
                status.display[status.tick_counter] = '/';
                if (status.tick_counter == 9)
                    status.state = FALLING;
                break;
            case FALLING:
                status.display[status.tick_counter] = '\\';
                if (status.tick_counter == 9)
                    status.state = RISING;
                break;
        }

        if (status.tick_counter < 9)
            status.tick_counter++;
        else 
            status.tick_counter = 0;

        // interval
        struct timespec interval_ts = {
            /*sec*/ 0,
            /*nsec*/ 1000*1000*1000 / 2 // half a second
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

static void* show_status_func(void*)
{
    // initial expiration in a second
    struct timespec initial_ts = {
        /*sec*/ 1,
        /*nsec*/ 0
    };
    nanosleep(&initial_ts, NULL);

    while (true) {
        write(STDOUT_FILENO, status.display, sizeof(status.display));

        struct timespec interval_ts = {
            /*sec*/ 1, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);
    }

    return NULL;
}

static void* flash_func(void*)
{
    bool is_reverse = false;
    static char reverse[] = "\033[7m";
    static char normal[] = "\033[0m";

    // nasty!
    int i = 0;

    while (true) {
        if (is_reverse)
            write(STDOUT_FILENO, reverse, sizeof(reverse));
        else
            write(STDOUT_FILENO, normal, sizeof(normal));

        is_reverse = !is_reverse;
        
        struct timespec interval_ts = {
            /*sec*/ 2, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);

        // nasty!
        if (++i == 7) {
            status.display[0] = '@';
            status.display[1] = '!';
            status.display[2] = '%';
            status.display[3] = '$';
            status.display[4] = '#';
            i = 0;
        }
    }
}


int main()
{
    // initialize application
    {
        status.state = INITIAL;
        status.tick_counter = 0;
        strcpy(status.display, "----------\n");
    }

    // start update task
    {
        pthread_t update_task;
        int error = pthread_create(&update_task, NULL, update_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    // start show task
    {
        pthread_t show_task;
        int error = pthread_create(&show_task, NULL, show_status_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    // start reverse/normal task
    {
        pthread_t flash_task;
        int error = pthread_create(&flash_task, NULL, flash_func, NULL);
        if (error) {
            fprintf(stderr, "pthread_create: %s\n", strerror(error));
            return 1;
        }
    }

    while (true)
        pause();  // or do power management, on a real bare metal
                  // platform

    return 0;
}
$ g++ -o /tmp/embedded-app-more-tasks-buggy embedded-app-more-tasks-buggy.cpp -lrt -lpthread
$ /tmp/embedded-app-more-tasks-buggy
...

Stability Considerations

Three tasks (indepenently running entities)

  1. Update status

  2. Show status

  3. Toggle terminal fore/background

Stability considerations

  • The first two are related (communicate)

  • The last one is complete unrelated - it even causes a bug

Probably the latter requires a bit more isolation!

Processes, Address Spaces

  • Memory Management Unit (MMU)

  • Maps virtual addresses to physical addresses

  • Adds memory protections (on a per-page granularity, usually 4K)

    • Read-only

    • Executable (contains CPU instructions)

    ⟶ Memory access exceptions, leading to program crash

  • Enter address spaces

    • A process has its own address space

    • Must not access memory that it does not own; Segmentation fault (in Unix terminology), or Memory access violation

Stabilizing

  • Lets rip the offender out; “decouple the rest from it

  • Make a single threaded program that we start separately. Respectively,

// g++ -o /tmp/flash-terminal flash-terminal.cpp

#include <unistd.h>
#include <time.h>


int main()
{
    bool is_reverse = false;
    static char reverse[] = "\033[7m";
    static char normal[] = "\033[0m";

    while (true) {
        if (is_reverse)
            write(STDOUT_FILENO, reverse, sizeof(reverse));
        else
            write(STDOUT_FILENO, normal, sizeof(normal));

        is_reverse = !is_reverse;
        
        struct timespec interval_ts = {
            /*sec*/ 2, 
            /*nsec*/ 0
        };
        nanosleep(&interval_ts, NULL);
    }

    return 0;
}
$ g++ -o /tmp/flash-terminal flash-terminal.cpp
$ /tmp/flash-terminal &         # <--- BACKGROUND, ON SAME TERMINAL!
$ /tmp/embedded-app-tasks
... stable terminal flashing here ...