Skip to content

Processes

Benoît Thébaudeau edited this page Sep 23, 2019 · 15 revisions

Code in Contiki runs in either of two execution contexts: cooperative or preemptive. Cooperative code runs sequentially with respect to other cooperative code. Preemptive code temporarily stops the cooperative code. Contiki processes run in the cooperative context, whereas interrupts and real-time timers run in the preemptive context.

All Contiki programs are processes. A process is a piece of code that is executed regularly by the Contiki system. Processes in Contiki are typically started when the system boots, or when a module that contains a process is loaded into the system. Processes run when something happens, such as a timer firing or an external event occurring.

Code in Contiki can run in one of two execution contexts: cooperative or preemptive. Code running in the cooperative execution context is run sequentially with respect to other code in the cooperative context. Cooperative code must run to completion before other cooperatively scheduled code can run. Preemptive code may stop the cooperative code at any time. When preemptive code stops the cooperative code, the cooperative code will not be resumed until the preemptive code has completed. The concept of Contiki's two scheduling contexts is illustrated above.

Processes always run in the cooperative context. The preemptive context is used by interrupt handlers in device drivers and by real-time tasks that have been scheduled for a specific deadline. We return to the real-time tasks when we discuss timers.

Table of Contents

The Structure of a Process

A Contiki process consists of two parts: a process control block and a process thread. The process control block, which is stored in RAM, contains run-time information about the process such as the name of the process, the state of the process, and a pointer to the process thread of the process. The process thread is the code of the process and is stored in ROM.

The Process Control Block

The process control block contains information about each process, such as the state of the process, a pointer to the process' thread, and the textual name of the process. The process control block is only used internally by the kernel and is never directly accessed by processes.

The internal structure of the process control block. None of the fields in the process control block should be directly accessed by user code:

 struct process {
   struct process *next;
   const char *name;
   int (* thread)(struct pt *,
                  process_event_t,
 		 process_data_t);
   struct pt pt;
   unsigned char state, needspoll;
 };

A process control block is lightweight in that it requires only a couple of bytes of memory. The structure of the process control block is shown above. None of the fields in this structure should be accessed directly. Only the process management functions should access them. The structure is shown here only to demonstrate how lightweight the process control block is. The first field in the process control block, next, points to the next process control block in the linked list of active processes. The name field points to the textual name of the process. The thread field, which is a function pointer, points to the process thread of the processes. The pt field holds the state of the protothread in the process thread. The state and needspoll fields are internal flags that keep the state of the process. The needspoll flag is set by the process_poll() function when the process is polled.

A process control block is not declared or defined directly, but through the PROCESS() macro. This macro takes two parameters: the variable name of the process control block, which is used when accessing the process, and a textual name of the process, which is used in debugging and when printing out lists of active processes to users. The definition of the process control block corresponding to the Hello World example is shown below.

An example process control block:

 PROCESS(hello_world_process, "Hello world process");

The Process Thread

A process thread contains the code of the process. The process thread is a single protothread that is invoked from the process scheduler. An example process thread is shown below.

An example process thread:

 PROCESS_THREAD(hello_world_process, ev, data)
 {
   PROCESS_BEGIN();
 
   printf("Hello, world\n");
 
   PROCESS_END();
 }

Protothreads

A protothread is a way to structure code in a way that allows the system to run other activities when the code is waiting for something to happen. The concept of protothreads was developed within the context of Contiki, but the concept is not Contiki-specific and protothreads are used in many other systems as well.

Protothread provides a way for C functions to work in a way that is similar to threads, without the memory overhead of threads. Reducing the memory overhead is important on the memory-constrained systems on which Contiki runs.

A protothread is a regular C function. The function starts and ends with two special macros, PT_BEGIN() and PT_END(). Between these macros, a set of protothread functions can be used.

C preprocessor implementation of the main protothread operations:

 struct pt { lc_t lc };
 #define PT_WAITING 0
 #define PT_EXITED  1
 #define PT_ENDED   2
 #define PT_INIT(pt)          LC_INIT(pt->lc)
 #define PT_BEGIN(pt)         LC_RESUME(pt->lc)
 #define PT_END(pt)           LC_END(pt->lc);    \
                              return PT_ENDED
 #define PT_WAIT_UNTIL(pt, c) LC_SET(pt->lc);    \
                              if(!(c))           \
                                return PT_WAITING
 #define PT_EXIT(pt)          return PT_EXITED

