Thread Safety of the Compiler's Run-Time Library

As of V2.92.08, µC/OS-II provides built-in support for run-time library thread safety through the use of Task Local Storage (TLS) for storage of task-specific run-time library static data and mutual exclusion semaphores to protect accesses to shared resources.

The run-time environment consists of the run-time library, which contains the functions defined by the C and the C++ standards, and includes files that define the library interface (the system header files). Compilers provide complete libraries that are compliant with Standard C and C++. These libraries also supports floating-point numbers in IEEE 754 format and can be configured to include different levels of support for locale, file descriptors, multi-byte characters, etc. Most parts of the libraries are reentrant, but some functionality and parts are not reentrant because they require the use of static data. Different compilers provide different methods to add reentrancy to their libraries through an API defined by the tool chain supplier.

In a multi-threaded environment the C/C++ library has to handle all library objects with a global state differently. Either an object is a true global object, then any updates of its state has to be guarded by some locking mechanism to make sure that only one task can update it at any one time, or an object is local to each task, then the static variables containing the objects state must reside in a variable area local for the task. This area is commonly named thread local storage or, TLS.

The run-time library may also need to use multiple types of locks. For example, a lock could be necessary to ensure exclusive access to the file stream, another one to the heap, etc. It is thus common to protect the following functions through one or more semaphores:

  • The heap through the usage of malloc(), free(), realloc(), and calloc().
  • The file system through the usage of fopen(), fclose(), fdopen(), fflush(), and freopen().
  • The signal system through the usage of signal().
  • The tempfile system through the usage of tmpnam().
  • Initialization of static function objects.

Thread-local storage is typically needed for the following library objects:

  • Error functions through errno and strerror
  • Locale functions through the usage of localeconv() and setlocale()
  • Time functions through the usage of asctime(), localtime(), gmtime(), and mktime()
  • Multibyte functions through the usage of mbrlen(), mbrtowc(), mbsrtowc(), mbtowc(), wcrtomb(), wcsrtomb(), and wctomb()
  • Random functions through the usage of rand() and srand()
  • Other functions through the usage of atexit() and strtok()
  • C++ exception engine

Different compilers require different implementations and those implementation details are encapsulated into a single file called os_tls.c. There is thus one os_tls.c file associated with each compiler supported by Micrium and each implementation is placed in its own directory as follows:

\Micrium\Software\uCOS-II\TLS\<compiler manufacturer>\os_tls.c

Where ‘compiler manufacturer’ is the name of the compiler manufacturer or the code name for the compiler for which thread safety has been implemented. Refer to the code distribution to see if your compiler is supported.

Enabling Thread Safety

In order to enable thread safety, you need to do the following:

  • Set OS_TLS_TBL_SIZE in os_cfg.h to a value greater than 1. The actual value depends on the number of entries needed by the compiler used. In most cases you would set this to 5 but you should consult the os_tls.c that you plan to use for additional information.
  • Add to your build, the os_tls.c file that corresponds to the compiler you are using.
  • Depending on the compiler and how TLS is allocated, you may also need to make sure that you have a heap. Consult your compiler documentation on how you can enable the heap and determine its size.
  • Most likely, os_tls.c will make use of semaphores to guard access to shared resources (such as the heap or files) then you need to make sure OS_SEM_EN is set to 1 in os_cfg.h. Also, the run-time library may already define APIs to lock and unlock sections of code. The implementation of these functions should also be part of os_tls.c.

Task Specific Storage

When OS_TLS_TBL_SIZE is set to 1 or greater, each task’s OS_TCB will contain a new array called .OSTCBTLSTbl[] as shown below. Each array element is of type OS_TLS which is actually a pointer to void. This allows an OS_TCB to be extended so that it can have as many TLS areas as needed.

Figure - Each OS_TCB contains an array of OS_TLS when OS_TLS_TBL_SIZE is greater than 0 in os_cfg.h

Each OS_TCB contains an array of OS_TLS when OS_TLS_TBL_SIZE is greater than 0 in os_cfg.h


