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

@@ -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);