Local continuations implemented with the C switch statement:

 typedef unsigned short lc_t;
 #define LC_INIT(c)   c = 0
 #define LC_RESUME(c) switch(c) { case 0:
 #define LC_SET(c)    c = __LINE__; case __LINE__:
 #define LC_END(c)    }

Protothreads in Processes

Contiki process implement their own version of protothreads, that allow processes to wait for incoming events. The protothread statements used in Contiki processes are therefore slightly different than the pure protothread statements as presented in the previous section.

Process-specific protothread macros that are used in Contiki processes:

PROCESS_BEGIN(); // Declares the beginning of a process' protothread. 
PROCESS_END(); // Declares the end of a process' protothread. 
PROCESS_EXIT(); // Exit the process. 
PROCESS_WAIT_EVENT(); // Wait for any event. 
PROCESS_WAIT_EVENT_UNTIL(); // Wait for an event, but with a condition.
PROCESS_YIELD(); // Wait for any event, equivalent to PROCESS_WAIT_EVENT().
PROCESS_WAIT_UNTIL(); // Wait for a given condition; may not yield the process.
PROCESS_PAUSE(); // Temporarily yield the process.

Events

In Contiki, a process is run when it receives an event. There are two types of events: asynchronous events and synchronous events.

When an asynchronous events is posted, the event is put on the kernel's event queue and delivered to the receiving process at some later time.

When a synchronous event is posted, the event is immediately delivered to the receiving process.

Asynchronous Events

Asynchronous events are delivered to the receiving process some time after they have been posted. Between their posting and their delivery, the asynchronous events are held on an event queue inside the Contiki kernel.

The events on the event queue are delivered to the receiving process by the kernel. The kernel loops through the event queue and delivers the events to the processes on the queue by invoking the processes.

The receiver of an asynchronous event can either be a specific process, or all running processes. When the receiver is a specific process, the kernel invokes this process to deliver the event. When the receiver of an event is set to be all processes in the system, the kernel sequentially delivers the same event to all processes, one after another.

Asynchronous events are posted with the process_post() function. The internals of the process_post() function is simple. It first checks the size of the current event queue to determine if there is room for the event on the queue. If not, the function returns an error. If there is room for the event on the queue, the function inserts the event at the end of the event queue and returns.

Synchronous Events

Unlike asynchronous events, synchronous events are delivered directly when they are posted. Synchronous events can only be posted to a specific process.

Because synchronous events are delivered immediately, posting a synchronous event is functionally equivalent to a function call: the process to which the event is delivered is directly invoked, and the process that posted the event is blocked until the receiving process has finished processing the event. The receiving process is, however, not informed whether the event was posted synchronously or asynchronously.

Polling

A poll request is a special type of event. A process is polled by calling the function process_poll(). Calling this function on a process causes the process to be scheduled as quickly as possible. The process is passed a special event that informs the process that it has been polled.

Polling is the way to make a process run from an interrupt. The process_poll() function is the only function in the process module that is safe to call from preemptive mode.

Event Identifiers

Events are identified by an event identifier. The event identifier is an 8-bit number that is passed to the process that receives an event. The event identifier allows the receiving process to perform different actions depending on what type of event that was received.

Event identifiers below 127 can be freely used within a user process, whereas event identifiers above 128 are intended to be used between different processes. Identifiers above 128 are managed by the kernel.

The first numbers over 128 are statically allocated by the kernel, to be used for a range of different purposes. The definition for these event identifiers are shown below.

Event identifiers reserved by the Contiki kernel:

 #define PROCESS_EVENT_NONE            128
 #define PROCESS_EVENT_INIT            129
 #define PROCESS_EVENT_POLL            130
 #define PROCESS_EVENT_EXIT            131
 #define PROCESS_EVENT_CONTINUE        133
 #define PROCESS_EVENT_MSG             134
 #define PROCESS_EVENT_EXITED          135
 #define PROCESS_EVENT_TIMER           136

These event identifiers are used as follows.

