mirror of
https://github.com/apache/nuttx.git
synced 2026-08-01 20:28:58 +00:00
* This completes task list in https://github.com/apache/nuttx/issues/11127. * This preserves selected content from cwiki and moves it to new docs. * Most pages are simple copy-paste with a simple RST formatting updates, with minor updates. * Content update / reorganization will follow later on when needed. * Files added (or updated title from cwiki -> current docs): * Documentation/implementation: * index. * cancellation_points. * Asynchronous vs. Synchronous Context Switches -> context_switches.rst. * ARMv7-M Hardfaults, SVCALL, and Debuggers -> hardfatuls.rst. * chip.h FAQ -> chip_h.rst. * Debug Output (SYSLOG) Issues -> syslog.rst. * Detaching File Descriptors -> file_descriptors.rst. * device_nodes.rst. * Dynamic Clocking -> power_management.rst. * ENOTTY ioctl() Return Value -> ioctl.rst. * memory_configurations.rst. * kernel_modules_vs_shared_libraries.rst. * NAKing USB OUT/IN Tokens -> usb.rst. * naming_arch_mcu_board_interfaces.rst. * naming_os_internals.rst. * nuttx_tasking.rst. * oneshot_timers_and_cpu_load.rst. * nuttx_initialization_sequence.rst. * short_time_delays.rst. * Signal Handler Tour -> signal_handlers.rst. * smp.rst. * syslog.rst. * Task Exit Sequence -> nuttx_tasking.rst. * tasks_vs_threads.rst. * tls.rst. * tickless_os.rst. * Why Can't Kernel Threads Have pthreads -> kernel_threads_vs_pthreads.rst. * Documentation/components/filesystem: * smartfs.rst. Signed-off-by: Tomasz 'CeDeROM' CEDRO <tomek@cedro.info>
441 lines
19 KiB
ReStructuredText
441 lines
19 KiB
ReStructuredText
.. _cancellation-points:
|
|
|
|
===================
|
|
Cancellation Points
|
|
===================
|
|
|
|
Cancellation Types
|
|
==================
|
|
|
|
In POSIX, there are two pthread cancellation types:
|
|
``PTHREAD_CANCEL_DEFERRED`` and ``PTHREAD_CANCEL_ASYNCHRONOUS``.
|
|
|
|
``PTHREAD_CANCEL_DEFERRED`` is the default and the normal kind of pthread
|
|
cancellation that should be used.
|
|
You cannot have the ``PTHREAD_CANCEL_DEFERRED`` cancellation type without
|
|
cancellation points and you will not have cancellation points unless you
|
|
enable them with ``CONFIG_CANCELLATION_POINTS=y``.
|
|
|
|
``PTHREAD_CANCEL_ASYNCHRONOUS`` is brutal; it simply kills the pthread
|
|
immediately without regard for what it is doing.
|
|
``PTHREAD_CANCEL_DEFERRED`` works differently; nothing happens immediately.
|
|
Instead, the cancellation will be deferred until the pthread is at
|
|
(or a better preposition would be within) a cancellation point.
|
|
|
|
Cancellation points add special instrumentation to a specific set
|
|
of interface functions. Those interface functions are listed here:
|
|
http://OpenGroup.org.
|
|
|
|
|
|
Enter and Leave Cancelation Point
|
|
=================================
|
|
|
|
In NuttX, each of these functions ``CONFIG_CANCELLATION_POINTS=y`` will enable
|
|
a special call to ``enter_cancellation_point()`` at the beginning
|
|
of the function and a matching call to ``leave_cancellation_point()`` before
|
|
the interface function returns.
|
|
|
|
These may be nested: One cancellation point function may call another which
|
|
may call another, etc. The ``select()`` function, for example, calls
|
|
``poll()`` which calls ``sem_wait()`` so the nesting will be three deep.
|
|
|
|
These two cancellation point hooks behave in a complementary way.
|
|
``enter_cancellation_point()`` increments the nesting level and
|
|
``leave_cancellation_point()`` decrements the nesting level.
|
|
No action is taken unless the nesting level is zero then, in that case,
|
|
if there is a pending cancellation then the thread exists normally
|
|
by simply calling ``pthread_exit()``.
|
|
|
|
|
|
pthread_cancel()
|
|
================
|
|
|
|
When ``pthread_cancel()`` is called, it will check if the deferred
|
|
cancellation mode is enabled for the target pthread. If so it will:
|
|
|
|
1. Mark the cancellation as pending.
|
|
2. If the thread is waiting on a semaphore or message queue (and is in
|
|
a cancelable state), it will wake it just in the same was as if
|
|
the thread received a single (except with ``ECANCELED``
|
|
instead of ``EINTR``).
|
|
|
|
If, on the other hand, the asynchronous mode is enabled (and the thread is
|
|
in a cancelable state), ``pthread_cancel()`` will, instead,
|
|
kill the thread immediately.
|
|
|
|
.. note:: For pthreads, the cancelable state is controlled by the interface
|
|
``pthread_setcancelstate().`` This allows the pthread to disable or
|
|
re-enable cancelability. If the thread is not cancelable when
|
|
``pthread_cancel()`` is called, it will never do more than just mark
|
|
the cancellation as pending. When the cancelable state is
|
|
re-enabled, then any pending cancellation will take effect.
|
|
|
|
|
|
select()
|
|
========
|
|
|
|
Let's walk through the case of ``select()`` to see how it works:
|
|
|
|
* When ``select()`` is called, it allocates some memory that it needs to
|
|
handle the conversion to ``poll()``.
|
|
* It then calls ``poll()`` which sets up the poll with all of the relevant
|
|
drivers and, eventually, calls ``sem_wait()``.
|
|
|
|
So this pthread will spend almost all of its time waiting in ``sem_wait()``
|
|
and that is the most likely state of the pthread when ``pthread_cancel()``
|
|
is called with deferred cancellation eanbled.
|
|
|
|
* When ``pthread_cancel()`` is called, it will set the pending cancellation
|
|
state and wake up ``sem_wait()`` with the ``ECANCELED`` error.
|
|
* ``sem_wait()`` will see the pending cancellation, but will do nothing
|
|
because the nesting level is decremented only to two. It will return to
|
|
``poll()`` with the ``ECANCELED`` error. Note that the semaphore is left
|
|
in a healthy state.
|
|
* ``poll()`` will tear-down down the poll from all of the drivers and call
|
|
``leave_cancellation_point()`` before it exits. Again, it will see the
|
|
pending cancellation but will do nothing because the nesting level
|
|
decrements only to 1. Note that since ``poll()`` does the complete
|
|
tear-down and all of the drivers are left in a healthy state.
|
|
* Finally, ``select()`` cleans up all of its memory allocations and calls
|
|
``leave_cancellation_point()``. This time, the nesting level decrements
|
|
to zero and ``leave_cancellation_point()`` will call ``pthread_exit()``
|
|
with everything in a proper state.
|
|
|
|
If asynchronous pthread cancellation were selected, then the behavior would
|
|
be very different. The first two steps would be the same but then:
|
|
|
|
* When ``pthread_cancel()`` is called, the thread would be immediately killed.
|
|
The semaphore would be left in the locked state; the memory allocated by
|
|
``select()`` would be stranded; drivers would be left with stale pointers
|
|
to stranded memory. Not a good state to leave the system!
|
|
* ``select()`` will not return to the caller in either case.
|
|
|
|
|
|
Application Interfaces
|
|
======================
|
|
|
|
The following pthread application interfaces are available to manage cancellation:
|
|
|
|
* ``pthread_cancel()`` cancels a thread.
|
|
* ``pthread_setcancelstate()`` can enable or disable a cancellation.
|
|
* ``pthread_setcanceltype()`` can be used to switched between asynchronous
|
|
and deferred thread cancellation.
|
|
* ``pthread_testcancel()`` can be used to force a cancellation point.
|
|
|
|
|
|
Task Deletion
|
|
=============
|
|
|
|
NuttX also supports some non-standard interfaces for task deletion
|
|
(i.e., cancellation) of tasks as well. The cancellation point logic
|
|
is identical and the task application interfaces are very similar:
|
|
|
|
* ``task_delete()`` deletes (cancels) a task.
|
|
* ``task_setcancelstate()`` can enable or disable task cancellation.
|
|
* ``task_setcanceltype()`` can be used to switched between asynchronous
|
|
and deferred task cancellation.
|
|
* ``task_testcancel()`` can be used to force a cancellation point.
|
|
|
|
|
|
Design Issues
|
|
=============
|
|
|
|
.. note:: The issue addressed in this paragraph is covered by
|
|
`Issue 619 <https://github.com/apache/nuttx/issues/619>`_
|
|
has been corrected in the code base through a series of changes
|
|
culiminating in
|
|
`PR 733 <https://github.com/apache/nuttx/pull/733>`_.
|
|
This discussion still has value, however, as a warning
|
|
for things that one needs to take into consideration
|
|
when designing logic within the OS.
|
|
|
|
There is one logical problem in many parts of the system. The following
|
|
sequence of code appears in many places in the code base.
|
|
It will work not work with thread/task cancellation:
|
|
|
|
.. code-block:: c
|
|
|
|
void some_lock(FAR sem_t *sem)
|
|
{
|
|
while ((ret = nxsem_wait(sem)) < 0)
|
|
{
|
|
DEBUGASSERT(ret = -EINTR || ret = -ECANCELED);
|
|
}
|
|
}
|
|
|
|
That logic will makes it impossible to cancel the thread.
|
|
Let me explain why:
|
|
|
|
* The fact that ``-ECANCELED`` is occurring means that some other task
|
|
tried to cancel this one.
|
|
* The logic sequence is probably something like:
|
|
|
|
* OS Function ``A`` is called by the application. Function ``A`` is
|
|
a cancellation point. It calls ``enter_cancellation_point()`` on entry
|
|
and ``leave_cancellation_point()`` on exit. If the application task is
|
|
canceled, then the call to ``leave_cancellation_point()`` is where
|
|
the task actually ends (by simply calling exit()).
|
|
* OS Function ``A`` calls some internal Function ``B`` which has the above
|
|
killer logic in it. It calls ``some_lock()`` and the task is suspended
|
|
on waiting to get a count on the semaphore.
|
|
|
|
When the task is canceled, ``nxsem_wait()`` wakes up with the ``-ECANCELED``
|
|
error and returns that error to Function ``B``. But because Function ``B``
|
|
just loops and calls ``nxsem_wait()`` again, the task does not terminate;
|
|
it just goes back to waiting.
|
|
|
|
The end result is that Function ``B`` does not return to Function ``A`` so
|
|
``leave_cancellation_point()`` is never called and the task is not canceled.
|
|
The fix requires a little re-design. The locking function would
|
|
have to be like:
|
|
|
|
.. code-block:: c
|
|
|
|
int some_lock(FAR sem_t *sem)
|
|
{
|
|
while ((ret = nxsem_wait(sem)) < 0)
|
|
{
|
|
DEBUGASSERT(ret = -EINTR || ret = -ECANCELED);
|
|
if (ret != -EINTR)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
It then returns a success/failure indication. Calling logic would also have
|
|
to be modified to detect the failure case and handle it appropriately.
|
|
In the case that the failure case, the calling logic (Function ``B`` in this
|
|
example) needs to clean up and return the failure upstream (to Function ``A``)
|
|
which must also clean up. Then, eventually, ``leave_critical_section()`` will
|
|
be called and the function will exit correctly.
|
|
|
|
|
|
Issue 619 Discussion
|
|
====================
|
|
|
|
.. note:: The
|
|
`Issue 619 <https://github.com/apache/nuttx/issues/619>`_
|
|
has been corrected in the code base through a series of changes
|
|
culiminating in
|
|
`PR 733 <https://github.com/apache/nuttx/pull/733>`_.
|
|
|
|
nxsem_wait_uninterruptible()
|
|
----------------------------
|
|
|
|
In NuttX, thread or tasks may be canceled using ``pthread_cancle()`` or
|
|
``task_delete()``. These can also be canceled via certain signals if default
|
|
signal actions are enabled.
|
|
|
|
When threads or tasks are canceled asynchronously, resources may be stranded.
|
|
For example, if memory is allocated, it will not be deallocated when the
|
|
task/thread is canceled in the FLAT build. These kind on leaks can be handled
|
|
by the application with functions registered with ``pthread_cleanup()`` or
|
|
``on_exit()``, but others cannot.
|
|
|
|
The generic solution for NuttX is through the use of cancellation points.
|
|
Cancellation points are a mechanism that assures that all resources used
|
|
internal by OS interface calls are cleaned-up automatically at designated
|
|
cancellation points. An interface is normally a cancellation point if
|
|
it has any possibility of blocking.
|
|
|
|
In NuttX, when threads are blocked waiting of a semaphore when they are
|
|
canceled, ``nxsem_wait()`` will return ``-ECANCELED`` and the correct behavior
|
|
to let the error ripple back to the cancellation point where the thread/task
|
|
will be canceled after all resources have been cleaned up.
|
|
|
|
Many internal OS functions use ``nxsem_wait_uninterruptible()`` for waiting.
|
|
``nxsem_wait_uninterruptible()`` is defined in ``include/nuttx/semaphore.h``.
|
|
``nxsem_wait_uninterruptible()`` wraps a call to ``nxsem_wait()`` in
|
|
a loop like:
|
|
|
|
.. code-block:: c
|
|
|
|
static inline int nxsem_wait_uninterruptible(FAR sem_t *sem)
|
|
{
|
|
int ret;
|
|
|
|
do
|
|
{
|
|
/* Take the semaphore (perhaps waiting) */
|
|
|
|
ret = nxsem_wait(sem);
|
|
}
|
|
while (ret == -EINTR || ret == -ECANCELED);
|
|
|
|
return ret;
|
|
}
|
|
|
|
The correct behavior is to continue waiting on ``-EINTR`` only.
|
|
If the task/thread is awakened by a signal just continue waiting.
|
|
The behavior for ``-ECANCELED`` is not correct. It should not loop.
|
|
Looping like this will prevent the cancellation from working.
|
|
Instead of returning through the cancellation point, the code will be stuck
|
|
in the above loop (in defferred cancellation mode) and cannot be cancelled.
|
|
|
|
The correct behavior is to return when ``-ECANCELED`` is encountered,
|
|
not to continue looping:
|
|
|
|
* GOOD: ``while (ret == -EINTR);``
|
|
* BAD: ``while (ret == -EINTR || ret == -ECANCELED);``
|
|
|
|
The only complexity to this suggested change is that the callers of
|
|
``nxsem_wait_uninterruptible()`` must also check the return value for any
|
|
errors and make sure that that error is returned to the caller.
|
|
The error must propogate all the way back up to the cancellation point.
|
|
|
|
These are other related inline functions in ``include/nuttx/semaphore.h``
|
|
that behave in this same way. This issue applies to them as well.
|
|
|
|
.. important:: If cancellation points are enabled, the default cancellation
|
|
mode should be deferred mode.
|
|
|
|
.. note:: There are three interfaces affected by this issue:
|
|
``nxsem_wait_uninterruptible()`` as discussed above, but also
|
|
``nxsem_timedwait_uninterruptible()`` and
|
|
``nxsem_tickwait_uninteruptible()`` which are closely related.
|
|
|
|
Mutual Exclusion Semaphores vs. Signaling Semaphores
|
|
----------------------------------------------------
|
|
|
|
Semaphore have two usages in NuttX:
|
|
|
|
* Mutual Exclusion Semaphores: Protect the shared data and force mutually
|
|
exclusive use of semaphores.
|
|
* Signling Semaphores: Used to wait for events to be signaled asynchronously.
|
|
|
|
Should we return ``-ECANCEL/-EINTR`` for both? Or only for ithe second?
|
|
I think both should work the same: For both the semaphore wait should be
|
|
terminated and correct error should be returned.
|
|
|
|
Mutual Exclusion Semaphores
|
|
---------------------------
|
|
|
|
If the API requires returning ``EINTR`` if a signal is received, we should
|
|
return the error. This is required in many POSIX interfaces and don't see
|
|
how you could avoid that. However, the mutual exclusion semaphores are
|
|
the just there for the correctness in the design. There is seldom any real
|
|
competitor for the exclusion semaphore so, in general, they do not block and,
|
|
hence, would never generate any error.
|
|
|
|
And even if taking the mutual exclusion semaphore does cause the thread
|
|
to block, it should block for only a very brief moment of time and the
|
|
likelihood of a signal received or of the task being canceled is very small.
|
|
|
|
But if there is even a small possibility of that event happening,
|
|
it WILL happen in an embedded system (Murphy's law).
|
|
|
|
But if during the that brief moment while the thread is blocked,
|
|
a signal is received, I think that it is correct to return ``-EINTR``.
|
|
This is not so critical only because such an event it is rare.
|
|
But the specification requires it so we should do it.
|
|
|
|
When to Return ECANCELED
|
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The ``ECANCELED`` error is **ABSOLUTELY** critical to return under any
|
|
circumstance. That cancellation notification is received only once and
|
|
it if is ignored, then the thread will not cancel it will continue to run.
|
|
It really must return immediately with ``ECANCELED`` for the cancellation
|
|
to work quickly and reliably, where ever ``ECANCELED`` is detected.
|
|
Ignoring it is a hard bug in any case.
|
|
|
|
Signaling Semaphores
|
|
^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The signaling semaphores are a little different story. They may block
|
|
for a very long time, for example, waiting for the receipt of data that
|
|
may never come. For example, a read from a serial port will hang indefinitely
|
|
if nothing is received on the serial port. So if a signal is received or
|
|
if the thread is canceled, then it is essential to terminate the semaphore
|
|
wait and return the error condition.
|
|
We should have no difference of opinion on that.
|
|
|
|
There is really no difference in the actions that must be taken if a signal
|
|
is received of if the task is canceled. Both mutual exclusion semaphores
|
|
and signaling semaphores should wake up and return the error condition.
|
|
The only real differences is that mutual exclusion semaphores either
|
|
do not block or, if they do, the do not block as long.
|
|
|
|
Mutual Exclusion Semaphores and Serialization
|
|
---------------------------------------------
|
|
|
|
There are situations were the task may block for a very long time,
|
|
even indefinitely, on a mutual exclusion semaphore. Consider the following
|
|
scenario: A driver that reads data could have a sequence like this:
|
|
|
|
1. Get exclusive access to the driver by taking a semaphore.
|
|
2. Wait on a signaling semaphore for data (might be a long time).
|
|
3. Release the mutual exclusion semaphore.
|
|
|
|
Now suppose there are two applications, Task ``A`` and Task ``B``,
|
|
that open the driver and try to read from it. The first, Task ``A``, will get
|
|
the mutual exclusion semaphore at ``(1)`` and then will hang waiting
|
|
for data at (2). It may have to wait for a very long time to receive data.
|
|
When re-entered by the second, Task ``B``, it will hang at ``(1)``
|
|
also for very long time. Task ``B`` is effectively queued and cannot event
|
|
begin its wait on the signaling semphore until Task ``A`` receives its data
|
|
and releases the mutual exclusion semaphore. This is normal behavior
|
|
and exactly, how this kind of driver is supposed to work:
|
|
This use of mutual exclusion impelements serialization of I/O.
|
|
|
|
But now suppose that Task ``B`` receives a signal interrupt or a cancellation
|
|
event while waiting on the mutual exclusion semaphore. It MUST terminate the
|
|
wait and return ``-EINTR`` or ``-ECANCEL`` immediately. Task ``A`` may never
|
|
receive data and if that action is not taken, the signal interrup
|
|
or cancellation event is lost and the functionality has failed.
|
|
|
|
When to Return EINTR
|
|
^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Returning ``EINTR`` really only applies to ``read()`` and ``write()``
|
|
(and ``open()``) functions as covered by Issue #669 . In other places,
|
|
``EINTR`` can be ignored. ``open()`` and ``close()`` should also
|
|
return ``EINTR``. But I don't think that is very meaningful for ``close()``
|
|
since most people ignore the return value from ``close()``.
|
|
But, on the other hand, ``ECANCELED`` can never be ignored under any
|
|
condition.
|
|
|
|
Detecting, Remembering, and Propagating Signal Interrupts and Cancellation Events
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Currently, the only way for an OS interface function to know if a signal
|
|
was received is to be awakened with an ``EINTR`` error. That is why
|
|
it is critical to always return the ``EINTR`` error and conform with
|
|
the POSIX requirements.
|
|
|
|
However, if a signal is received while the OS interface is NOT waiting,
|
|
then there is no way to know if the signal was received. I think that
|
|
is a limitation in the current design. There probably should be some latching
|
|
indication, perhaps in the TCB, that a signal interrupt has been received.
|
|
|
|
There is already a ``TCB_FLAG_CANCEL_PENDING`` in the TCB that will tell us
|
|
that the if the task has been canceled. That flag is tested in the
|
|
``leave_cancellation_point()`` function so I don't think that there is any
|
|
corresponding issue for thread cancellation. We just need to make sure that
|
|
all waits are aborted if ``ECANCELED`` is received and let the error
|
|
indication ripple all they back to the ``leave_cancellation_point()``
|
|
function. Then the function will exit cleanly, safely, and quickly.
|
|
|
|
Interestingly, the ``ECANCELED`` error will never be seen by the application.
|
|
It just triggers the return uwind sequence where all resources are recovered
|
|
and finally until ``leave_cancellation_point()`` is called. Then the thread
|
|
will exit before it returns to the applicaton.
|
|
|
|
You can see all of this working in the ``board/sim/sim/sim/configs/ostest``
|
|
configuration if you also enable::
|
|
|
|
CONFIG_CANCELLATION_POINTS=y
|
|
CONFIG_PTHREAD_CLEANUP=y
|
|
|
|
There is a ``cancel.c`` test within the OS test, but the more interesting test
|
|
is the ``pthread_cleanup.c`` test. You can see how all this works when the code
|
|
unwinds with the ``ECANCELED`` error and calls ``leave_cancellation_point()``.
|
|
Just single step through ``pthread_cond_wait()`` to see this.
|
|
``pthread_cond_wait()`` is a ``cancellation_point()`` and that is where
|
|
the thread exit will occur.
|
|
|
|
`PR#749 <https://github.com/apache/nuttx/pull/749>`_ adds those settings
|
|
to that ``sim:otest`` defconfig.
|