1

I'm just trying to get my head around multithreading environments, specifically how you would implement a cooperative one in c (on an AVR, but out of interest I would like to keep this general).

My problem comes with the thread switch itself: I'm pretty sure I could write this in assembler, flushing all the registers to a stack and then saving the PC to return to later.

How would one pull something like this off in c? I have been told it can do "everything".

I realize this is quite a general question, so any links with information on this topic would be greatly appreciated.

Thanks

jayjay
  • 1,017
  • 1
  • 11
  • 23
  • You would write some parts of the thread-switching code in assembler. There's no way to do the important parts in pure C. –  Jun 29 '13 at 15:46
  • Usually, you save the SP - the PC does not differentiate between threads running the same code. – Martin James Jun 29 '13 at 16:02
  • I've got some (simple) example code [in this answer](http://stackoverflow.com/a/16229020/733077). – luser droog Jun 29 '13 at 19:33

4 Answers4

5

You can do this with setjmp/longjmp on most systems -- here is some code I've use in the past for task switching:

void task_switch(Task *to, int exit)
{
int tmp;
int task_errno;     /* save space for errno */

    task_errno = errno;
    if (!(tmp = setjmp(current_task->env))) {
        tmp = exit ? (int)current_task : 1;
        current_task = to;
        longjmp(to->env, tmp); }
    if (exit) {
        /* if we get here, the stack pointer is pointing into an already
        ** freed block ! */
        abort(); }
    if (tmp != 1)
        free((void *)tmp);
    errno = task_errno;
}

This depends on sizeof(int) == sizeof(void *) in order to pass a pointer as the argument to setjmp/longjmp, but that could be avoided by using handles (indexes into a global array of all task structures) instead of raw pointers here, or by using a static pointer.

Of course, the tricky part is setting up jmpbuf objects for newly created tasks, each with their own stack. You can use a signal handler with sigaltstack for that:

static      void                    (*tfn)(void *);
static      void                    *tfn_arg;
static      stack_t                 old_ss;
static      int                     old_sm;
static      struct sigaction        old_sa;

            Task                    *current_task = 0;
static      Task                    *parent_task;
static      int                     task_count;

static void newtask()
{
int  sm;
void (*fn)(void *);
void *fn_arg;

    task_count++;
    sigaltstack(&old_ss, 0);
    sigaction(SIGUSR1, &old_sa, 0);
    sm = old_sm;
    fn = tfn;
    fn_arg = tfn_arg;
    task_switch(parent_task);
    sigsetmask(sm);
    (*fn)(fn_arg);
    abort();
}

Task *task_start(int ssize, void (*_tfn)(void *), void *_arg)
{
Task                *volatile new;
stack_t             t_ss;
struct sigaction    t_sa;

    old_sm = sigsetmask(~sigmask(SIGUSR1));
    if (!current_task) task_init();
    tfn = _tfn;
    tfn_arg = _arg;
    new = malloc(sizeof(Task) + ssize + ALIGN);
    new->next = 0;
    new->task_data = 0;
    t_ss.ss_sp = (void *)(new + 1);
    t_ss.ss_size = ssize;
    t_ss.ss_flags = 0;
    if ((unsigned long)t_ss.ss_sp & (ALIGN-1))
        t_ss.ss_sp = (void *)(((unsigned long)t_ss.ss_sp+ALIGN) & ~(ALIGN-1));
    t_sa.sa_handler = newtask;
    t_sa.sa_mask = ~sigmask(SIGUSR1);
    t_sa.sa_flags = SA_ONSTACK|SA_RESETHAND;
    sigaltstack(&t_ss, &old_ss);
    sigaction(SIGUSR1, &t_sa, &old_sa);
    parent_task = current_task;
    if (!setjmp(current_task->env)) {
        current_task = new;
        kill(getpid(), SIGUSR1); }
    sigaltstack(&old_ss, 0);
    sigaction(SIGUSR1, &old_sa, 0);
    sigsetmask(old_sm);
    return new;
}
Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
1

If you wanted to keep it pure C, I think you might be able to use setjmp and longjmp, but I've never tried it myself, and I imagine there's probably some platforms on which this wouldn't work (i.e. certain registers/other settings not being saved). The only other alternative would be to write it in assembly.

Drew McGowen
  • 11,471
  • 1
  • 31
  • 57
  • it seems like setjmp() is the way to go (I will probably end up writing it in assembler but setjmp() looks like a great solution for now), thanks for your comment! – jayjay Jun 29 '13 at 16:39
0

As mentioned, setjmp/longjmp are standard C and are available even in the libc of 8-bit AVRs. They do exactly what you said you'd do in assembler: save the processor context. But one has to keep in mind that the intended purpose of those functions is just to jump backwards in the flow of control; switching between tasks is an abuse. It does work anyway, and looks like this is even frequently used in a variety of user-level thread libraries -- like GNU Pth. But still, is an abuse of the intended purpose, and requires being careful.

As Chris Dodd said, you still need to provide an stack for each new task. He used sigaltstack() and other signal-related functions, but those do not exist in standard C, only in unix-like environments. For example, the AVR libc does not provide them. So as an alternative you can try reserving a part of your existing stack (by declaring a big local array, or using alloca()) for use as the stack of the new thread. Just keep in mind that the main/scheduler thread will keep using its stack, each thread uses its own stack, and all of them will grow and shrink as stacks usually do, so they will need space for doing so without interfering with each other.

And since we're already mentioning unix-like, non-standard-C mechanisms, there is also makecontext()/swapcontext() and family, which are more powerful but harder to find than setjmp()/longjmp(). The names say it all really: the context functions let you manage full process contexts (stacks included), the jmp functions let you just jump around - you'll have to hack the rest.

For the AVR anyway, given that you won't probably have an OS to help nor much memory to blindly reserve, you'd be probably better off using assembler for the switching and stack initializing.

hmijail
  • 1,069
  • 9
  • 17
0

In my experience if people start writing schedulers it isn't too long before they start wanting things like network stacks, memory allocation and file systems too. It's almost never worth going down that route; you end up spending more time writing your own operating system than you're spending on your actual application.

First whiff of your project heading that way and it's almost always worth putting the effort to put in an existing OS (linux, VxWorks, etc). Of course, that might mean that you run into problems if the CPU isn't up to it. And AVR isn't exactly a whole lot of CPU, and fitting an existing OS on to it ranges from mostly impossible to tricky for the major OSes, though there are some tiny OSes (some open source, see http://en.wikipedia.org/wiki/List_of_real-time_operating_systems).

So at the commencement of a project you should carefully consider how you might wish to evolve it going into the future. This might influence your choice of CPU now to save having to do hideous things in software later.

bazza
  • 7,580
  • 15
  • 22