Upstream SHA: c8d3e4134ee24f8c3bbc598dfc80520c5a0c46d5 Signed-off-by: Alberto Escolar Piedras <alberto.escolar.piedras@nordicsemi.no>
705 lines
21 KiB
C
705 lines
21 KiB
C
/*
|
|
* Copyright (c) 2017 Oticon A/S
|
|
* Copyright (c) 2023 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/*
|
|
* Native simulator, CPU Thread emulation (nct)
|
|
*/
|
|
|
|
/**
|
|
* Native simulator single CPU threading emulation,
|
|
* an *optional* module provided by the Native simulator
|
|
* the hosted embedded OS / SW can use to emulate the threading
|
|
* context switching which would be handled by a OS CPU AL
|
|
*
|
|
* Principle of operation:
|
|
*
|
|
* The embedded OS threads are run as a set of native Linux pthreads.
|
|
* The embedded OS only sees one of this thread executing at a time.
|
|
*
|
|
* The hosted OS shall call nct_init() to initialize the state of an
|
|
* instance of this module, and nct_clean_up() once it desires to destroy it.
|
|
*
|
|
* For SOCs with several micro-controllers (AMP) one instance of this module
|
|
* would be instantiated per simulated uC and embedded OS.
|
|
*
|
|
* To create a new embedded thread, the hosted OS shall call nct_new_thread().
|
|
* To swap to a thread nct_swap_threads(), and to terminate a thread
|
|
* nct_abort_thread().
|
|
* The hosted OS can optionally use nct_first_thread_start() to swap
|
|
* to the "first thread".
|
|
*
|
|
* Whenever a thread calls nct_swap_threads(next_thread_idx) it will be blocked,
|
|
* and the thread identified by next_thread_idx will continue executing.
|
|
*
|
|
*
|
|
* Internal design:
|
|
*
|
|
* Which thread is running is controlled using {cond|mtx}_threads and
|
|
* currently_allowed_thread.
|
|
*
|
|
* The main part of the execution of each thread will occur in a fully
|
|
* synchronous and deterministic manner, and only when commanded by
|
|
* the embedded operating system kernel.
|
|
*
|
|
* The creation of a thread will spawn a new pthread whose start
|
|
* is asynchronous to the rest, until synchronized in nct_wait_until_allowed()
|
|
* below.
|
|
* Similarly aborting and canceling threads execute a tail in a quite an
|
|
* asynchronous manner.
|
|
*
|
|
* This implementation is meant to be portable in between fully compatible
|
|
* POSIX systems.
|
|
* A table (threads_table) is used to abstract the native pthreads.
|
|
* An index in this table is used to identify threads in the IF to the
|
|
* embedded OS.
|
|
*/
|
|
|
|
#define NCT_DEBUG_PRINTS 0
|
|
|
|
#include <pthread.h>
|
|
#include <stdbool.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include "nct_if.h"
|
|
#include "nsi_internal.h"
|
|
#include "nsi_safe_call.h"
|
|
|
|
#if NCT_DEBUG_PRINTS
|
|
#define NCT_DEBUG(fmt, ...) nsi_print_trace(PREFIX fmt, __VA_ARGS__)
|
|
#else
|
|
#define NCT_DEBUG(...)
|
|
#endif
|
|
|
|
#define PREFIX "Tread Simulator: "
|
|
#define ERPREFIX PREFIX"error on "
|
|
#define NO_MEM_ERR PREFIX"Can't allocate memory\n"
|
|
|
|
#define NCT_ENABLE_CANCEL 0 /* See Note.c1 */
|
|
#define NCT_ALLOC_CHUNK_SIZE 64 /* In how big chunks we grow the thread table */
|
|
#define NCT_REUSE_ABORTED_ENTRIES 0
|
|
/* For the Zephyr OS, tests/kernel/threads/scheduling/schedule_api fails when setting
|
|
* NCT_REUSE_ABORTED_ENTRIES => don't set it by now
|
|
*/
|
|
|
|
struct te_status_t;
|
|
|
|
struct threads_table_el {
|
|
/* Pointer to the overall status of the threading emulator instance */
|
|
struct te_status_t *ts_status;
|
|
struct threads_table_el *next; /* Pointer to the next element of the table */
|
|
int thread_idx; /* Index of this element in the threads_table*/
|
|
|
|
enum {NOTUSED = 0, USED, ABORTING, ABORTED, FAILED} state;
|
|
bool running; /* Is this the currently running thread */
|
|
pthread_t thread; /* Actual pthread_t as returned by the native kernel */
|
|
int thead_cnt; /* For debugging: Unique, consecutive, thread number */
|
|
|
|
/*
|
|
* Pointer to data from the hosted OS architecture
|
|
* What that is, if anything, is up to that the hosted OS
|
|
*/
|
|
void *payload;
|
|
};
|
|
|
|
struct te_status_t {
|
|
struct threads_table_el *threads_table; /* Pointer to the threads table */
|
|
int thread_create_count; /* (For debugging) Thread creation counter */
|
|
int threads_table_size; /* Size of threads_table */
|
|
/* Pointer to the hosted OS function to be called when a thread is started */
|
|
void (*fptr)(void *payload);
|
|
/*
|
|
* Conditional variable to block/awake all threads during swaps.
|
|
* (we only need 1 mutex and 1 cond variable for all threads)
|
|
*/
|
|
pthread_cond_t cond_threads;
|
|
/* Mutex for the conditional variable cond_threads */
|
|
pthread_mutex_t mtx_threads;
|
|
/* Token which tells which thread is allowed to run now */
|
|
int currently_allowed_thread;
|
|
bool terminate; /* Are we terminating the program == cleaning up */
|
|
};
|
|
|
|
static void nct_exit_and_cleanup(struct te_status_t *this);
|
|
static struct threads_table_el *ttable_get_element(struct te_status_t *this, int index);
|
|
|
|
/**
|
|
* Helper function, run by a thread which is being aborted
|
|
*/
|
|
static void abort_tail(struct te_status_t *this, int this_th_nbr)
|
|
{
|
|
struct threads_table_el *tt_el = ttable_get_element(this, this_th_nbr);
|
|
|
|
NCT_DEBUG("Thread [%i] %i: %s: Aborting (exiting) (rel mut)\n",
|
|
tt_el->thead_cnt,
|
|
this_th_nbr,
|
|
__func__);
|
|
|
|
tt_el->running = false;
|
|
tt_el->state = ABORTED;
|
|
nct_exit_and_cleanup(this);
|
|
}
|
|
|
|
/**
|
|
* Helper function to block this thread until it is allowed again
|
|
*
|
|
* Note that we go out of this function (the while loop below)
|
|
* with the mutex locked by this particular thread.
|
|
* In normal circumstances, the mutex is only unlocked internally in
|
|
* pthread_cond_wait() while waiting for cond_threads to be signaled
|
|
*/
|
|
static void nct_wait_until_allowed(struct te_status_t *this, int this_th_nbr)
|
|
{
|
|
struct threads_table_el *tt_el = ttable_get_element(this, this_th_nbr);
|
|
|
|
tt_el->running = false;
|
|
|
|
NCT_DEBUG("Thread [%i] %i: %s: Waiting to be allowed to run (rel mut)\n",
|
|
tt_el->thead_cnt,
|
|
this_th_nbr,
|
|
__func__);
|
|
|
|
while (this_th_nbr != this->currently_allowed_thread) {
|
|
pthread_cond_wait(&this->cond_threads, &this->mtx_threads);
|
|
|
|
if (tt_el->state == ABORTING) {
|
|
abort_tail(this, this_th_nbr);
|
|
}
|
|
}
|
|
|
|
tt_el->running = true;
|
|
|
|
NCT_DEBUG("Thread [%i] %i: %s(): I'm allowed to run! (hav mut)\n",
|
|
tt_el->thead_cnt,
|
|
this_th_nbr,
|
|
__func__);
|
|
}
|
|
|
|
/**
|
|
* Helper function to let the thread <next_allowed_th> run
|
|
*
|
|
* Note: nct_let_run() can only be called with the mutex locked
|
|
*/
|
|
static void nct_let_run(struct te_status_t *this, int next_allowed_th)
|
|
{
|
|
#if NCT_DEBUG_PRINTS
|
|
struct threads_table_el *tt_el = ttable_get_element(this, next_allowed_th);
|
|
|
|
NCT_DEBUG("%s: We let thread [%i] %i run\n",
|
|
__func__,
|
|
tt_el->thead_cnt,
|
|
next_allowed_th);
|
|
#endif
|
|
|
|
this->currently_allowed_thread = next_allowed_th;
|
|
|
|
/*
|
|
* We let all threads know one is able to run now (it may even be us
|
|
* again if fancied)
|
|
* Note that as we hold the mutex, they are going to be blocked until
|
|
* we reach our own nct_wait_until_allowed() while loop or abort_tail()
|
|
* mutex release
|
|
*/
|
|
NSI_SAFE_CALL(pthread_cond_broadcast(&this->cond_threads));
|
|
}
|
|
|
|
/**
|
|
* Helper function, run by a thread which is being ended
|
|
*/
|
|
static void nct_exit_and_cleanup(struct te_status_t *this)
|
|
{
|
|
/*
|
|
* Release the mutex so the next allowed thread can run
|
|
*/
|
|
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_threads));
|
|
|
|
/* We detach ourselves so nobody needs to join to us */
|
|
pthread_detach(pthread_self());
|
|
|
|
pthread_exit(NULL);
|
|
}
|
|
|
|
/**
|
|
* Let the ready thread run and block this managed thread until it is allowed again
|
|
*
|
|
* The hosted OS shall call this when it has decided to swap in/out two of its threads,
|
|
* from the thread that is being swapped out.
|
|
*
|
|
* Note: If called without having ever let another managed thread run / from a thread not
|
|
* managed by this nct instance, it will behave like nct_first_thread_start(),
|
|
* and terminate the calling thread while letting the managed thread
|
|
* <next_allowed_thread_nbr> continue.
|
|
*
|
|
* inputs:
|
|
* this_arg: Pointer to this thread emulator instance as returned by nct_init()
|
|
* next_allowed_thread_nbr: Identifier of the thread the hosted OS wants to swap in
|
|
*/
|
|
void nct_swap_threads(void *this_arg, int next_allowed_thread_nbr)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
int this_th_nbr = this->currently_allowed_thread;
|
|
|
|
nct_let_run(this, next_allowed_thread_nbr);
|
|
|
|
if (this_th_nbr == -1) { /* This is the first time a thread was swapped in */
|
|
NCT_DEBUG("%s: called from an unmanaged thread, terminating it\n",
|
|
__func__);
|
|
nct_exit_and_cleanup(this);
|
|
}
|
|
|
|
struct threads_table_el *tt_el = ttable_get_element(this, this_th_nbr);
|
|
|
|
if (tt_el->state == ABORTING) {
|
|
NCT_DEBUG("Thread [%i] %i: %s: Aborting curr.\n",
|
|
tt_el->thead_cnt,
|
|
this_th_nbr,
|
|
__func__);
|
|
abort_tail(this, this_th_nbr);
|
|
} else {
|
|
nct_wait_until_allowed(this, this_th_nbr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Let the very first hosted thread run, and exit this thread.
|
|
*
|
|
* The hosted OS shall call this when it has decided to swap in into another
|
|
* thread, and wants to terminate the currently executing thread, which is not
|
|
* a thread managed by the thread emulator.
|
|
*
|
|
* This function allows to emulate a hosted OS doing its first swapping into one
|
|
* of its hosted threads from the init thread, abandoning/terminating the init
|
|
* thread.
|
|
*/
|
|
void nct_first_thread_start(void *this_arg, int next_allowed_thread_nbr)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
|
|
nct_let_run(this, next_allowed_thread_nbr);
|
|
NCT_DEBUG("%s: Init thread dying now (rel mut)\n",
|
|
__func__);
|
|
nct_exit_and_cleanup(this);
|
|
}
|
|
|
|
/**
|
|
* Handler called when any thread is cancelled or exits
|
|
*/
|
|
static void nct_cleanup_handler(void *arg)
|
|
{
|
|
struct threads_table_el *element = (struct threads_table_el *)arg;
|
|
struct te_status_t *this = element->ts_status;
|
|
|
|
/*
|
|
* If we are not terminating, this is just an aborted thread,
|
|
* and the mutex was already released
|
|
* Otherwise, release the mutex so other threads which may be
|
|
* caught waiting for it could terminate
|
|
*/
|
|
|
|
if (!this->terminate) {
|
|
return;
|
|
}
|
|
|
|
NCT_DEBUG("Thread %i: %s: Canceling (rel mut)\n",
|
|
element->thread_idx,
|
|
__func__);
|
|
|
|
|
|
NSI_SAFE_CALL(pthread_mutex_unlock(&this->mtx_threads));
|
|
|
|
/* We detach ourselves so nobody needs to join to us */
|
|
pthread_detach(pthread_self());
|
|
}
|
|
|
|
/**
|
|
* Helper function to start a hosted thread as a POSIX thread:
|
|
* It will block the pthread until the embedded OS devices to "swap in"
|
|
* this thread.
|
|
*/
|
|
static void *nct_thread_starter(void *arg_el)
|
|
{
|
|
struct threads_table_el *tt_el = (struct threads_table_el *)arg_el;
|
|
struct te_status_t *this = tt_el->ts_status;
|
|
|
|
int thread_idx = tt_el->thread_idx;
|
|
|
|
NCT_DEBUG("Thread [%i] %i: %s: Starting\n",
|
|
tt_el->thead_cnt,
|
|
thread_idx,
|
|
__func__);
|
|
|
|
/*
|
|
* We block until all other running threads reach the while loop
|
|
* in nct_wait_until_allowed() and they release the mutex
|
|
*/
|
|
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_threads));
|
|
|
|
/*
|
|
* The program may have been finished before this thread ever got to run
|
|
*/
|
|
/* LCOV_EXCL_START */ /* See Note1 */
|
|
if (!this->threads_table || this->terminate) {
|
|
nct_cleanup_handler(arg_el);
|
|
pthread_exit(NULL);
|
|
}
|
|
/* LCOV_EXCL_STOP */
|
|
|
|
pthread_cleanup_push(nct_cleanup_handler, arg_el);
|
|
|
|
NCT_DEBUG("Thread [%i] %i: %s: After start mutex (hav mut)\n",
|
|
tt_el->thead_cnt,
|
|
thread_idx,
|
|
__func__);
|
|
|
|
/*
|
|
* The thread would try to execute immediately, so we block it
|
|
* until allowed
|
|
*/
|
|
nct_wait_until_allowed(this, thread_idx);
|
|
|
|
this->fptr(tt_el->payload);
|
|
|
|
/*
|
|
* We only reach this point if the thread actually returns which should
|
|
* not happen. But we handle it gracefully just in case
|
|
*/
|
|
/* LCOV_EXCL_START */
|
|
nsi_print_trace(PREFIX"Thread [%i] %i [%lu] ended!?!\n",
|
|
tt_el->thead_cnt,
|
|
thread_idx,
|
|
pthread_self());
|
|
|
|
tt_el->running = false;
|
|
tt_el->state = FAILED;
|
|
|
|
pthread_cleanup_pop(1);
|
|
|
|
return NULL;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
static struct threads_table_el *ttable_get_element(struct te_status_t *this, int index)
|
|
{
|
|
struct threads_table_el *threads_table = this->threads_table;
|
|
|
|
if (index >= this->threads_table_size) { /* LCOV_EXCL_BR_LINE */
|
|
nsi_print_error_and_exit("%s: Programming error, attempted out of bound access to "
|
|
"thread table (%i>=%i)\n",
|
|
index, this->threads_table_size); /* LCOV_EXCL_LINE */
|
|
}
|
|
while (index >= NCT_ALLOC_CHUNK_SIZE) {
|
|
index -= NCT_ALLOC_CHUNK_SIZE;
|
|
threads_table = threads_table[NCT_ALLOC_CHUNK_SIZE - 1].next;
|
|
}
|
|
return &threads_table[index];
|
|
}
|
|
|
|
/**
|
|
* Return the first free entry index in the threads table
|
|
*/
|
|
static int ttable_get_empty_slot(struct te_status_t *this)
|
|
{
|
|
struct threads_table_el *tt_el = this->threads_table;
|
|
|
|
for (int i = 0; i < this->threads_table_size; i++, tt_el = tt_el->next) {
|
|
if ((tt_el->state == NOTUSED)
|
|
|| (NCT_REUSE_ABORTED_ENTRIES
|
|
&& (tt_el->state == ABORTED))) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* else, we run out of table without finding an index
|
|
* => we expand the table
|
|
*/
|
|
|
|
struct threads_table_el *new_chunk;
|
|
|
|
new_chunk = calloc(NCT_ALLOC_CHUNK_SIZE, sizeof(struct threads_table_el));
|
|
if (new_chunk == NULL) { /* LCOV_EXCL_BR_LINE */
|
|
nsi_print_error_and_exit(NO_MEM_ERR); /* LCOV_EXCL_LINE */
|
|
}
|
|
|
|
/* Link new chunk to last element */
|
|
tt_el = ttable_get_element(this, this->threads_table_size-1);
|
|
tt_el->next = new_chunk;
|
|
|
|
this->threads_table_size += NCT_ALLOC_CHUNK_SIZE;
|
|
|
|
/* Link all new elements together */
|
|
for (int i = 0 ; i < NCT_ALLOC_CHUNK_SIZE - 1; i++) {
|
|
new_chunk[i].next = &new_chunk[i+1];
|
|
}
|
|
new_chunk[NCT_ALLOC_CHUNK_SIZE - 1].next = NULL;
|
|
|
|
/* The first newly created entry is good, we return it */
|
|
return this->threads_table_size - NCT_ALLOC_CHUNK_SIZE;
|
|
}
|
|
|
|
/**
|
|
* Create a new pthread for the new hosted OS thread.
|
|
*
|
|
* Returns a unique integer thread identifier/index, which should be used
|
|
* to refer to this thread for future calls to the thread emulator.
|
|
*
|
|
* It takes as parameter a pointer which will be passed to
|
|
* function registered in nct_init when the thread is swapped in.
|
|
*
|
|
* Note that the thread is created but not swapped in.
|
|
* The new thread execution will be held until nct_swap_threads()
|
|
* (or nct_first_thread_start()) is called with this newly created
|
|
* thread number.
|
|
*/
|
|
int nct_new_thread(void *this_arg, void *payload)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
struct threads_table_el *tt_el;
|
|
int t_slot;
|
|
|
|
t_slot = ttable_get_empty_slot(this);
|
|
tt_el = ttable_get_element(this, t_slot);
|
|
|
|
tt_el->state = USED;
|
|
tt_el->running = false;
|
|
tt_el->thead_cnt = this->thread_create_count++;
|
|
tt_el->payload = payload;
|
|
tt_el->ts_status = this;
|
|
tt_el->thread_idx = t_slot;
|
|
|
|
NSI_SAFE_CALL(pthread_create(&tt_el->thread,
|
|
NULL,
|
|
nct_thread_starter,
|
|
(void *)tt_el));
|
|
|
|
NCT_DEBUG("%s created thread [%i] %i [%lu]\n",
|
|
__func__,
|
|
tt_el->thead_cnt,
|
|
t_slot,
|
|
tt_el->thread);
|
|
|
|
return t_slot;
|
|
}
|
|
|
|
/**
|
|
* Initialize an instance of the threading emulator.
|
|
*
|
|
* Returns a pointer to the initialize threading emulator instance.
|
|
* This pointer shall be passed to all subsequent calls of the
|
|
* threading emulator when interacting with this particular instance.
|
|
*
|
|
* The input fptr is a pointer to the hosted OS function
|
|
* to be called each time a thread which is created on its request
|
|
* with nct_new_thread() is swapped in (from that thread context)
|
|
*/
|
|
void *nct_init(void (*fptr)(void *))
|
|
{
|
|
struct te_status_t *this;
|
|
|
|
/*
|
|
* Note: This (and the calloc below) won't be free'd by this code
|
|
* but left for the OS to clear at process end.
|
|
* This is a conscious choice, see nct_clean_up() for more info.
|
|
* If you got here due to valgrind's leak report, please use the
|
|
* provided valgrind suppression file valgrind.supp
|
|
*/
|
|
this = calloc(1, sizeof(struct te_status_t));
|
|
if (this == NULL) { /* LCOV_EXCL_BR_LINE */
|
|
nsi_print_error_and_exit(NO_MEM_ERR); /* LCOV_EXCL_LINE */
|
|
}
|
|
|
|
this->fptr = fptr;
|
|
this->thread_create_count = 0;
|
|
this->currently_allowed_thread = -1;
|
|
|
|
NSI_SAFE_CALL(pthread_cond_init(&this->cond_threads, NULL));
|
|
NSI_SAFE_CALL(pthread_mutex_init(&this->mtx_threads, NULL));
|
|
|
|
this->threads_table = calloc(NCT_ALLOC_CHUNK_SIZE,
|
|
sizeof(struct threads_table_el));
|
|
if (this->threads_table == NULL) { /* LCOV_EXCL_BR_LINE */
|
|
nsi_print_error_and_exit(NO_MEM_ERR); /* LCOV_EXCL_LINE */
|
|
}
|
|
|
|
this->threads_table_size = NCT_ALLOC_CHUNK_SIZE;
|
|
|
|
for (int i = 0 ; i < NCT_ALLOC_CHUNK_SIZE - 1; i++) {
|
|
this->threads_table[i].next = &this->threads_table[i+1];
|
|
}
|
|
this->threads_table[NCT_ALLOC_CHUNK_SIZE - 1].next = NULL;
|
|
|
|
NSI_SAFE_CALL(pthread_mutex_lock(&this->mtx_threads));
|
|
|
|
return (void *)this;
|
|
}
|
|
|
|
/**
|
|
* Free any allocated memory by the threading emulator and clean up.
|
|
* Note that this function cannot be called from a SW thread
|
|
* (the CPU is assumed halted. Otherwise we would cancel ourselves)
|
|
*
|
|
* Note: This function cannot guarantee the threads will be cancelled before the HW
|
|
* thread exists. The only way to do that, would be to wait for each of them in
|
|
* a join without detaching them, but that could lead to locks in some
|
|
* convoluted cases; as a call to this function can come due to a hosted OS
|
|
* assert or other error termination, we better do not assume things are working fine.
|
|
* => we prefer the supposed memory leak report from valgrind, and ensure we
|
|
* will not hang.
|
|
*/
|
|
void nct_clean_up(void *this_arg)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
|
|
if (!this || !this->threads_table) { /* LCOV_EXCL_BR_LINE */
|
|
return; /* LCOV_EXCL_LINE */
|
|
}
|
|
|
|
this->terminate = true;
|
|
|
|
#if (NCT_ENABLE_CANCEL)
|
|
struct threads_table_el *tt_el = this->threads_table;
|
|
|
|
for (int i = 0; i < this->threads_table_size; i++, tt_el = tt_el->next) {
|
|
if (tt_el->state != USED) {
|
|
continue;
|
|
}
|
|
|
|
/* LCOV_EXCL_START */
|
|
if (pthread_cancel(tt_el->thread)) {
|
|
nsi_print_warning(
|
|
PREFIX"cleanup: could not stop thread %i\n",
|
|
i);
|
|
}
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
#endif
|
|
/*
|
|
* This is the cleanup we do not do:
|
|
*
|
|
* free(this->threads_table);
|
|
* Including all chunks
|
|
* this->threads_table = NULL;
|
|
*
|
|
* (void)pthread_cond_destroy(&this->cond_threads);
|
|
* (void)pthread_mutex_destroy(&this->mtx_threads);
|
|
*
|
|
* free(this);
|
|
*/
|
|
}
|
|
|
|
|
|
/*
|
|
* Mark a thread as being aborted. This will result in the underlying pthread
|
|
* being terminated some time later:
|
|
* If the thread is marking itself as aborting, as soon as it is swapped out
|
|
* by the hosted (embedded) OS
|
|
* If it is marking another thread, at some non-specific time in the future
|
|
* (But note that no embedded part of the aborted thread will execute anymore)
|
|
*
|
|
* * thread_idx : The thread identifier as provided during creation (return from nct_new_thread())
|
|
*/
|
|
void nct_abort_thread(void *this_arg, int thread_idx)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
struct threads_table_el *tt_el = ttable_get_element(this, thread_idx);
|
|
|
|
if (thread_idx == this->currently_allowed_thread) {
|
|
NCT_DEBUG("Thread [%i] %i: %s Marked myself "
|
|
"as aborting\n",
|
|
tt_el->thead_cnt,
|
|
thread_idx,
|
|
__func__);
|
|
} else {
|
|
if (tt_el->state != USED) { /* LCOV_EXCL_BR_LINE */
|
|
/* The thread may have been already aborted before */
|
|
return; /* LCOV_EXCL_LINE */
|
|
}
|
|
|
|
NCT_DEBUG("Aborting not scheduled thread [%i] %i\n",
|
|
tt_el->thead_cnt,
|
|
thread_idx);
|
|
}
|
|
tt_el->state = ABORTING;
|
|
/*
|
|
* Note: the native thread will linger in RAM until it catches the
|
|
* mutex or awakes on the condition.
|
|
* Note that even if we would pthread_cancel() the thread here, that
|
|
* would be the case, but with a pthread_cancel() the mutex state would
|
|
* be uncontrolled
|
|
*/
|
|
}
|
|
|
|
/*
|
|
* Return a unique thread identifier for this thread for this
|
|
* run. This identifier is only meant for debug purposes
|
|
*
|
|
* thread_idx is the value returned by nct_new_thread()
|
|
*/
|
|
int nct_get_unique_thread_id(void *this_arg, int thread_idx)
|
|
{
|
|
struct te_status_t *this = (struct te_status_t *)this_arg;
|
|
struct threads_table_el *tt_el = ttable_get_element(this, thread_idx);
|
|
|
|
return tt_el->thead_cnt;
|
|
}
|
|
|
|
/*
|
|
* Notes about coverage:
|
|
*
|
|
* Note1:
|
|
*
|
|
* This condition will only be triggered in very unlikely cases
|
|
* (once every few full regression runs).
|
|
* It is therefore excluded from the coverage report to avoid confusing
|
|
* developers.
|
|
*
|
|
* Background: A pthread is created as soon as the hosted kernel creates
|
|
* a hosted thread. A pthread creation is an asynchronous process handled by the
|
|
* host kernel.
|
|
*
|
|
* This emulator normally keeps only 1 thread executing at a time.
|
|
* But part of the pre-initialization during creation of a new thread
|
|
* and some cleanup at the tail of the thread termination are executed
|
|
* in parallel to other threads.
|
|
* That is, the execution of those code paths is a bit indeterministic.
|
|
*
|
|
* Only when the hosted kernel attempts to swap to a new thread does this
|
|
* emulator need to wait until its pthread is ready and initialized
|
|
* (has reached nct_wait_until_allowed())
|
|
*
|
|
* In some cases (tests) hosted threads are created which are never actually needed
|
|
* (typically the idle thread). That means the test may finish before that
|
|
* thread's underlying pthread has reached nct_wait_until_allowed().
|
|
*
|
|
* In this unlikely cases the initialization or cleanup of the thread follows
|
|
* non-typical code paths.
|
|
* This code paths are there to ensure things work always, no matter
|
|
* the load of the host. Without them, very rare & mysterious segfault crashes
|
|
* would occur.
|
|
* But as they are very atypical and only triggered with some host loads,
|
|
* they will be covered in the coverage reports only rarely.
|
|
*
|
|
* Note2:
|
|
*
|
|
* Some other code will never or only very rarely trigger and is therefore
|
|
* excluded with LCOV_EXCL_LINE
|
|
*
|
|
*
|
|
* Notes about (memory) cleanup:
|
|
*
|
|
* Note.c1:
|
|
*
|
|
* In some very rare cases in very loaded machines, a race in the glibc pthread_cancel()
|
|
* seems to be triggered.
|
|
* In this, the cancelled thread cleanup overtakes the pthread_cancel() code, and frees the
|
|
* pthread structure before pthread_cancel() has finished, resulting in a dereference into already
|
|
* free'd memory, and therefore a segfault.
|
|
* Calling pthread_cancel() during cleanup is not required beyond preventing a valgrind
|
|
* memory leak report (all threads will be canceled immediately on exit).
|
|
* Therefore we do not do this, to avoid this very rare crashes.
|
|
*/
|