PROCESS_EVENT_NONE : This event identifier is not used.
PROCESS_EVENT_INIT : This event is sent to new processes when they are initiated.
PROCESS_EVENT_POLL : This event is sent to a process that is being polled.
PROCESS_EVENT_EXIT : This event is sent to a process that is being killed by the kernel. The process may choose to clean up any allocated resources, as the process will not be invoked again after receiving this event.
PROCESS_EVENT_CONTINUE : This event is sent by the kernel to a process that is waiting in a PROCESS_YIELD() statement.
PROCESS_EVENT_MSG : This event is sent to a process that has received a communication message. It is typically used by the IP stack to inform a process that a message has arrived, but can also be used between processes as a generic event indicating that a message has arrived.
PROCESS_EVENT_EXITED : This event is sent to all processes when another process is about to exit. A pointer to the process control block of the process that is existing is sent along the event. When receiving this event, the receiving processes may clean up state that was allocated by the process that is about to exit.
PROCESS_EVENT_TIMER : This event is sent to a process when an event timer (etimer) has expired.
In addition to the statically allocated event numbers, processes can allocate event identifiers above 128 to be used between processes. The allocated event identifiers are stored in a variable, which the receiving process can use to match the event identifier with.

The Process Scheduler

The purpose of the process scheduler is to invoke processes when it is their time to run. The process scheduler invokes process by calling the function that implements the process' thread. All process invocation in Contiki is done in response to an event being posted to a process, or a poll has being requested for the process, the process scheduler passes the event identifier to the process that is being invoked. With the event identifier, an opaque pointer is passed. The pointer is provided by the caller and may be set to NULL to indicate that no data is to be passed with the event. When a poll is requested for a process, no data can be passed.

Starting Processes

Processes are started with the process_start(). The purpose of this function is to set up the process control structure, place the process on the kernel's list of active processes, and to call the initialization code in the process thread. After process_start() has been called, the process is started.

The process_start() function first does a sanity check if the process that is to be started already exists on the list of active processes. If so, the process has already been started and the process_start() function returns.

After making sure that the process is not already started, the process is put on the list of active processes and the process control block is set up. The state of the process is set to PROCESS_STATE_RUNNING and the process' protothread is initialized with PT_INIT().

Finally, the process is sent a synchronous event, PROCESS_EVENT_INIT. An opaque pointer is provided together with the event. This pointer is passed from the process that invoked process_start() and can be used as a way to transport information to the process that is to be started. Normally, however, this pointer is set to NULL.

As the process receives its first event, the PROCESS_EVENT_INIT event, the process executes the first part of its process thread. Normally, this part of the process thread contains initialization code that is to be run once when the process starts.

Exiting and Killing Processes

Processes exit in either of two ways. Either the process itself exits, or it is killed by another process. Processes exit either by calling the PROCESS_EXIT() function or when the execution of the process thread reaches a PROCESS_END() statement. A process can be killed by another process by calling the process_exit() function.

When a process exits, regardless of it exits itself or if it is being killed by another process, the Contiki kernel send an event to all other processes to inform them of the process exiting. This can be used by other processes to free up any resource allocations made by the process that is exiting. For example, the uIP TCP/IP stack will close and remove any active network connections that the exiting process has. The event, PROCESS_EVENT_EXITED, is sent as a synchronous event to all active processes except for the one that is exiting.

If a process is killed by another process, the process that is killed is also sent a synchronous event, PROCESS_EVENT_EXIT. This event informs the process that it is about to be killed and the process can take the opportunity to free up any resource allocations it has made, or inform other processes that it is about to exit.

After the Contiki kernel has sent the events informing the processes about the process that is about to exit, the process is removed from the list of active processes.

Autostarting Processes

Contiki provides a mechanism whereby processes can be automatically started either when the system is booted, or when a module that contains the processes is loaded. This mechanism is implemented by the autostart module. The purpose of the autostart mechanism is to provide a way that the developer of a module can inform the system about what active processes the module contains. The list is also used when killing processes as a module is to be unloaded from memory.

Autostarted processes are kept on a list, that is used by the autostart module to start the processes. The processes are started in the order they appear on the list.

There are two ways in which processes are autostarted: during system boot-up or when a module is loaded. All processes that are to be started at system boot-up must be contained in a single, system-wide list. This list is supplied by the user, typically in one of the user modules that are compiled with the Contiki kernel. When the module is used as a loadable module, the same list is used to know what processes to start when the module is loaded.

When a module is loaded, the module loader finds the list of autostart processes and starts these processes after the module has been loaded into executable memory. When the module is to be unloaded, the module loader uses the same process list to kill the processes that were started as part of the module loading process.

In Contiki, the autostart mechanism is the most common way through which user processes are started.

An Example Process

To make the discussion about how processes work concrete, we now turn to an example of two processes. This example not only shows how the code that implements a process looks like, but also the different steps in the lifetime of a process. Below is a brief example of a "Hello, world"-style program, but this example is a little more elaborate.