The number of entries (i.e., the value to set OS_TLS_TBL_SIZE to) depends on the compiler being supported as well as whether TLS storage is needed for other purposes.

OS_TLS_GetID()

The index into .OSTCBTLSTbl[] is called the TLS ID and TLS IDs are assigned through an API function. In other words, TLS IDs are assigned dynamically as needed. Once a TLS ID is assigned for a specific purpose, it cannot be ‘unassigned’. The function used to assign a TLS ID is called OS_TLS_GetID().

OS_TLS_SetValue()

µC/OS-II sets the value of a .OSTCBTLSTbl[] entry by calling OS_TLS_SetValue(). Because TLS is specific to a given task then you will need to specify the address of the OS_TCB of the task, the TLS ID that you want to set and the value to store into the table entry. Shown below is .OSTCBTLSTbl[] containing two entries (i.e., pointers) assigned by OS_TLS_SetValue().

OS_TLS_SetValue() assigns a pointer to a .OSTCBTLSTbl[] entry

OS_TLS_GetValue()

The value stored into a .OSTCBTLSTbl[] entry can be retrieved by calling OS_TLS_GetValue(). The address of the OS_TCB of the task you are interested has to be specified as part of the call as well as the desired TLS ID. OS_TLS_GetValue() returns the value stored in that task’s .TLS_Tbl[] entry indexed by the TLS ID.

OS_TLS_SetDestruct()

Finally, each .OSTCBTLSTbl[] entry can have a ‘destructor’ associated with it. A destructor is a function that is called when the task is deleted. Destructors are common to all tasks. This means that if a destructor is assigned for a TLS ID, the same destructor will be called for all the tasks for that entry. Also, when a task is deleted, the destructor for all of the TLS IDs will be called – assuming, of course, that a destructor was assigned to the corresponding TLS ID. You set a destructor function by calling OS_TLS_SetDestruct() and specify the TLS ID associated with the destructor as well as a pointer to the function that will be called. Note that a destructor function must be declared as follows:


void MyDestructFunction (OS_TCB    *p_tcb,
                         OS_TLS_ID  id,
                         OS_TLS     value);


The drawing below shows the global destructor table. Note that not all implementations of os_tls.c will have destructors for the TLS.

Array of pointers to destructor functions (global to all tasks)

OS_TLS.C Internal Functions

There are four mandatory internal functions that needs to be implemented in os_tls.c if OS_CFG_TLS_TBL_SIZE is set to a non-zero value.

void OS_TLS_Init (void)

This function is called by OSInit() and in fact, is called after creating the kernel objects but before creating any of the internal µC/OS-III tasks. This means that OS_TLS_Init() is allowed to create event flags, semaphores, mutexes and message queues. OS_TLS_Init() would typically create mutexes to protect access to shared resources such as the heap or streams.

void OS_TLS_TaskCreate (OS_TCB *p_tcb)

This function is called by OSTaskCreate() allowing each task to allocate TLS storage as needed at task creation time. If a task needs to use a specific TLS ID, the TLS ID must have been previously assigned, most likely by the startup code in main() or in one of the first task that runs.

OS_TLS_TaskCreate() is called immediately after calling OSTaskCreateHook().

You should note that you cannot call OS_TLS_GetValue() or OS_TLS_SetValue() for the specified task, unless the task has been created.

OS_TLS_TaskCreate() should check that TLS is a feature enabled for the task being created. This is done by examining the OS_TCB’s option field (i.e., p_tcb->Opt) as follows:


void OS_TLS_TaskCreate (OS_TCB *p_tcb)
{
    OS_TLS p_tls;


    if ((p_tcb->Opt & OS_OPT_TASK_NO_TLS) == OS_OPT_NONE) {
        p_tls                    = /* Allocate storage for TLS */
        p_tcb->TLS_Tbl[MyTLS_ID] = p_tls;
    }
}


void OS_TLS_TaskDel (OS_TCB *p_tcb)

This function is called by OSTaskDel() allowing each task to deallocate TLS storage that was allocated by OS_TLS_TaskCreate(). If the os_tls.c file implements destructor functions then OS_TLS_Del() should call all the destructors for the TLS IDs that have been assigned.

OS_TLS_TaskDel() is called by OSTaskDel(), immediately after calling OSTaskDelHook().

The code below shows how OS_TLS_TaskDel() can be implemented.


void OS_TLS_TaskDel (OS_TCB *p_tcb)
{
    OS_TLS_ID            id;
    OS_TLS_DESTRUCT_PTR *p_tbl;


    for (id = 0; id < OS_TLS_NextAvailID; id++) {
        p_tbl = &OS_TLS_DestructPtrTbl[id];
        if (*p_tbl != (OS_TLS_DESTRUCT_PTR)0) {
           (*p_tbl)(p_tcb, id, p_tcb->TLS_Tbl[id]);
        }
    }
}


OS_TLS_TaskDel() should actually check that TLS was used by the task being deleted. This is done by examining the OS_TCB’s option field (i.e., p_tcb->Opt) as follows:


void OS_TLS_TaskDel (OS_TCB *p_tcb)
{
    OS_TLS_ID            id;
    OS_TLS_DESTRUCT_PTR *p_tbl;


    for (id = 0; id < OS_TLS_NextAvailID; id++) {
        p_tbl = &OS_TLS_DestructPtrTbl[id];
        if (*p_tbl != (OS_TLS_DESTRUCT_PTR)0) {
           (*p_tbl)(p_tcb, id, p_tcb->TLS_Tbl[id]);
        }
    }
}


An alternate implementation is shown below where OS_TLS_TaskDel() needs to deallocate storage for the task is shown below.

void OS_TLS_TaskSw (void)

This function is called by OSSched() before invoking OS_TASK_SW() and also, by OSIntExit() before calling OSIntCtxSw(). When OS_TLS_TaskSw() is called, OSTCBCurPtr will point to the task being switched out and OSTCBHighRdyPtr will point to the task being switched in.

OS_TLS_TaskSw() allows you to change the “current TLS” during a context switch. For example, if a compiler uses a global pointer that points to the current TLS then, OS_TLS_TaskSw() could set this pointer to point to the new task’s TLS.

OS_TLS_TaskSw() should check that TLS is desired for the task being switched in. This is done by examining the OS_TCB’s option field (i.e. p_tcb->Opt) as follows:


if ((p_tcb->Opt & OS_OPT_TASK_NO_TLS) == OS_OPT_NONE) {
    /* TLS option enabled for this task */
}


Compiler-Specific Lock APIs

As previously mentioned, some compilers may already have declared API functions that are called to ensure exclusive access to shared resources. For example, APIs such as _mutex_lock_file_system() and _mutex_unlock_file_system() could be required by the compiler to ensure exclusive access to the file system. os_tls.c might then implement these using µC/OS-III as shown below. Note that we also included the code to initialize the mutex in OS_TLS_Init().


OS_EVENT *OS_TLS_FS_Sem;   /* Needed to ensure exclusive access to the FS */


void OS_TLS_Init (INT8U *p_err)
{
    OS_TLS_NextAvailID = 0u;
    OS_TLS_NewLibID    = OS_TLS_GetID(p_err);
    if (*p_err != OS_ERR_NONE) {
        return;
    }
    OS_TLS_FS_Sem = OSSemCreate(1);
}


void _mutex_lock_file_system (void)
{
    INT8U  os_err;


    if (OSRunning == 0) {
        return;
    }
    OSSemPend((OS_EVENT *)OS_TLS_FS_Sem,
              (INT32U    )0u,
              (INT8U    *)&os_err);
}


void _mutex_unlock_file_system (void)
{
    INT8U err;


    if (OSRunning == 0) {
        return;
    }
    OSSemPost((OS_SEM *)OS_TLS_FS_Sem);
}


The compiler may require the implementation of many such API functions to ensure exclusive access to the heap, environment variables, etc. These would all be found in os_tls.c.