Merge patch series "Uthreads"

Jerome Forissier <jerome.forissier@linaro.org> says:

This series introduces threads and uses them to improve the performance
of the USB bus scanning code and to implement background jobs in the
shell via two new commands: 'spawn' and 'wait'.

The threading framework is called 'uthread' and is inspired from the
barebox threads [2]. setjmp() and longjmp() are used to save and
restore contexts, as well as a non-standard extension called initjmp().
This new function is added in several patches, one for each
architecture that supports HAVE_SETJMP. A new symbol is defined:
HAVE_INITJMP. Two tests, one for initjmp() and one for the uthread
scheduling, are added to the lib suite.

After introducing threads and making schedule() and udelay() a thread
re-scheduling point, the USB stack initialization is modified to benefit
from concurrency when UTHREAD is enabled, where uthreads are used in
usb_init() to initialize and scan multiple busses at the same time.
The code was tested on arm64 and arm QEMU with 4 simulated XHCI buses
and some devices. On this platform the USB scan takes 2.2 s instead of
5.6 s. Tested on i.MX93 EVK with two USB hubs, one ethernet adapter and
one webcam on each, "usb start" takes 2.4 s instead of 4.6 s.

Finally, the spawn and wait commands are introduced, allowing the use of
threads from the shell. Tested on the i.MX93 EVK with a spinning HDD
connected to USB1 and the network connected to ENET1. The USB plus DHCP
init sequence "spawn usb start; spawn dhcp; wait" takes 4.5 seconds
instead of 8 seconds for "usb start; dhcp".

[1] https://patchwork.ozlabs.org/project/uboot/list/?series=446674
[2] https://github.com/barebox/barebox/blob/master/common/bthread.c

Link: https://lore.kernel.org/r/20250418141114.2056981-1-jerome.forissier@linaro.org
This commit is contained in:
Tom Rini
2025-04-23 13:21:39 -06:00
38 changed files with 1317 additions and 63 deletions

View File

@@ -1825,6 +1825,15 @@ T: git https://source.denx.de/u-boot/custodians/u-boot-usb.git topic-xhci
F: drivers/usb/host/xhci*
F: include/usb/xhci.h
UTHREAD
M: Jerome Forissier <jerome.forissier@linaro.org>
S: Maintained
F: cmd/spawn.c
F: include/uthread.h
F: lib/uthread.c
F: test/cmd/spawn.c
F: test/lib/uthread.c
UUID testing
M: Abdellatif El Khlifi <abdellatif.elkhlifi@arm.com>
S: Maintained

View File

@@ -13,6 +13,13 @@ config HAVE_SETJMP
help
The architecture supports setjmp() and longjmp().
config HAVE_INITJMP
bool
depends on HAVE_SETJMP
help
The architecture supports initjmp(), a non-standard companion to
setjmp() and longjmp().
config SUPPORT_BIG_ENDIAN
bool
@@ -88,6 +95,7 @@ config ARC
config ARM
bool "ARM architecture"
select HAVE_SETJMP
select HAVE_INITJMP
select ARCH_SUPPORTS_LTO
select CREATE_ARCH_SYMLINK
select HAVE_PRIVATE_LIBGCC if !ARM64
@@ -145,6 +153,7 @@ config RISCV
bool "RISC-V architecture"
select CREATE_ARCH_SYMLINK
select HAVE_SETJMP
select HAVE_INITJMP
select SUPPORT_ACPI
select SUPPORT_LITTLE_ENDIAN
select SUPPORT_OF_CONTROL
@@ -171,6 +180,7 @@ config RISCV
config SANDBOX
bool "Sandbox"
select HAVE_SETJMP
select HAVE_INITJMP
select ARCH_SUPPORTS_LTO
select BOARD_LATE_INIT
select BZIP2

View File

