Skip to content

RFC: libc: thread-safe newlib #21519

@kaidoho

Description

@kaidoho

Introduction

Extend Zephyr to be able to use the C standard library newlib in a multi-threaded environment.

Problem description

The minimum requirement to use libstdc++ in an RTOS environment is to have a thread-safe C library at hand – which is, from my point of view, regarding Zephyr’s adoption of newlib not the case today.

Proposed change

Implement the hooks provided by newlib to allow its use in a thread-safe manner.

Detailed RFC

Despite the fact that there's a scheduler, none of the requirements to use newlib in a multi-threaded environment have been fullfilled.

Reentrancy

Newlib manages multi-threaded applications with the help of structs of type _reent, which serve as a constant data container between library calls. In addition to that, stubs to allow an RTOS to implement reentrant versions of the standard functions (open_r, write_r, etc.) are provided. Simplified, in a multi-threaded environment each thread should have its own _reent struct. It should then either call the reentrant library functions and pass it's _reent struct or set up the global _reent struct impure_ptr and use the non-reentrant library functions.

Today, the non-reentrant library functions are used and all threads operate on the same global _reent struct impure_ptr defined within newlib.

Malloc Hooks

Issue #17552 aims to provide thread-safety for malloc by adding a semaphore within sbrk. Newlib has addressed this issue by providing the hooks __malloc_lock and __malloc_unlock. The advantage of implementing them is that they guard also the calling code, not only sbrk.

Retarget Locking

Newlib internally uses mutex synchronization quite often. At the moment empty stubs are used as Zephyr does not provide an implementation. Therefore, there’s no synchronization at all. The stubs can be replaced at application build time if newlib was build with --enable-newlib-retargetable-locking. In such a scenario several __retarget_lock_* functions to create, delete, and operate recursive and non-recursive mutex become available for reimplementation. In addition to the mutex created during runtime by newlib one has to provide nine static locks.

GNU ARM Embedded toolchain is build using the switch above, but Zephyr SDK is not doing that yet.

Proposed change (Detailed)

Reentrancy

According to the documentation of newlib there are two ways to achieve reentrancy. Both require each thread of execution control to initialize a unique global variable of type struct _reent:

  1. Use the reentrant versions of the library functions, after initializing a global reentrancy structure for each process. Use the pointer to this structure as the extra argument for all library functions.
  2. Ensure that each thread of execution control has a pointer to its own unique reentrancy structure in the global variable _impure_ptr, and call the standard library subroutines.

Option 1 would require us to always call the reentrant functions such as malloc_r instead of malloc. On the one hand, this has to be explained to every user to make sure they call the right functions. On the other hand, 3rd party code can not be simply dropped in without modifications. Therefore, I propose to implement option 2.

Proposed change:

  • Add reent struct to each k_thread struct
  • Initialize thread's reent struct
  • Switch impure_ptr to point to thread's reent struct after context switch

Malloc Hooks / Retarget Locking

Proposed change:

  • Remove the semaphore from sbrk
  • If newlib was compiled for retargetable locking, a define _RETARGETABLE_LOCKING is present. Depending on this define make either
    • __malloc_lock and __malloc_unlock or
    • the __retarget_lock_* functions visible and add the static locks (there's one for malloc, too)
  • Some memory is required to store the locks created during runtime
    • Add a CONFIG_ to adjust the size of the memory to be reserverd
    • If the size is set to 0 (default), fall back and use only the __malloc_lock and __malloc_unlock
    • Area is defined as sys_mem_pool

Dependencies

  • Newlib build with --enable-newlib-retargetable-locking
  • Optional --enable-newlib-reent-small (AFAIK GNU ARM Embedded + Zephyr SDK have that set)

Concerns and Unresolved Questions

General

  • I am wondering why this hasn't been addessed already. Either simply nobody knew / cared, or I've taken a terribly wrong road somewhere.

Implementation

  • I haven’t dived into the Zephyr Kernel before, so I might have placed code at places where it doesnt belong. Is arch_swap the right place for setting up impure_ptr after context switch? (Done only for ARM until there’s some feedback.)
  • Carrying struct reent around within k_thread has obviously some impact on memory consumption, but I have no better idea (except saying "Well, use reent_small")
  • The non recursive mutex are implemented using a semaphore, is there something lightweight which could be used for the recursive mutex?
  • Haven't looked into errno - think there's a per thread errno within reent

Implementation

PR #21518

Relevant Links

Documentation

Newlib Documentation on __malloc_lock

Newlib Documentation on Reentrancy

Mailing Lists

Newlib discussion on --enable-newlib-retargetable-locking

[PATCH, newlib] Allow locking routine to be retargeted

What are the __retarget_lock functions?

Alternatives

Go the RTEMS - way and extend newlib / GCC with a port for Zephyr. That would also give us the opportunity to have OpenMP and a thread-safe libstdc++, which would be striking.

Metadata

Metadata

Assignees

Labels

area: C LibraryC Standard Libraryarea: newlibNewlib C Standard LibrarybugThe issue is a bug, or the PR is fixing a bugpriority: highHigh impact/importance bug

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions