diff options
38 files changed, 1316 insertions, 62 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index 9d5af7ebca4..ebbb0b6e8d0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -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 diff --git a/arch/Kconfig b/arch/Kconfig index 35b19f9bfdc..ea33d07c086 100644 --- a/arch/Kconfig +++ b/arch/Kconfig @@ -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 diff --git a/arch/arm/lib/setjmp.S b/arch/arm/lib/setjmp.S index 2f041aeef01..81bef578719 100644 --- a/arch/arm/lib/setjmp.S +++ b/arch/arm/lib/setjmp.S @@ -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 diff --git a/arch/arm/lib/setjmp_aarch64.S b/arch/arm/lib/setjmp_aarch64.S index 1b8d000eb48..01193ccc426 100644 --- a/arch/arm/lib/setjmp_aarch64.S +++ b/arch/arm/lib/setjmp_aarch64.S @@ -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 diff --git a/arch/riscv/lib/setjmp.S b/arch/riscv/lib/setjmp.S index 99d6195827e..9e1f3d5749b 100644 --- a/arch/riscv/lib/setjmp.S +++ b/arch/riscv/lib/setjmp.S @@ -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 diff --git a/arch/sandbox/cpu/Makefile b/arch/sandbox/cpu/Makefile index bfcdc335d32..038ad78accc 100644 --- a/arch/sandbox/cpu/Makefile +++ b/arch/sandbox/cpu/Makefile @@ -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 $@ $< diff --git a/arch/sandbox/cpu/initjmp.c b/arch/sandbox/cpu/initjmp.c new file mode 100644 index 00000000000..6e72d32cb4b --- /dev/null +++ b/arch/sandbox/cpu/initjmp.c @@ -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; +} + diff --git a/cmd/Kconfig b/cmd/Kconfig index c2ce519d1e3..2d31abcef73 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -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 diff --git a/cmd/Makefile b/cmd/Makefile index 8f0cee8d714..80cf70b7fe8 100644 --- a/cmd/Makefile +++ b/cmd/Makefile @@ -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/ diff --git a/cmd/spawn.c b/cmd/spawn.c new file mode 100644 index 00000000000..eddbcb792b3 --- /dev/null +++ b/cmd/spawn.c @@ -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"); diff --git a/common/cyclic.c b/common/cyclic.c index fad071a39c6..b695f092f52 100644 --- a/common/cyclic.c +++ b/common/cyclic.c @@ -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) diff --git a/configs/imx93_11x11_evk_defconfig b/configs/imx93_11x11_evk_defconfig index 59c4da7fa2b..d03895ed6ea 100644 --- a/configs/imx93_11x11_evk_defconfig +++ b/configs/imx93_11x11_evk_defconfig @@ -129,3 +129,5 @@ CONFIG_ULP_WATCHDOG=y CONFIG_WDT=y CONFIG_LZO=y CONFIG_BZIP2=y +CONFIG_UTHREAD=y +CONFIG_CMD_SPAWN=y diff --git a/configs/qemu-riscv32_defconfig b/configs/qemu-riscv32_defconfig index 6f8f9827611..b9f28873c15 100644 --- a/configs/qemu-riscv32_defconfig +++ b/configs/qemu-riscv32_defconfig @@ -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 diff --git a/configs/qemu-riscv32_smode_defconfig b/configs/qemu-riscv32_smode_defconfig index 1d57b68086a..cd89571e40c 100644 --- a/configs/qemu-riscv32_smode_defconfig +++ b/configs/qemu-riscv32_smode_defconfig @@ -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 diff --git a/configs/qemu-riscv64_defconfig b/configs/qemu-riscv64_defconfig index e00050db4e1..c67fb9a3352 100644 --- a/configs/qemu-riscv64_defconfig +++ b/configs/qemu-riscv64_defconfig @@ -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 diff --git a/configs/qemu-riscv64_smode_defconfig b/configs/qemu-riscv64_smode_defconfig index a424f978294..d28e9fbeceb 100644 --- a/configs/qemu-riscv64_smode_defconfig +++ b/configs/qemu-riscv64_smode_defconfig @@ -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 diff --git a/configs/qemu_arm64_defconfig b/configs/qemu_arm64_defconfig index cd9a4f2ae26..b1371d4258f 100644 --- a/configs/qemu_arm64_defconfig +++ b/configs/qemu_arm64_defconfig @@ -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 diff --git a/configs/qemu_arm_defconfig b/configs/qemu_arm_defconfig index 2d642f86ba6..97d2f2f2e49 100644 --- a/configs/qemu_arm_defconfig +++ b/configs/qemu_arm_defconfig @@ -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 diff --git a/configs/sandbox64_defconfig b/configs/sandbox64_defconfig index 42c40077823..23df4c3b635 100644 --- a/configs/sandbox64_defconfig +++ b/configs/sandbox64_defconfig @@ -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 diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index 87f21fdbd12..c4b1b8114d6 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -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 diff --git a/doc/api/index.rst b/doc/api/index.rst index a108718ea99..506843ed74a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -25,6 +25,8 @@ U-Boot API documentation rng sandbox serial + setjmp sysreset timer unicode + uthread diff --git a/doc/api/setjmp.rst b/doc/api/setjmp.rst new file mode 100644 index 00000000000..c30e51c2b55 --- /dev/null +++ b/doc/api/setjmp.rst @@ -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: diff --git a/doc/api/uthread.rst b/doc/api/uthread.rst new file mode 100644 index 00000000000..8b25cc1ff80 --- /dev/null +++ b/doc/api/uthread.rst @@ -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: diff --git a/drivers/usb/host/usb-uclass.c b/drivers/usb/host/usb-uclass.c index bfec303e7af..7247245a702 100644 --- a/drivers/usb/host/usb-uclass.c +++ b/drivers/usb/host/usb-uclass.c @@ -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,64 +292,127 @@ 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; - } + usb_init_bus(bus); + } - ret = usb_probe_companion(bus); - if (ret) - continue; + if (CONFIG_IS_ENABLED(UTHREAD)) + run_threads(); - controllers_initialized++; - usb_started = true; - } + usb_started = true; /* * lowlevel init done, now scan the bus for devices i.e. search HUBs @@ -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) diff --git a/include/setjmp.h b/include/setjmp.h index 37d3a8af85d..32dd48803e9 100644 --- a/include/setjmp.h +++ b/include/setjmp.h @@ -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_ */ diff --git a/include/u-boot/schedule.h b/include/u-boot/schedule.h index 4fd34c41229..4605971fdcb 100644 --- a/include/u-boot/schedule.h +++ b/include/u-boot/schedule.h @@ -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 diff --git a/include/uthread.h b/include/uthread.h new file mode 100644 index 00000000000..89fa552a6f6 --- /dev/null +++ b/include/uthread.h @@ -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_ */ diff --git a/lib/Kconfig b/lib/Kconfig index ac34ec45bb1..b2aecd8a49e 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -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" diff --git a/lib/Makefile b/lib/Makefile index 7178c5a3e3c..18ae0cd87bf 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -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 # diff --git a/lib/time.c b/lib/time.c index d88edafb196..0e9b079f9cf 100644 --- a/lib/time.c +++ b/lib/time.c @@ -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); } diff --git a/lib/uthread.c b/lib/uthread.c new file mode 100644 index 00000000000..062fca7d209 --- /dev/null +++ b/lib/uthread.c @@ -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, ¤t->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, ¤t->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; +} diff --git a/test/boot/bootdev.c b/test/boot/bootdev.c index d5499918249..9af94786870 100644 --- a/test/boot/bootdev.c +++ b/test/boot/bootdev.c @@ -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; diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c index 5f9c037ff53..b261bd5f620 100644 --- a/test/boot/bootflow.c +++ b/test/boot/bootflow.c @@ -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)); diff --git a/test/cmd/Makefile b/test/cmd/Makefile index 8596c5ad753..595e4cfcada 100644 --- a/test/cmd/Makefile +++ b/test/cmd/Makefile @@ -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 diff --git a/test/cmd/spawn.c b/test/cmd/spawn.c new file mode 100644 index 00000000000..8f48f5ee25c --- /dev/null +++ b/test/cmd/spawn.c @@ -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); diff --git a/test/lib/Makefile b/test/lib/Makefile index 97ab71ba5d1..d620510f998 100644 --- a/test/lib/Makefile +++ b/test/lib/Makefile @@ -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 diff --git a/test/lib/initjmp.c b/test/lib/initjmp.c new file mode 100644 index 00000000000..5b4b50b3f0f --- /dev/null +++ b/test/lib/initjmp.c @@ -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); diff --git a/test/lib/uthread.c b/test/lib/uthread.c new file mode 100644 index 00000000000..10a94d1c560 --- /dev/null +++ b/test/lib/uthread.c @@ -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); |