@@ -34,3 +34,15 @@ ENTRY(longjmp)
ret lr
ENDPROC(longjmp)
.popsection
.pushsection .text.initjmp, "ax"
ENTRY(initjmp)
stm a1, {v1-v8}
/* a2: entry point address, a3: stack base, a4: stack size */
add a3, a3, a4
str a3, [a1, #32] /* where setjmp would save sp */
str a2, [a1, #36] /* where setjmp would save lr */
mov a1, #0
ret lr
ENDPROC(initjmp)
.popsection

View File

@@ -39,3 +39,13 @@ ENTRY(longjmp)
ret
ENDPROC(longjmp)
.popsection
.pushsection .text.initjmp, "ax"
ENTRY(initjmp)
/* x1: entry point address, x2: stack base, x3: stack size */
add x2, x2, x3
stp x1, x2, [x0,#88]
mov x0, #0
ret
ENDPROC(initjmp)
.popsection

View File

@@ -59,3 +59,14 @@ ENTRY(longjmp)
ret
ENDPROC(longjmp)
.popsection
.pushsection .text.initjmp, "ax"
ENTRY(initjmp)
/* a1: entry point address, a2: stack base, a3: stack size */
add a2, a2, a3
STORE_IDX(a1, 12)
STORE_IDX(a2, 13)
li a0, 0
ret
ENDPROC(initjmp)
.popsection

View File

@@ -5,7 +5,7 @@
# (C) Copyright 2000-2003
# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
obj-y := cache.o cpu.o state.o
obj-y := cache.o cpu.o state.o initjmp.o
extra-y := start.o os.o
extra-$(CONFIG_SANDBOX_SDL) += sdl.o
obj-$(CONFIG_XPL_BUILD) += spl.o
@@ -29,6 +29,15 @@ cmd_cc_eth-raw-os.o = $(CC) $(filter-out -nostdinc, \
$(obj)/eth-raw-os.o: $(src)/eth-raw-os.c FORCE
$(call if_changed_dep,cc_eth-raw-os.o)
# initjmp.c is build in the system environment, so needs standard includes
# CFLAGS_REMOVE_initjmp.o cannot be used to drop header include path
quiet_cmd_cc_initjmp.o = CC $(quiet_modtag) $@
cmd_cc_initjmp.o = $(CC) $(filter-out -nostdinc, \
$(patsubst -I%,-idirafter%,$(c_flags))) -c -o $@ $<
$(obj)/initjmp.o: $(src)/initjmp.c FORCE
$(call if_changed_dep,cc_initjmp.o)
# sdl.c fails to build with -fshort-wchar using musl
cmd_cc_sdl.o = $(CC) $(filter-out -nostdinc -fshort-wchar, \
$(patsubst -I%,-idirafter%,$(c_flags))) -fno-lto -c -o $@ $<

175
arch/sandbox/cpu/initjmp.c Normal file
View File

@@ -0,0 +1,175 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/*
* An implementation of initjmp() in C, that plays well with the system's
* setjmp() and longjmp() functions.
* Taken verbatim from arch/sandbox/os/setjmp.c in the barebox project.
* Modified so that initjmp() accepts a stack_size argument.
*
* Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
* Copyright (C) 2011 Kevin Wolf <kwolf@redhat.com>
* Copyright (C) 2012 Alex Barcelo <abarcelo@ac.upc.edu>
* Copyright (C) 2021 Ahmad Fatoum, Pengutronix
* Copyright (C) 2025 Linaro Ltd.
* This file is partly based on pth_mctx.c, from the GNU Portable Threads
* Copyright (c) 1999-2006 Ralf S. Engelschall <rse@engelschall.com>
*/
/* XXX Is there a nicer way to disable glibc's stack check for longjmp? */
#ifdef _FORTIFY_SOURCE
#undef _FORTIFY_SOURCE
#endif
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <signal.h>
typedef sigjmp_buf _jmp_buf __attribute__((aligned((16))));
_Static_assert(sizeof(_jmp_buf) <= 512, "sigjmp_buf size exceeds expectation");
/*
* Information for the signal handler (trampoline)
*/
static struct {
_jmp_buf *reenter;
void (*entry)(void);
volatile sig_atomic_t called;
} tr_state;
/*
* "boot" function
* This is what starts the coroutine, is called from the trampoline
* (from the signal handler when it is not signal handling, read ahead
* for more information).
*/
static void __attribute__((noinline, noreturn))
coroutine_bootstrap(void (*entry)(void))
{
for (;;)
entry();
}
/*
* This is used as the signal handler. This is called with the brand new stack
* (thanks to sigaltstack). We have to return, given that this is a signal
* handler and the sigmask and some other things are changed.
*/
static void coroutine_trampoline(int signal)
{
/* Get the thread specific information */
tr_state.called = 1;
/*
* Here we have to do a bit of a ping pong between the caller, given that
* this is a signal handler and we have to do a return "soon". Then the
* caller can reestablish everything and do a siglongjmp here again.
*/
if (!sigsetjmp(*tr_state.reenter, 0)) {
return;
}
/*
* Ok, the caller has siglongjmp'ed back to us, so now prepare
* us for the real machine state switching. We have to jump
* into another function here to get a new stack context for
* the auto variables (which have to be auto-variables
* because the start of the thread happens later). Else with
* PIC (i.e. Position Independent Code which is used when PTH
* is built as a shared library) most platforms would
* horrible core dump as experience showed.
*/
coroutine_bootstrap(tr_state.entry);
}
int __attribute__((weak)) initjmp(_jmp_buf jmp, void (*func)(void),
void *stack_base, size_t stack_size)
{
struct sigaction sa;
struct sigaction osa;
stack_t ss;
stack_t oss;
sigset_t sigs;
sigset_t osigs;
/* The way to manipulate stack is with the sigaltstack function. We
* prepare a stack, with it delivering a signal to ourselves and then
* put sigsetjmp/siglongjmp where needed.
* This has been done keeping coroutine-ucontext (from the QEMU project)
* as a model and with the pth ideas (GNU Portable Threads).
* See coroutine-ucontext for the basics of the coroutines and see
* pth_mctx.c (from the pth project) for the
* sigaltstack way of manipulating stacks.
*/
tr_state.entry = func;
tr_state.reenter = (void *)jmp;
/*
* Preserve the SIGUSR2 signal state, block SIGUSR2,
* and establish our signal handler. The signal will
* later transfer control onto the signal stack.
*/
sigemptyset(&sigs);
sigaddset(&sigs, SIGUSR2);
pthread_sigmask(SIG_BLOCK, &sigs, &osigs);
sa.sa_handler = coroutine_trampoline;
sigfillset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
if (sigaction(SIGUSR2, &sa, &osa) != 0) {
return -1;
}
/*
* Set the new stack.
*/
ss.ss_sp = stack_base;
ss.ss_size = stack_size;
ss.ss_flags = 0;
if (sigaltstack(&ss, &oss) < 0) {
return -1;
}
/*
* Now transfer control onto the signal stack and set it up.
* It will return immediately via "return" after the sigsetjmp()
* was performed. Be careful here with race conditions. The
* signal can be delivered the first time sigsuspend() is
* called.
*/
tr_state.called = 0;
pthread_kill(pthread_self(), SIGUSR2);
sigfillset(&sigs);
sigdelset(&sigs, SIGUSR2);
while (!tr_state.called) {
sigsuspend(&sigs);
}
/*
* Inform the system that we are back off the signal stack by
* removing the alternative signal stack. Be careful here: It
* first has to be disabled, before it can be removed.
*/
sigaltstack(NULL, &ss);
ss.ss_flags = SS_DISABLE;
if (sigaltstack(&ss, NULL) < 0) {
return -1;
}
sigaltstack(NULL, &ss);
if (!(oss.ss_flags & SS_DISABLE)) {
sigaltstack(&oss, NULL);
}
/*
* Restore the old SIGUSR2 signal handler and mask
*/
sigaction(SIGUSR2, &osa, NULL);
pthread_sigmask(SIG_SETMASK, &osigs, NULL);
/*
* jmp can now be used to enter the trampoline again, but not as a
* signal handler. Instead it's longjmp'd to directly.
*/
return 0;
}

View File

@@ -3081,4 +3081,21 @@ config CMD_MESON
help
Enable useful commands for the Meson Soc family developed by Amlogic Inc.
config CMD_SPAWN
bool "spawn and wait commands"
depends on UTHREAD
help
spawn runs a command in the background and sets the job_id environment
variable. wait is used to suspend the shell execution until one or more
jobs are complete.
config CMD_SPAWN_NUM_JOBS
int "Maximum number of simultaneous jobs for spawn"
default 16
help
Job identifiers are in the range 1..CMD_SPAWN_NUM_JOBS. In other words
there can be no more that CMD_SPAWN_NUM_JOBS running simultaneously.
When a jobs exits, its identifier is available to be re-used by the next
spawn command.
endif

View File

@@ -239,6 +239,8 @@ obj-$(CONFIG_CMD_SCP03) += scp03.o
obj-$(CONFIG_HUSH_SELECTABLE) += cli.o
obj-$(CONFIG_CMD_SPAWN) += spawn.o
obj-$(CONFIG_ARM) += arm/
obj-$(CONFIG_RISCV) += riscv/
obj-$(CONFIG_SANDBOX) += sandbox/

187
cmd/spawn.c Normal file
View File

@@ -0,0 +1,187 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright (c) 2011 The Chromium OS Authors.
*/
#include <command.h>
#include <console.h>
#include <malloc.h>
#include <vsprintf.h>
#include <uthread.h>
/* Spawn arguments and job index */
struct spa {
int argc;
char **argv;
unsigned int job_idx;
};
/*
* uthread group identifiers for each running job
* 0: job slot available, != 0: uthread group id
* Note that job[0] is job_id 1, job[1] is job_id 2 etc.
*/
static unsigned int job[CONFIG_CMD_SPAWN_NUM_JOBS];
/* Return values of the commands run as jobs */
static enum command_ret_t job_ret[CONFIG_CMD_SPAWN_NUM_JOBS];
static void spa_free(struct spa *spa)
{
int i;
if (!spa)
return;
for (i = 0; i < spa->argc; i++)
free(spa->argv[i]);
free(spa->argv);
free(spa);
}
static struct spa *spa_create(int argc, char *const argv[])
{
struct spa *spa;
int i;
spa = calloc(1, sizeof(*spa));
if (!spa)
return NULL;
spa->argc = argc;
spa->argv = malloc(argc * sizeof(char *));
if (!spa->argv)
goto err;
for (i = 0; i < argc; i++) {
spa->argv[i] = strdup(argv[i]);
if (!spa->argv[i])
goto err;
}
return spa;
err:
spa_free(spa);
return NULL;
}
static void spawn_thread(void *arg)
{
struct spa *spa = (struct spa *)arg;
ulong cycles = 0;
int repeatable = 0;
job_ret[spa->job_idx] = cmd_process(0, spa->argc, spa->argv,
&repeatable, &cycles);
spa_free(spa);
}
static unsigned int next_job_id(void)
{
int i;
for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
if (!job[i])
return i + 1;
/* No job available */
return 0;
}
static void refresh_jobs(void)
{
int i;
for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
if (job[i] && uthread_grp_done(job[i]))
job[i] = 0;
}
static int do_spawn(struct cmd_tbl *cmdtp, int flag, int argc,
char *const argv[])
{
unsigned int id;
unsigned int idx;
struct spa *spa;
int ret;
if (argc == 1)
return CMD_RET_USAGE;
spa = spa_create(argc - 1, argv + 1);
if (!spa)
return CMD_RET_FAILURE;
refresh_jobs();
id = next_job_id();
if (!id)
return CMD_RET_FAILURE;
idx = id - 1;
job[idx] = uthread_grp_new_id();
ret = uthread_create(NULL, spawn_thread, spa, 0, job[idx]);
if (ret) {
job[idx] = 0;
return CMD_RET_FAILURE;
}
ret = env_set_ulong("job_id", id);
if (ret)
return CMD_RET_FAILURE;
return CMD_RET_SUCCESS;
}
U_BOOT_CMD(spawn, CONFIG_SYS_MAXARGS, 0, do_spawn,
"run commands and summarize execution time",
"command [args...]\n");
static enum command_ret_t wait_job(unsigned int idx)
{
int prev = disable_ctrlc(false);
while (!uthread_grp_done(job[idx])) {
if (ctrlc()) {
puts("<INTERRUPT>\n");
disable_ctrlc(prev);
return CMD_RET_FAILURE;
}
uthread_schedule();
}
job[idx] = 0;
disable_ctrlc(prev);
return job_ret[idx];
}
static int do_wait(struct cmd_tbl *cmdtp, int flag, int argc,
char *const argv[])
{
enum command_ret_t ret = CMD_RET_SUCCESS;
unsigned long id;
unsigned int idx;
int i;
if (argc == 1) {
for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
if (job[i])
ret = wait_job(i);
} else {
for (i = 1; i < argc; i++) {
id = dectoul(argv[i], NULL);
if (id < 0 || id > CONFIG_CMD_SPAWN_NUM_JOBS)
return CMD_RET_USAGE;
idx = (int)id - 1;
ret = wait_job(idx);
}
}
return ret;
}
U_BOOT_CMD(wait, CONFIG_SYS_MAXARGS, 0, do_wait,
"wait for one or more jobs to complete",
"[job_id ...]\n"
" - Wait until all specified jobs have exited and return the\n"
" exit status of the last job waited for. When no job_id is\n"
" given, wait for all the background jobs.\n");

View File

@@ -16,6 +16,7 @@
#include <linux/list.h>
#include <asm/global_data.h>
#include <u-boot/schedule.h>
#include <uthread.h>
DECLARE_GLOBAL_DATA_PTR;
@@ -100,6 +101,8 @@ void schedule(void)
*/
if (gd)
cyclic_run();
uthread_schedule();
}
int cyclic_unregister_all(void)

View File

@@ -129,3 +129,5 @@ CONFIG_ULP_WATCHDOG=y
CONFIG_WDT=y
CONFIG_LZO=y
CONFIG_BZIP2=y
CONFIG_UTHREAD=y
CONFIG_CMD_SPAWN=y

View File

@@ -16,8 +16,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -17,8 +17,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -16,8 +16,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -19,8 +19,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -37,6 +37,7 @@ CONFIG_CMD_PCI=y
CONFIG_CMD_EFIDEBUG=y
CONFIG_CMD_TPM=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_SPAWN=y
CONFIG_ENV_IS_IN_FLASH=y
CONFIG_SCSI_AHCI=y
CONFIG_AHCI_PCI=y
@@ -75,4 +76,5 @@ CONFIG_MBEDTLS_LIB=y
CONFIG_TPM=y
CONFIG_TPM_PCR_ALLOCATE=y
CONFIG_GENERATE_SMBIOS_TABLE_VERBOSE=y
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -35,6 +35,7 @@ CONFIG_CMD_MTD=y
CONFIG_CMD_PCI=y
CONFIG_CMD_TPM=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_SPAWN=y
CONFIG_ENV_IS_IN_FLASH=y
CONFIG_SCSI_AHCI=y
CONFIG_AHCI_PCI=y
@@ -67,4 +68,5 @@ CONFIG_TPM2_MMIO=y
CONFIG_USB_EHCI_HCD=y
CONFIG_USB_EHCI_PCI=y
CONFIG_TPM=y
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y

View File

@@ -99,6 +99,7 @@ CONFIG_CMD_CBFS=y
CONFIG_CMD_CRAMFS=y
CONFIG_CMD_EXT4_WRITE=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_SPAWN=y
CONFIG_MAC_PARTITION=y
CONFIG_AMIGA_PARTITION=y
CONFIG_OF_CONTROL=y
@@ -275,6 +276,7 @@ CONFIG_TPM=y
CONFIG_ERRNO_STR=y
CONFIG_GETOPT=y
CONFIG_TEST_FDTDEC=y
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
CONFIG_UT_TIME=y
CONFIG_UT_DM=y

View File

@@ -143,6 +143,7 @@ CONFIG_CMD_EXT4_WRITE=y
CONFIG_CMD_SQUASHFS=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_STACKPROTECTOR_TEST=y
CONFIG_CMD_SPAWN=y
CONFIG_MAC_PARTITION=y
CONFIG_OF_CONTROL=y
CONFIG_OF_LIVE=y
@@ -364,6 +365,7 @@ CONFIG_TPM=y
CONFIG_ERRNO_STR=y
CONFIG_GETOPT=y
CONFIG_TEST_FDTDEC=y
CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
CONFIG_UT_TIME=y
CONFIG_UT_DM=y

View File

@@ -25,6 +25,8 @@ U-Boot API documentation
rng
sandbox
serial
setjmp
sysreset
timer
unicode
uthread

20
doc/api/setjmp.rst Normal file
View File

@@ -0,0 +1,20 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
Long jump API
=============
.. kernel-doc:: include/setjmp.h
:doc: Overview
.. kernel-doc:: include/setjmp.h
:internal:
Example
-------
Here is an example showing how to use the a long jump functions and
initjmp() in particular:
.. literalinclude:: ../../test/lib/initjmp.c
:language: c
:linenos:

19
doc/api/uthread.rst Normal file
View File

@@ -0,0 +1,19 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
Uthread API
===========
.. kernel-doc:: include/uthread.h
:doc: Overview
.. kernel-doc:: include/uthread.h
:internal:
Example
-------
Here is an example of how to use this API:
.. literalinclude:: ../../test/lib/uthread.c
:language: c
:linenos:

View File

@@ -9,6 +9,7 @@
#define LOG_CATEGORY UCLASS_USB
#include <bootdev.h>
#include <uthread.h>
#include <dm.h>
#include <errno.h>
#include <log.h>
@@ -17,6 +18,7 @@
#include <dm/device-internal.h>
#include <dm/lists.h>
#include <dm/uclass-internal.h>
#include <time.h>
static bool asynch_allowed;
@@ -172,6 +174,10 @@ int usb_get_max_xfer_size(struct usb_device *udev, size_t *size)
return ops->get_max_xfer_size(bus, size);
}
#if CONFIG_IS_ENABLED(UTHREAD)
static struct uthread_mutex mutex = UTHREAD_MUTEX_INITIALIZER;
#endif
int usb_stop(void)
{
struct udevice *bus;
@@ -180,10 +186,14 @@ int usb_stop(void)
struct usb_uclass_priv *uc_priv;
int err = 0, ret;
uthread_mutex_lock(&mutex);
/* De-activate any devices that have been activated */
ret = uclass_get(UCLASS_USB, &uc);
if (ret)
if (ret) {
uthread_mutex_unlock(&mutex);
return ret;
}
uc_priv = uclass_get_priv(uc);
@@ -218,28 +228,23 @@ int usb_stop(void)
uc_priv->companion_device_count = 0;
usb_started = 0;
uthread_mutex_unlock(&mutex);
return err;
}
static void usb_scan_bus(struct udevice *bus, bool recurse)
static void _usb_scan_bus(void *arg)
{
struct udevice *bus = (struct udevice *)arg;
struct usb_bus_priv *priv;
struct udevice *dev;
int ret;
priv = dev_get_uclass_priv(bus);
assert(recurse); /* TODO: Support non-recusive */
printf("scanning bus %s for devices... ", bus->name);
debug("\n");
ret = usb_scan_device(bus, 0, USB_SPEED_FULL, &dev);
if (ret)
printf("failed, error %d\n", ret);
else if (priv->next_addr == 0)
printf("No USB Device found\n");
else
printf("%d USB Device(s) found\n", priv->next_addr);
printf("Scanning bus %s failed, error %d\n", bus->name, ret);
}
static void remove_inactive_children(struct uclass *uc, struct udevice *bus)
@@ -287,65 +292,128 @@ static int usb_probe_companion(struct udevice *bus)
return 0;
}
static void _usb_init_bus(void *arg)
{
struct udevice *bus = (struct udevice *)arg;
int ret;
/* init low_level USB */
/*
* For Sandbox, we need scan the device tree each time when we
* start the USB stack, in order to re-create the emulated USB
* devices and bind drivers for them before we actually do the
* driver probe.
*
* For USB onboard HUB, we need to do some non-trivial init
* like enabling a power regulator, before enumeration.
*/
if (IS_ENABLED(CONFIG_SANDBOX) ||
IS_ENABLED(CONFIG_USB_ONBOARD_HUB)) {
ret = dm_scan_fdt_dev(bus);
if (ret) {
printf("Bus %s: USB device scan from fdt failed (%d)\n",
bus->name, ret);
return;
}
}
ret = device_probe(bus);
if (ret == -ENODEV) { /* No such device. */
printf("Bus %s: Port not available.\n", bus->name);
return;
}
if (ret) { /* Other error. */
printf("Bus %s: probe failed, error %d\n", bus->name, ret);
return;
}
usb_probe_companion(bus);
}
static int nthr;
static int grp_id;
static void usb_init_bus(struct udevice *bus)
{
if (!grp_id)
grp_id = uthread_grp_new_id();
if (!uthread_create(NULL, _usb_init_bus, (void *)bus, 0, grp_id))
nthr++;
}
static void usb_scan_bus(struct udevice *bus, bool recurse)
{
if (!grp_id)
grp_id = uthread_grp_new_id();
if (!uthread_create(NULL, _usb_scan_bus, (void *)bus, 0, grp_id))
nthr++;
}
static void usb_report_devices(struct uclass *uc)
{
struct usb_bus_priv *priv;
struct udevice *bus;
uclass_foreach_dev(bus, uc) {
if (!device_active(bus))
continue;
priv = dev_get_uclass_priv(bus);
printf("Bus %s: ", bus->name);
if (priv->next_addr == 0)
printf("No USB Device found\n");
else
printf("%d USB Device(s) found\n", priv->next_addr);
}
}
static void run_threads(void)
{
#if CONFIG_IS_ENABLED(UTHREAD)
if (!nthr)
return;
while (!uthread_grp_done(grp_id))
uthread_schedule();
nthr = 0;
grp_id = 0;
#endif
}
int usb_init(void)
{
int controllers_initialized = 0;
unsigned long t0 = timer_get_us();
struct usb_uclass_priv *uc_priv;
struct usb_bus_priv *priv;
struct udevice *bus;
struct uclass *uc;
int ret;
uthread_mutex_lock(&mutex);
if (usb_started) {
ret = 0;
goto out;
}
asynch_allowed = 1;
ret = uclass_get(UCLASS_USB, &uc);
if (ret)
return ret;
goto out;
uc_priv = uclass_get_priv(uc);
uclass_foreach_dev(bus, uc) {
/* init low_level USB */
printf("Bus %s: ", bus->name);
/*
* For Sandbox, we need scan the device tree each time when we
* start the USB stack, in order to re-create the emulated USB
* devices and bind drivers for them before we actually do the
* driver probe.
*
* For USB onboard HUB, we need to do some non-trivial init
* like enabling a power regulator, before enumeration.
*/
if (IS_ENABLED(CONFIG_SANDBOX) ||
IS_ENABLED(CONFIG_USB_ONBOARD_HUB)) {
ret = dm_scan_fdt_dev(bus);
if (ret) {
printf("USB device scan from fdt failed (%d)", ret);
continue;
}
}
ret = device_probe(bus);
if (ret == -ENODEV) { /* No such device. */
puts("Port not available.\n");
controllers_initialized++;
continue;
}
if (ret) { /* Other error. */
printf("probe failed, error %d\n", ret);
continue;
}
ret = usb_probe_companion(bus);
if (ret)
continue;
controllers_initialized++;
usb_started = true;
usb_init_bus(bus);
}
if (CONFIG_IS_ENABLED(UTHREAD))
run_threads();
usb_started = true;
/*
* lowlevel init done, now scan the bus for devices i.e. search HUBs
* and configure them, first scan primary controllers.
@@ -354,11 +422,16 @@ int usb_init(void)
if (!device_active(bus))
continue;
controllers_initialized++;
priv = dev_get_uclass_priv(bus);
if (!priv->companion)
usb_scan_bus(bus, true);
}
if (CONFIG_IS_ENABLED(UTHREAD))
run_threads();
/*
* Now that the primary controllers have been scanned and have handed
* over any devices they do not understand to their companions, scan
@@ -375,21 +448,34 @@ int usb_init(void)
}
}
debug("scan end\n");
if (CONFIG_IS_ENABLED(UTHREAD))
run_threads();
usb_report_devices(uc);
/* Remove any devices that were not found on this scan */
remove_inactive_children(uc, bus);
ret = uclass_get(UCLASS_USB_HUB, &uc);
if (ret)
return ret;
goto out;
remove_inactive_children(uc, bus);
/* if we were not able to find at least one working bus, bail out */
if (controllers_initialized == 0)
printf("No USB controllers found\n");
debug("USB initialized in %ld ms\n",
(timer_get_us() - t0) / 1000);
uthread_mutex_unlock(&mutex);
return usb_started ? 0 : -ENOENT;
out:
uthread_mutex_unlock(&mutex);
return ret;
}
int usb_setup_ehci_gadget(struct ehci_ctrl **ctlrp)

View File

@@ -3,12 +3,27 @@
#ifndef _SETJMP_H_
#define _SETJMP_H_ 1
/**
* DOC: Overview
*
* The long jump API allows to perform nonlocal gotos, that is jump from one
* function to another typically further down in the stack, while properly
* restoring the stack's state (unwinding). The two functions needed to do this
* are setjmp() and longjmp().
*
* In addition to these two standard POSIX.1-2001/C89 functions, a third one is
* present in U-Boot: initjmp(). It is an extension which allows to implement
* user-mode threads.
*/
#ifdef CONFIG_HAVE_SETJMP
#include <asm/setjmp.h>
#else
struct jmp_buf_data {
};
#endif
#include <linux/compiler_attributes.h>
#include <stddef.h>
/**
* typedef jmp_buf - information needed to restore a calling environment
@@ -37,4 +52,21 @@ int setjmp(jmp_buf env);
*/
void longjmp(jmp_buf env, int val);
/**
* initjmp() - prepare for a long jump to a given function with a given stack
*
* This function sets up a jump buffer for later use with longjmp(). It allows
* to branch to a specific function with a specific stack. Please note that
* @func MUST NOT return. It shall typically restore the main stack and resume
* execution by doing a long jump to a jump buffer initialized by setjmp()
* before the long jump. initjmp() allows to implement multithreading.
*
* @env: jump buffer
* @func: function to be called on longjmp(), MUST NOT RETURN
* @stack_base: the stack to be used by @func (lower address)
* @stack_sz: the stack size in bytes
*/
int initjmp(jmp_buf env, void __noreturn (*func)(void), void *stack_base,
size_t stack_sz);
#endif /* _SETJMP_H_ */

View File

@@ -3,6 +3,8 @@
#ifndef _U_BOOT_SCHEDULE_H
#define _U_BOOT_SCHEDULE_H
#include <uthread.h>
#if CONFIG_IS_ENABLED(CYCLIC)
/**
* schedule() - Schedule all potentially waiting tasks
@@ -17,6 +19,7 @@ void schedule(void);
static inline void schedule(void)
{
uthread_schedule();
}
#endif

183
include/uthread.h Normal file
View File

@@ -0,0 +1,183 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Copyright 2025 Linaro Limited
*/
#include <linux/list.h>
#include <linux/types.h>
#include <setjmp.h>
#ifndef _UTHREAD_H_
#define _UTHREAD_H_
/**
* DOC: Overview
*
* The uthread framework is a basic task scheduler that allows to run functions
* "in parallel" on a single CPU core. The scheduling is cooperative, not
* preemptive -- meaning that context switches from one task to another task is
* voluntary, via a call to uthread_schedule(). This characteristic makes thread
* synchronization much easier, because a thread cannot be interrupted in the
* middle of a critical section (reading from or writing to shared state, for
* instance).
*
* CONFIG_UTHREAD in lib/Kconfig enables the uthread framework. When disabled,
* the uthread_create() and uthread_schedule() functions may still be used so
* that code differences between uthreads enabled and disabled can be reduced to
* a minimum.
*/
/**
* struct uthread - a thread object
*
* @fn: thread entry point
* @arg: argument passed to the entry point when the thread is started
* @ctx: context to resume execution of this thread (via longjmp())
* @stack: initial stack pointer for the thread
* @done: true once @fn has returned, false otherwise
* @grp_id: user-supplied identifier for this thread and possibly others. A
* thread can belong to zero or one group (not more), and a group may contain
* any number of threads.
* @list: link in the global scheduler list
*/
struct uthread {
void (*fn)(void *arg);
void *arg;
jmp_buf ctx;
void *stack;
bool done;
unsigned int grp_id;
struct list_head list;
};
/**
* Internal state of a struct uthread_mutex
*/
enum uthread_mutex_state {
UTHREAD_MUTEX_UNLOCKED = 0,
UTHREAD_MUTEX_LOCKED = 1
};
/**
* Uthread mutex
*/
struct uthread_mutex {
enum uthread_mutex_state state;
};
#define UTHREAD_MUTEX_INITIALIZER { .state = UTHREAD_MUTEX_UNLOCKED }
#ifdef CONFIG_UTHREAD
/**
* uthread_create() - Create a uthread object and make it ready for execution
*
* Threads are automatically deleted when they return from their entry point.
*
* @uthr: a pointer to a user-allocated uthread structure to store information
* about the new thread, or NULL to let the framework allocate and manage its
* own structure.
* @fn: the thread's entry point
* @arg: argument passed to the thread's entry point
* @stack_sz: stack size for the new thread (in bytes). The stack is allocated
* on the heap.
* @grp_id: an optional thread group ID that the new thread should belong to
* (zero for no group)
*/
int uthread_create(struct uthread *uthr, void (*fn)(void *), void *arg,
size_t stack_sz, unsigned int grp_id);
/**
* uthread_schedule() - yield the CPU to the next runnable thread
*
* This function is called either by the main thread or any secondary thread
* (that is, any thread created via uthread_create()) to switch execution to
* the next runnable thread.
*
* Return: true if a thread was scheduled, false if no runnable thread was found
*/
bool uthread_schedule(void);
/**
* uthread_grp_new_id() - return a new ID for a thread group
*
* Return: the new thread group ID
*/
unsigned int uthread_grp_new_id(void);
/**
* uthread_grp_done() - test if all threads in a group are done
*
* @grp_id: the ID of the thread group that should be considered
* Return: false if the group contains at least one runnable thread (i.e., one
* thread which entry point has not returned yet), true otherwise
*/
bool uthread_grp_done(unsigned int grp_id);
/**
* uthread_mutex_lock() - lock a mutex
*
* If the cwmutexlock is available (i.e., not owned by any other thread), then
* it is locked for use by the current thread. Otherwise the current thread
* blocks: it enters a wait loop by scheduling other threads until the mutex
* becomes unlocked.
*
* @mutex: pointer to the mutex to lock
* Return: 0 on success, in which case the lock is owned by the calling thread.
* != 0 otherwise (the lock is not owned by the calling thread).
*/
int uthread_mutex_lock(struct uthread_mutex *mutex);
/**
* uthread_mutex_trylock() - lock a mutex if not currently locked
*
* Similar to uthread_mutex_lock() except return immediately if the mutex is
* locked already.
*
* @mutex: pointer to the mutex to lock
* Return: 0 on success, in which case the lock is owned by the calling thread.
* EBUSY if the mutex is already locked by another thread. Any other non-zero
* value on error.
*/
int uthread_mutex_trylock(struct uthread_mutex *mutex);
/**
* uthread_mutex_unlock() - unlock a mutex
*
* The mutex is assumed to be owned by the calling thread on entry. On exit, it
* is unlocked.
*
* @mutex: pointer to the mutex to unlock
* Return: 0 on success, != 0 on error
*/
int uthread_mutex_unlock(struct uthread_mutex *mutex);
#else
static inline int uthread_create(struct uthread *uthr, void (*fn)(void *),
void *arg, size_t stack_sz,
unsigned int grp_id)
{
fn(arg);
return 0;
}
static inline bool uthread_schedule(void)
{
return false;
}
static inline unsigned int uthread_grp_new_id(void)
{
return 0;
}
static inline bool uthread_grp_done(unsigned int grp_id)
{
return true;
}
/* These are macros for convenience on the caller side */
#define uthread_mutex_lock(_mutex) ({ 0; })
#define uthread_mutex_trylock(_mutex) ({ 0 })
#define uthread_mutex_unlock(_mutex) ({ 0; })
#endif /* CONFIG_UTHREAD */
#endif /* _UTHREAD_H_ */

View File

@@ -1258,6 +1258,27 @@ config PHANDLE_CHECK_SEQ
enable this config option to distinguish them using
phandles in fdtdec_get_alias_seq() function.
config UTHREAD
bool "Enable thread support"
depends on HAVE_INITJMP
help
Implement a simple form of cooperative multi-tasking based on
context-switching via initjmp(), setjmp() and longjmp(). The
uthread_ interface enables the main thread of execution to create
one or more secondary threads and schedule them until they all have
returned. At any point a thread may suspend its execution and
schedule another thread, which allows for the efficient multiplexing
of leghthy operations.
config UTHREAD_STACK_SIZE
int "Default uthread stack size"
depends on UTHREAD
default 32768
help
The default stack size for uthreads. Each uthread has its own stack.
When the stack_sz argument to uthread_create() is zero then this
value is used.
endmenu
source "lib/fwu_updates/Kconfig"

View File

@@ -159,6 +159,8 @@ obj-$(CONFIG_LIB_ELF) += elf.o
obj-$(CONFIG_$(PHASE_)SEMIHOSTING) += semihosting.o
obj-$(CONFIG_UTHREAD) += uthread.o
#
# Build a fast OID lookup registry from include/linux/oid_registry.h
#

View File

@@ -17,6 +17,7 @@
#include <asm/global_data.h>
#include <asm/io.h>
#include <linux/delay.h>
#include <uthread.h>
#ifndef CFG_WD_PERIOD
# define CFG_WD_PERIOD (10 * 1000 * 1000) /* 10 seconds default */
@@ -197,7 +198,13 @@ void udelay(unsigned long usec)
do {
schedule();
kv = usec > CFG_WD_PERIOD ? CFG_WD_PERIOD : usec;
__udelay(kv);
if (CONFIG_IS_ENABLED(UTHREAD)) {
ulong t0 = timer_get_us();
while (timer_get_us() - t0 < kv)
uthread_schedule();
} else {
__udelay(kv);
}
usec -= kv;
} while(usec);
}

