7.2. Background execution in barebox

barebox does not use interrupts to avoid the associated increase in complexity. Nevertheless it is sometimes desired to execute code “in the background”, like for example polling for completion of transfers or to regularly blink a heartbeat LED. For these scenarios barebox offers the techniques described below.

7.2.1. Pollers

Pollers are a way in barebox to frequently execute code in the background. They don’t run within their own threads, but instead they are executed whenever is_timeout() is called. This has a few implications. First of all, pollers are not executed when is_timeout() is not called. For this and other reasons, loops polling for hardware events should always use a timeout, which is best implemented with is_timeout(). Another thing to remember is that pollers can be executed anywhere in the middle of other device accesses whenever is_timeout() is called. Care must be taken that a poller doesn’t access the very same device again itself. See “slices” below on how devices can safely be accessed from pollers. Some operations are entirely forbidden from pollers. For example it is forbidden to access the virtual filesystem, as this could trigger arbitrary device accesses. The macro assert_command_context() is added to those places to make sure they are not called in the wrong context. The poller interface is declared in include/poller.h. poller_register() is used to register a poller. The struct poller_struct passed to poller_register() needs to have the poller_struct.func() member populated with the function that shall be called. From the moment a poller is registered poller_struct.func() will be called regularly as long as someone calls is_timeout(). A special poller can be registered with poller_async_register(). A poller registered this way won’t be called right away, instead running it can be triggered by calling poller_call_async(). This will execute the poller after the @delay_ns argument. poller_call_async() may also be called from with the poller, so with this it’s possible to run a poller regularly with configurable delays.

Pollers are limited in the things they can do. Poller code must always be prepared for the case that the resources it accesses are currently busy and handle this gracefully by trying again later. Most places in barebox either do not test for resources being busy or return with an error if they are busy. Calling into the filesystem potentially uses arbitrary other devices, so doing this from pollers is forbidden. For the same reason it is forbidden to execute barebox commands from pollers.

7.2.2. Workqueues

Sometimes code wants to access files from poller context or even wants to execute barebox commands from poller context. The fastboot implementation is an example for such a case. The fastboot commands come in via USB or network and the completion and packet receive handlers are executed in poller context. Here workqueues come into play. They allow to queue work items from poller context. These work items are executed later at the point where the shell fetches its next command. At this point we implicitly know that it’s allowed to execute commands or to access the filesystem, because otherwise the shell could not do it. This means that execution of the next shell command will be delayed until all work items are being done. Likewise the work items will only be executed when the current shell command has finished.

The workqueue interface is declared in include/work.h.

wq_register() is the first step to use the workqueue interface. wq_register() registers a workqueue to which work items can be attached. Before registering a workqueue a struct work_queue has to be populated with two function callbacks. work_queue.fn() is the callback that actually does the work once it is scheduled. work_queue.cancel() is a callback which is executed when the pending work shall be cancelled. This is primarily for freeing the memory associated to a work item. wq_queue_work() is for actually queueing a work item on a work queue. This can be called from poller code. Usually a work item is allocated by the poller and then freed either in work_queue.fn() or in work_queue.cancel().

7.2.3. bthreads

barebox threads are co-operative green threads, which are scheduled for the same context as workqueues: Before the shell executes the next command. This means that bthreads can be used to implement workqueues, but not pollers.

The bthread interface is declared in include/bthread.h. bthread_create() is used to allocate a bthread control block along with its stack. bthread_wake() can be used to add it into the run queue. From this moment on and until the thread terminates, the thread will be switched to regularly as long as the shell processes commands.

bthreads are allowed to call is_timeout(), which will eventually arrange for other threads to execute. This allowed implementing a Linux-like completion API on top, which can be useful for porting threaded kernel code.

7.2.4. Slices

Slices are a way to check if a device is currently busy and thus may not be called into currently. Pollers wanting to access a device must call slice_busy() on the slice provided by the device before calling into it. When the slice is acquired (which can only happen inside a poller) the poller can’t continue at this moment and must try again next time it is executed. Drivers whose devices provide a slice must call slice_acquire() before accessing the device and slice_release() afterwards. Slices can have dependencies to other slices, for example a USB network controller uses the corresponding USB host controller. A dependency can be expressed with slice_depends_on(). With this the USB network controller can add a dependency from the network device it provides itself to the USB host controller it depends on. With this slice_busy() on the network device will return true when the USB host controller is busy.

The usual pattern for using slices is that the device driver for a device calls slice_acquire() when entering the driver and slice_release() before leaving the driver. The driver also provides a function returning the slice for a device, for example the ethernet support code provides struct slice *eth_device_slice(struct eth_device *edev). Poller code which wants to use the ethernet device checks for the availability doing slice_busy(eth_device_slice(edev)) before accessing the ethernet device. When the slice is not busy the poller can continue with accessing that device. Otherwise the poller must return and try again next time it is called.

7.2.5. Limitations

What barebox does is best described as cooperative multitasking. As pollers can’t be interrupted, it will directly affect the user experience when they take too long. When barebox reacts sluggishly to key presses, then probably pollers take too long to execute. A first test if this is the case can be done by executing poller -t on the command line. This command will print how many times we can execute all registered pollers in one second. When this number is too low then pollers are guilty responsible. Workqueues help to run schedule/execute longer running code, but during the time while workqueues are executed nothing else happens. This means that when fastboot flashes an image in a workqueue then barebox won’t react to any key presses on the command line. The usage of the interfaces described in this document is not yet very widespread in barebox. The interfaces are used in the places where we need them, but there are other places which do not use them but should.

For example using a LED driven by a I2C GPIO expander used as hearbeat LED used to not work properly before addition of slices. Consider the I2C driver accesses an unrelated I2C device, like an EEPROM. After having initiated the transfer the driver polls for the transfer being completed using a is_timeout() loop. The call to is_timeout() then calls into the registered pollers from which one accesses the same I2C bus whose driver is just polling for completion of another transfer. Without slices, this left the I2C driver in an undefined state, where it would likely not work anymore.