An example process that receives events and prints out their number:

 #include "contiki.h"
 
 PROCESS(example_process, "Example process");
 AUTOSTART_PROCESSES(&example_process);
 
 PROCESS_THREAD(example_process, ev, data)
 {
   PROCESS_BEGIN();
 
   while(1) {
     PROCESS_WAIT_EVENT();
     printf("Got event number %d\n", ev);
   }
 
   PROCESS_END();
 }

The code shown above is a complete example of a Contiki process. The process is declared, defined, and is automatically started with the autostart feature. We go through the example line by line.

At line 1, we include the Contiki header files. The contiki.h file will include all the headers necessary for the basic Contiki functions to work. At line 3, we define the process control block. The process control definition defines both the variable name of the process control block, which in this case is example_process, and the textual, human-readable name of the process, which in this case is Example process.

After defining the process control block, we can use the variable name in other expressions. At line 4, the AUTOSTART_PROCESSES() declaration tells Contiki that the example_process is to be automatically started when Contiki boots, or, if this module is compiled as a loadable module, when the module is loaded. The autostart list consists of pointers to process control blocks, so we take the address of example_process by prefixing it with an ampersand.

At line 6, we begin the definition of the process' thread. This includes the variable name of the process, example_process, and the names of the event passing variables ev and data. The event passing variables are used when the process receives events, as we describe below.

At line 8, we begin the process with the PROCESS_BEGIN() declaration. This declaration marks start of code belonging to the process thread. Code that is placed above this declaration will be (re)executed each time the process is scheduled to run. Code placed below the declaration will be executed based on the actual process thread control flow. In most cases, you don't need to place any code above PROCESS_BEGIN().

At line 10, we start the main loop of the process. As we have previously said, Contiki processes cannot start loops that never end, but in this case we are safe to do this because we wait for events below. When a Contiki process waits for events, it returns control to the Contiki kernel. The Contiki kernel will service other processes while this process is waiting.

The process waits for events at line 11. The PROCESS_WAIT_EVENT() statement will return control to the Contiki kernel, and wait for the kernel to deliver an event to this process. When the Contiki kernel delivers an event to the process, the code that follows the PROCESS_WAIT_EVENT() is executed. After the process wakes up, the printf() statement at line 12 is executed. This line simply prints out the event number of the event that the process received. The event number is contained in the ev variable. If a pointer was passed along with the event, this pointer is available in the data variable. In this example, however, we disregard any such pointer.

The PROCESS_END() statement at line 15 marks the end of the process. Every Contiki process must have a PROCESS_BEGIN() and a PROCESS_END() statement. When the execution reaches the PROCESS_END() statement, the process will end and will be removed from the kernel's active list. In this case, however, the PROCESS_END() statement will never be reached, because of the never-ending loop between lines 10 and 13. Instead, this process will continue to run either until the system is switched off, or until another process kills it via process_exit().

A function that starts a process and sends it events:

 static char msg[] = "Data";
 
 static void
 example_function(void)
 {
   /* Start "Example process", and send it a NULL
      pointer. */
 
   process_start(&example_process, NULL);
  
   /* Send the PROCESS_EVENT_MSG event synchronously to
      "Example process", with a pointer to the message in the
      array 'msg'. */
   process_post_synch(&example_process,
                      PROCESS_EVENT_CONTINUE, msg);
  
   /* Send the PROCESS_EVENT_MSG event asynchronously to 
      "Example process", with a pointer to the message in the
      array 'msg'. */
   process_post(&example_process,
                PROCESS_EVENT_CONTINUE, msg);
 
   /* Poll "Example process". */
   process_poll(&example_process);
 }

Interaction between two processes is done with events. The example above shows an example of a function that starts a process and sends it a synchronous event, an asynchronous event, and a poll.

Conclusions

Processes are the primary way applications are run in Contiki. Processes consist of a process control block and a process thread. The process control block contains run-time information about the process and the process thread contains the code of the process. The process thread is implemented as a protothread, which is a lightweight thread specifically designed for memory-constrained systems.

In Contiki, code run in either of two execution contexts: cooperative, in which code never preempts other code, and preemptive, which preempts the execution of the cooperative code and returns control when the preemptive code is finished. Processes always run in cooperative mode, where as interrupts run in preemptive mode. The only process control function that can be called from preemptive mode is process_poll().

Processes communicate with each other by posting events to each other. Events are also posted when a process starts and exits.

Clone this wiki locally