165
lib/uthread.c Normal file
View File

@@ -0,0 +1,165 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2021 Ahmad Fatoum, Pengutronix
* Copyright (C) 2025 Linaro Limited
*
* An implementation of cooperative multi-tasking inspired from barebox threads
* https://github.com/barebox/barebox/blob/master/common/bthread.c
*/
#include <compiler.h>
#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <malloc.h>
#include <setjmp.h>
#include <stdint.h>
#include <uthread.h>
static struct uthread main_thread = {
.list = LIST_HEAD_INIT(main_thread.list),
};
static struct uthread *current = &main_thread;
/**
* uthread_trampoline() - Call the current thread's entry point then resume the
* main thread.
*
* This is a helper function which is used as the @func argument to the
* initjmp() function, and ultimately invoked via setjmp(). It does not return
* but instead longjmp()'s back to the main thread.
*/
static void __noreturn uthread_trampoline(void)
{
struct uthread *curr = current;
curr->fn(curr->arg);
curr->done = true;
current = &main_thread;
longjmp(current->ctx, 1);
/* Not reached */
while (true)
;
}
/**
* uthread_free() - Free memory used by a uthread object.
*/
static void uthread_free(struct uthread *uthread)
{
if (!uthread)
return;
free(uthread->stack);
free(uthread);
}
int uthread_create(struct uthread *uthr, void (*fn)(void *), void *arg,
size_t stack_sz, unsigned int grp_id)
{
bool user_allocated = false;
if (!stack_sz)
stack_sz = CONFIG_UTHREAD_STACK_SIZE;
if (uthr) {
user_allocated = true;
} else {
uthr = calloc(1, sizeof(*uthr));
if (!uthr)
return -1;
}
uthr->stack = memalign(16, stack_sz);
if (!uthr->stack)
goto err;
uthr->fn = fn;
uthr->arg = arg;
uthr->grp_id = grp_id;
list_add_tail(&uthr->list, &current->list);
initjmp(uthr->ctx, uthread_trampoline, uthr->stack, stack_sz);
return 0;
err:
if (!user_allocated)
free(uthr);
return -1;
}
/**
* uthread_resume() - switch execution to a given thread
*
* @uthread: the thread object that should be resumed
*/
static void uthread_resume(struct uthread *uthread)
{
if (!setjmp(current->ctx)) {
current = uthread;
longjmp(uthread->ctx, 1);
}
}
bool uthread_schedule(void)
{
struct uthread *next;
struct uthread *tmp;
list_for_each_entry_safe(next, tmp, &current->list, list) {
if (!next->done) {
uthread_resume(next);
return true;
}
/* Found a 'done' thread, free its resources */
list_del(&next->list);
uthread_free(next);
}
return false;
}
unsigned int uthread_grp_new_id(void)
{
static unsigned int id;
return ++id;
}
bool uthread_grp_done(unsigned int grp_id)
{
struct uthread *next;
list_for_each_entry(next, &main_thread.list, list) {
if (next->grp_id == grp_id && !next->done)
return false;
}
return true;
}
int uthread_mutex_lock(struct uthread_mutex *mutex)
{
while (mutex->state == UTHREAD_MUTEX_LOCKED)
uthread_schedule();
mutex->state = UTHREAD_MUTEX_LOCKED;
return 0;
}
int uthread_mutex_trylock(struct uthread_mutex *mutex)
{
if (mutex->state == UTHREAD_MUTEX_UNLOCKED) {
mutex->state = UTHREAD_MUTEX_LOCKED;
return 0;
}
return -EBUSY;
}
int uthread_mutex_unlock(struct uthread_mutex *mutex)
{
mutex->state = UTHREAD_MUTEX_UNLOCKED;
return 0;
}

View File

@@ -392,8 +392,7 @@ static int bootdev_test_hunter(struct unit_test_state *uts)
ut_assert_console_end();
ut_assertok(bootdev_hunt("usb1", false));
ut_assert_nextline(
"Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
/* USB is 7th in the list, so bit 8 */
@@ -448,8 +447,7 @@ static int bootdev_test_cmd_hunt(struct unit_test_state *uts)
ut_assert_nextline("scanning bus for devices...");
ut_assert_skip_to_line("Hunting with: spi_flash");
ut_assert_nextline("Hunting with: usb");
ut_assert_nextline(
"Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_nextline("Hunting with: virtio");
ut_assert_console_end();
@@ -551,8 +549,7 @@ static int bootdev_test_hunt_prio(struct unit_test_state *uts)
ut_assertok(bootdev_hunt_prio(BOOTDEVP_5_SCAN_SLOW, true));
ut_assert_nextline("Hunting with: ide");
ut_assert_nextline("Hunting with: usb");
ut_assert_nextline(
"Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
return 0;
@@ -604,7 +601,7 @@ static int bootdev_test_hunt_label(struct unit_test_state *uts)
ut_assertnonnull(dev);
ut_asserteq_str("usb_mass_storage.lun0.bootdev", dev->name);
ut_asserteq(BOOTFLOW_METHF_SINGLE_UCLASS, mflags);
ut_assert_nextlinen("Bus usb@1: scanning bus usb@1");
ut_assert_nextline("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
return 0;

View File

@@ -1290,7 +1290,7 @@ static int bootflow_efi(struct unit_test_state *uts)
ut_assertok(run_command("bootflow scan", 0));
ut_assert_skip_to_line(
"Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
"Bus usb@1: 5 USB Device(s) found");
ut_assertok(run_command("bootflow list", 0));

View File

@@ -39,3 +39,4 @@ obj-$(CONFIG_CMD_WGET) += wget.o
endif
obj-$(CONFIG_ARM_FFA_TRANSPORT) += armffa.o
endif
obj-$(CONFIG_CMD_SPAWN) += spawn.o

32
test/cmd/spawn.c Normal file
View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Tests for spawn and wait commands
*
* Copyright 2025, Linaro Ltd.
*/
#include <command.h>
#include <test/cmd.h>
#include <test/test.h>
#include <test/ut.h>
static int test_cmd_spawn(struct unit_test_state *uts)
{
ut_assertok(run_command("wait; spawn sleep 2; setenv j ${job_id}; "
"spawn setenv spawned true; "
"setenv jj ${job_id}; wait; "
"echo ${j} ${jj} ${spawned}", 0));
console_record_readline(uts->actual_str, sizeof(uts->actual_str));
ut_asserteq_ptr(uts->actual_str,
strstr(uts->actual_str, "1 2 true"));
ut_assertok(run_command("spawn true; wait; setenv t $?; spawn false; "
"wait; setenv f $?; wait; echo $t $f $?", 0));
console_record_readline(uts->actual_str, sizeof(uts->actual_str));
ut_asserteq_ptr(uts->actual_str,
strstr(uts->actual_str, "0 1 0"));
ut_assert_console_end();
return 0;
}
CMD_TEST(test_cmd_spawn, UTF_CONSOLE);

View File

@@ -15,6 +15,7 @@ obj-$(CONFIG_SANDBOX) += kconfig.o
obj-y += lmb.o
obj-$(CONFIG_HAVE_SETJMP) += longjmp.o
obj-$(CONFIG_SANDBOX) += membuf.o
obj-$(CONFIG_HAVE_INITJMP) += initjmp.o
obj-$(CONFIG_CONSOLE_RECORD) += test_print.o
obj-$(CONFIG_SSCANF) += sscanf.o
obj-$(CONFIG_$(PHASE_)STRTO) += str.o
@@ -31,6 +32,7 @@ obj-$(CONFIG_CRC8) += test_crc8.o
obj-$(CONFIG_UT_LIB_CRYPT) += test_crypt.o
obj-$(CONFIG_UT_TIME) += time.o
obj-$(CONFIG_$(PHASE_)UT_UNICODE) += unicode.o
obj-$(CONFIG_UTHREAD) += uthread.o
obj-$(CONFIG_LIB_UUID) += uuid.o
else
obj-$(CONFIG_SANDBOX) += kconfig_spl.o

73
test/lib/initjmp.c Normal file
View File

@@ -0,0 +1,73 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright 2025 Linaro Limited
*
* Unit test for initjmp()
*/
#include <compiler.h>
#include <setjmp.h>
#include <stdbool.h>
#include <test/lib.h>
#include <test/ut.h>
#include <vsprintf.h>
static bool ep_entered;
static jmp_buf return_buf;
static void __noreturn entrypoint(void)
{
ep_entered = true;
/* Jump back to the main routine */
longjmp(return_buf, 1);
/* Not reached */
panic("longjmp failed\n");
}
static int lib_initjmp(struct unit_test_state *uts)
{
int ret;
void *stack;
jmp_buf buf;
/* Arbitrary but smaller values (< page size?) fail on SANDBOX */
size_t stack_sz = 8192;
(void)entrypoint;
ep_entered = false;
stack = malloc(stack_sz);
ut_assertnonnull(stack);
/*
* Prepare return_buf so that entrypoint may jump back just after the
* if()
*/
if (!setjmp(return_buf)) {
/* return_buf initialized, entrypoint not yet called */
/*
* Prepare another jump buffer to jump into entrypoint with the
* given stack
*/
ret = initjmp(buf, entrypoint, stack, stack_sz);
ut_assertok(ret);
/* Jump into entrypoint */
longjmp(buf, 1);
/*
* Not reached since entrypoint is expected to branch after
* the if()
*/
ut_assert(false);
}
ut_assert(ep_entered);
free(stack);
return 0;
}
LIB_TEST(lib_initjmp, 0);

146
test/lib/uthread.c Normal file
View File

@@ -0,0 +1,146 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2025 Linaro Limited
*
* Unit test for uthread
*/
#include <stdbool.h>
#include <test/lib.h>
#include <test/ut.h>
#include <uthread.h>
static int count;
/* A thread entry point */
static void worker(void *arg)
{
int loops = (int)(unsigned long)arg;
int i;
for (i = 0; i < loops; i++) {
count++;
uthread_schedule();
}
}
/*
* uthread() - testing the uthread API
*
* This function creates two threads with the same entry point. The first one
* receives 5 as an argument, the second one receives 10. The number indicates
* the number of time the worker thread should loop on uthread_schedule()
* before returning. The workers increment a global counter each time they loop.
* As a result the main thread knows how many times it should call
* uthread_schedule() to let the two threads proceed, and it also knows which
* value the counter should have at any moment.
*/
static int uthread(struct unit_test_state *uts)
{
int i;
int id1, id2;
count = 0;
id1 = uthread_grp_new_id();
ut_assert(id1 != 0);
id2 = uthread_grp_new_id();
ut_assert(id2 != 0);
ut_assert(id1 != id2);
ut_assertok(uthread_create(NULL, worker, (void *)5, 0, id1));
ut_assertok(uthread_create(NULL, worker, (void *)10, 0, 0));
/*
* The first call is expected to schedule the first worker, which will
* schedule the second one, which will schedule back to the main thread
* (here). Therefore count should be 2.
*/
ut_assert(uthread_schedule());
ut_asserteq(2, count);
ut_assert(!uthread_grp_done(id1));
/* Four more calls should bring the count to 10 */
for (i = 0; i < 4; i++) {
ut_assert(!uthread_grp_done(id1));
ut_assert(uthread_schedule());
}
ut_asserteq(10, count);
/* This one allows the first worker to exit */
ut_assert(uthread_schedule());
/* At this point there should be no runnable thread in group 'id1' */
ut_assert(uthread_grp_done(id1));
/* Five more calls for the second worker to finish incrementing */
for (i = 0; i < 5; i++)
ut_assert(uthread_schedule());
ut_asserteq(15, count);
/* Plus one call to let the second worker return from its entry point */
ut_assert(uthread_schedule());
/* Now both tasks should be done, schedule should return false */
ut_assert(!uthread_schedule());
return 0;
}
LIB_TEST(uthread, 0);
struct mw_args {
struct unit_test_state *uts;
struct uthread_mutex *m;
int flag;
};
static int mutex_worker_ret;
static int _mutex_worker(struct mw_args *args)
{
struct unit_test_state *uts = args->uts;
ut_asserteq(-EBUSY, uthread_mutex_trylock(args->m));
ut_assertok(uthread_mutex_lock(args->m));
args->flag = 1;
ut_assertok(uthread_mutex_unlock(args->m));
return 0;
}
static void mutex_worker(void *arg)
{
mutex_worker_ret = _mutex_worker((struct mw_args *)arg);
}
/*
* thread_mutex() - testing uthread mutex operations
*
*/
static int uthread_mutex(struct unit_test_state *uts)
{
struct uthread_mutex m = UTHREAD_MUTEX_INITIALIZER;
struct mw_args args = { .uts = uts, .m = &m, .flag = 0 };
int id;
int i;
id = uthread_grp_new_id();
ut_assert(id != 0);
/* Take the mutex */
ut_assertok(uthread_mutex_lock(&m));
/* Start a thread */
ut_assertok(uthread_create(NULL, mutex_worker, (void *)&args, 0,
id));
/* Let the thread run for a bit */
for (i = 0; i < 100; i++)
ut_assert(uthread_schedule());
/* Thread should not have set the flag due to the mutex */
ut_asserteq(0, args.flag);
/* Release the mutex */
ut_assertok(uthread_mutex_unlock(&m));
/* Schedule the thread until it is done */
while (uthread_schedule())
;
/* Now the flag should be set */
ut_asserteq(1, args.flag);
/* And the mutex should be available */
ut_assertok(uthread_mutex_trylock(&m));
ut_assertok(uthread_mutex_unlock(&m));
/* Of course no error are expected from the thread routine */
ut_assertok(mutex_worker_ret);
return 0;
}
LIB_TEST(uthread_mutex, 0);