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>
872 lines
33 KiB
ReStructuredText
872 lines
33 KiB
ReStructuredText
.. _smp:
|
||
|
||
===============================
|
||
SMP (Symmetric MultiProcessing)
|
||
===============================
|
||
|
||
Definition
|
||
==========
|
||
|
||
According to Wikipedia:
|
||
|
||
"Symmetric multiprocessing (SMP) involves a symmetric
|
||
multiprocessor system hardware and software architecture where two or more
|
||
identical processors connect to a single, shared main memory, have full access
|
||
to all I/O devices, and are controlled by a single operating system instance
|
||
that treats all processors equally, reserving none for special purposes.
|
||
Most multiprocessor systems today use an SMP architecture.
|
||
In the case of multi-core processors, the SMP architecture applies to
|
||
the cores, treating them as separate processors.
|
||
|
||
(..)
|
||
|
||
SMP systems are tightly coupled multiprocessor systems with a pool
|
||
of homogeneous processors running independently, each processor executing
|
||
different programs and working on different data and with capability
|
||
of sharing common resources (memory, I/O device, interrupt system and so on)
|
||
and connected using a system bus or a crossbar."
|
||
|
||
-- Source: https://en.wikipedia.org/wiki/Symmetric_multiprocessing.
|
||
|
||
|
||
Development Status
|
||
==================
|
||
|
||
SMP support is complete and stable in NuttX on several multi-core platforms.
|
||
|
||
|
||
Enabling SMP
|
||
============
|
||
|
||
SMP can be enabled on NuttX with the following configuration settings:
|
||
|
||
* ``CONFIG_SMP`` - Enables support for Symmetric Multi-Processing (SMP)
|
||
on a multi-CPU platform.
|
||
* ``CONFIG_SMP_NCPUS`` - This value identifies the number of CPUs support
|
||
by the processor that will be used for SMP.
|
||
* ``CONFIG_SMP_IDLETHREAD_STACKSIZE`` - Each CPU will have its own IDLE task.
|
||
System initialization occurs on CPU0 and uses
|
||
``CONFIG_IDLETHREAD_STACKSIZE``.
|
||
This setting provides the stack size for the IDLE task on CPUS 1
|
||
through ``(CONFIG_SMP_NCPUS-1)``.
|
||
|
||
This section provides the origin design specification for the implemention.
|
||
As a result, you may find that the test uses future and conditional tenses
|
||
when describing the implementation of SMP on NuttX.
|
||
|
||
This design has been maintained and now reflects the current "as-built"
|
||
state of SMP in NuttX.
|
||
|
||
|
||
Design Requirements
|
||
===================
|
||
|
||
The basic design requirements are pretty simple:
|
||
|
||
1. Need to be able to bring up NuttX running on multiple CPUs.
|
||
2. Need data structures to manage multiple active tasks.
|
||
3. Need to be able to schedule tasks on other CPUs.
|
||
4. Need to be able to modify tasks running on other CPUs.
|
||
5. Need to be able to manage critical sections on all CPUs.
|
||
6. Need spinlocks to block on all CPUs in all cases: semaphore,
|
||
signal, message queue, etc.
|
||
7. Need to understand how some non-standard NuttX operations things
|
||
like disabling pre-emption work.
|
||
|
||
|
||
Data Structures
|
||
===============
|
||
|
||
Task Lists
|
||
----------
|
||
|
||
At the core of the NuttX design are data structures called
|
||
**Task Control Blocks** or just **TCB**.
|
||
|
||
These data structures contain everything-you-need-to-know about
|
||
a thread or task.
|
||
These TCBs are retained in lists within the RTOS.
|
||
The state of a thread or task is then determined by which list
|
||
the TCB resides in.
|
||
|
||
The Read-To-Run Task List
|
||
-------------------------
|
||
|
||
On such TCB list is of particular importance in the implementation of SMP.
|
||
That is the so-called ready-to-run list, ``g_readytorun``.
|
||
That list contains the TCB of every task or thread that is not blocked
|
||
in any way and so is, well, ready to run.
|
||
|
||
The ``g_readytorun`` is a prioritized list. The lowest priority task
|
||
is in the list is the one at the end of the list and that must always
|
||
by the IDLE task.
|
||
|
||
That is the only task/thread that is permitted to have priority 0.
|
||
The highest priority, read-to-run task is always at the head of
|
||
``g_readytorun`` and must be the currently executing task.
|
||
|
||
All other tasks after this is eligle to run, but not currently running.
|
||
|
||
The Assigned Task List
|
||
----------------------
|
||
|
||
In order to support SMP, the function of the ``g_readytorun`` list
|
||
must change. This ``g_readytorun`` should still exist but it should
|
||
now contain only:
|
||
|
||
1. Only tasks/threads that are eligible to run, but not currently running, AND
|
||
2. Tasks/threads that have not been assigned to a CPU.
|
||
|
||
For SMP support there should be an array of assigned tasks like:
|
||
|
||
.. code-block:: c
|
||
|
||
volatile dq_queue_t g_assignedtasks[CONFIG_SMP_NCPUS];
|
||
|
||
Where ``CONFIG_SMP_NCPUS`` is the configured number of CPUs supported
|
||
by the processors. As its name suggests, on ``g_assignedtasks`` queue for
|
||
``CPU n`` would contain only tasks/threads that are assigned to CPU n.
|
||
Threads would be assigned a particular CPU by one of two mechanisms:
|
||
|
||
1. (Semi-)permanently through an RTOS interfaces such as
|
||
``pthread_attr_setaffinity()``, OR
|
||
2. Temporarily through new scheduling logic.
|
||
|
||
Tasks/threads that are assigned to a CPU via an interface like
|
||
``pthread_attr_setaffinity()`` would never go into the ``g_readytorun`` list,
|
||
but would only go into the ``g_assignedtasks[n]`` list for the CPU n to which
|
||
the thread has been assigned.
|
||
Hence, the ``g_readytorun`` list would hold only unassigned tasks/threads.
|
||
|
||
An indication within the TCB would indicated whether or not a task/thread
|
||
is assigned to a CPU and, if so, which CPU it is assigned to.
|
||
|
||
Scheduling logic would temporarily assign a task or thread to a CPU.
|
||
The assignment is only temporary because state data in the TCB would indicate
|
||
that the task is unassigned when, hence, it could be returned
|
||
to the ``g_readytorun`` list later.
|
||
|
||
The assigned tasks lists lists would be prioritized.
|
||
The highest priority task, and the one currently executing on CPU n would be
|
||
the one at the head of ``g_assignedtasks[n]``.
|
||
Tasks after the active task are ready-to-run and assigned to this CPU.
|
||
The tail of this assigned task list, the lowest priority task,
|
||
is always the CPU's IDLE task.
|
||
|
||
The CPU n scheduling logic would execute whenever the currently running task
|
||
is removed from the head of ``g_assignedtasks[n]``.
|
||
The algorithm might be something like:
|
||
|
||
.. code-block:: c
|
||
|
||
/* Is the assigned task list for the CPU empty? */
|
||
|
||
if (g_assignedtasks[cpu].head == NULL)
|
||
{
|
||
/* No.. Is the task at the head of the assigned list for the CPU lower
|
||
* in priority that the current (unassigned) task at the head of the
|
||
* ready-to-run list?
|
||
*/
|
||
|
||
FAR struct tcb_s *rtcb = (FAR struct tcb_s *)g_readytorun.head ;
|
||
FAR struct tcb_s *atcb = (FAR struct tcb_s *)g_assignedtasks[cpu].head;
|
||
if (atcb->sched_priority < rtcb->sched_priority)
|
||
{
|
||
/* Remove the TCB from the head of the g_readytorun list. */
|
||
|
||
/* Add that TCB to the g_assignedtasks[cpu] list (it will go at the
|
||
* head of the list).
|
||
*/
|
||
}
|
||
|
||
/* Now activate the task at the head of the g_assignedtasks[cpu] list on
|
||
* the CPU.
|
||
*/
|
||
|
||
}
|
||
|
||
The Current Task
|
||
----------------
|
||
|
||
There is a lot of logic in the RTOS now that obtains the TCB for the currently
|
||
excuting task by examining the head of the ``g_readytorun`` list.
|
||
You will see this assignment in many places, both in the core OS logic
|
||
in ``nuttx/sched`` but also in architecture-specific logic under
|
||
``nuttx/arch``:
|
||
|
||
.. code-block:: c
|
||
|
||
FAR struct tcb_s *rtcb = this_task();
|
||
|
||
Where ``this_task()`` is a macro defined in ``nuttx/sched/sched.h``
|
||
and expands as follows:
|
||
|
||
.. code-block:: c
|
||
|
||
#define current_task(cpu) ((FAR struct tcb_s *)g_readytorun.head)
|
||
#define this_cpu() (0)
|
||
#define this_task() (current_task(this_cpu))
|
||
|
||
Of course, that would not work with the proposed changes.
|
||
We would need to then get the TCB of the currently executing task/thread
|
||
for CPU n from the head of ``g_assignedtasks[n]``.
|
||
I would propose a replacing the above assignment with a macro like
|
||
``current_task()`` where that macro might expand to:
|
||
|
||
.. code-block:: c
|
||
|
||
#ifdef CONFIG_SMP
|
||
# define current_task(cpu) ((FAR struct tcb_s *)g_assignedtasks[cpu].head)
|
||
# define this_cpu() up_cpu_index()
|
||
#else
|
||
# define current_task(cpu) ((FAR struct tcb_s *)g_readytorun.head)
|
||
# define this_cpu() (0)
|
||
#endif
|
||
#define this_task() (current_task(this_cpu))
|
||
|
||
where ``up_cpu_index()`` is some new MCU specific interface that will
|
||
return an index associated with the currently active CPU.
|
||
|
||
.. note::
|
||
|
||
This is a two step operations: Step 1. Get the CPU number and
|
||
Step 2: Use the CPU number as an index into the
|
||
``g_assignedtasks[]`` array of lists. **This must be atomic!**
|
||
The schedule should be locked to assure that the task
|
||
is not suspended after fetching the CPU number then restarted
|
||
on a different CPU to access the ``g_assignedtasks[]`` array
|
||
of lists.
|
||
|
||
The IDLE Task
|
||
-------------
|
||
|
||
Without SMP, the ``g_readytorun`` list always ends with the TCB of IDLE task.
|
||
|
||
It is always guaranteed to be at the end of the list because the list
|
||
is prioritized and because the IDLE task has an impossibly low priority
|
||
that no other task/thread could have.
|
||
|
||
The IDLE task is necessary because it gives the CPU something to execute
|
||
when there is nothing else to be done.
|
||
|
||
But with SMP, there are multiple CPUs that need something to do when there
|
||
is nothing else to do. We are tentatively thinking that each CPU needs its own
|
||
IDLE thread whose TCB would reside at the end of each
|
||
``g_assignedtasks[cpu]`` list. But that does feel wasteful
|
||
|
||
I am not certain the mechanism as of this writing, but I assume that
|
||
the ``nx_start()`` initialization logic would need to create an IDLE task
|
||
for each CPU and assign each IDLE task to each CPU.
|
||
|
||
CPU Index
|
||
---------
|
||
|
||
In order to access arrays indexed by a CPU ID value, some method must be
|
||
generated to provide the CPU ID that the currently executing task
|
||
is running on. To provide this index value, an interface
|
||
``up_cpu_index()`` is proposed.
|
||
|
||
For ARM, the implementation of ``up_cpu_index()`` can be accomplished
|
||
by reading the CP15 Multiprocessor Affinity Register (MPDIR).
|
||
That register has a 2 bit field index provides exactly the index that
|
||
we need for the SMP implementation.
|
||
|
||
Looking at how Linux does this, Linux uses an interface called ``get_cpu()``
|
||
which is analogous to the proposed ``up_cpu_index()``.
|
||
``get_cpu()`` maps to ``smp_processor_id()`` and if debug options are
|
||
not enabled, this further maps to ``raw_smp_procesor_id()``.
|
||
For the case of ARM, this maps to ``(current_thread_info()->cpu)``
|
||
where ``current_thread_info()`` is a location at the far end of the
|
||
allocated stack:
|
||
``(current_stack_pointer & ~(THREAD_SIZE-1))`` and
|
||
``THREAD_SIZE`` is ``(PAGE_SIZE << THREAD_SIZE_ORDER)``.
|
||
|
||
So, to make that long story short, Linux solves the problem by putting
|
||
some magic information at the base of far end of each stack when
|
||
a context switch occurs (and when the CPU is also known).
|
||
That magic information can then just be recovered using the thread's
|
||
stack pointer at any time. This is part of the basic implementation
|
||
of Thread Local Storage (TLS) in Linux.
|
||
|
||
Something similar could be done with NuttX and would require:
|
||
|
||
1. Special aligned stack allocation,
|
||
2. Logic to write the CPU index into the stack when each thread
|
||
is [re-]started.
|
||
|
||
This would also place an upper limit on the size of the stack:
|
||
If we are going to find the far end of the stack by simply ANDing out
|
||
the lower bits, then size of that mask would also determine
|
||
the maximum size of the stack.
|
||
|
||
However, I believe that using the information from the MPIDR register
|
||
is a better general solution. Counter-arguments are:
|
||
|
||
1. There may be some architectures that do not have such a simple mechanism.
|
||
2. TLS has value in any event.
|
||
3. The stack-based TLS is in user-accessible memory and could be used
|
||
by applications in protected and kernel builds.
|
||
|
||
|
||
System Startup
|
||
==============
|
||
|
||
I assume that initially, only one CPU is active.
|
||
System initialization would then occur on that single thread.
|
||
At the completion of the initialization of the OS, just before beginning
|
||
normal multitasking, the additional CPUs would be started.
|
||
|
||
Each CPU would be provided the entry point to is IDLE task when started.
|
||
Perhaps the MCU interface would be something like:
|
||
|
||
.. code-block:: c
|
||
|
||
int up_cpu_start(int cpu, main_t idletask);
|
||
|
||
The OS initialization logic would call this function repeatedly
|
||
until each CPU is started.
|
||
|
||
|
||
Scheduler Interactions
|
||
======================
|
||
|
||
In the general case, the scheduler should have full control over the current
|
||
state of all tasks. It must make that that if there are N CPUs that the top N
|
||
highest priority tasks are running.
|
||
|
||
Srict priority scheduling is the requirement, but perhaps the scheduling logic
|
||
could do some load balancing to distribute work as evenly as possible
|
||
over the CPUs. When a new task or thread becomes ready to run,
|
||
the scheduler must include some heuristics for assigning that task to a CPU
|
||
to achieve some optimal performance.
|
||
|
||
There are complications to how one CPU controls the tasks already running
|
||
on another CPU. To determine a task should run, you would need to be able to:
|
||
|
||
* Keep the task data structures stable while they are being analyzed.
|
||
* Find the lowest priority running task which could be on any CPU.
|
||
* If that priority is lower than the priority task, then replace it with
|
||
the new task at the head of the ``g_assignedtasks[]`` list.
|
||
* If not, find the task with the next lowest priority and compare that one.
|
||
* Continue until until the new task is assigned to a CPU or until
|
||
it is determined that all of the currently running tasks are higher priority
|
||
than the new task. In that base, the new task should be added
|
||
to the ``g_readytorun`` list.
|
||
|
||
To support this behavior, I think that the following new MCU interfaces
|
||
will be needed:
|
||
|
||
.. code-block:: c
|
||
|
||
int up_cpu_pause(int cpu);
|
||
|
||
Which would stop execution on CPU0, saving the state of the currently running
|
||
task so that it may be resumed. And:
|
||
|
||
.. code-block:: c
|
||
|
||
int up_cpu_resume(int cpu);
|
||
|
||
Restart the CPU with the task at the head of the ``g_assignedtasks[]`` list.
|
||
|
||
.. note::
|
||
|
||
Please also note the the "Signal Handling" paragraph below.
|
||
The same issue exists for dispatching signals to threads actively
|
||
running on another CPU.
|
||
|
||
|
||
Interrupt Handling
|
||
==================
|
||
|
||
Per-CPU Interrupts
|
||
------------------
|
||
|
||
How will interrupts be taken? On one CPU or on multiple CPUs?
|
||
|
||
This may work different on different hardware platforms.
|
||
This design requires only that:
|
||
|
||
* If the processor supports interrupts on only one CPU, then interrupts
|
||
cannot be nested; further interrupts must be disabled while that interrupt
|
||
handler runs (see Nested Interrupts and High Priority,
|
||
Zero Latency Interrupts.).
|
||
* If the process supports device interrupts on multiple CPUs, the interrupt
|
||
handling on the CPUs is not concurrent: When interrupts are disabled
|
||
on one CPU, they are disabled on all CPUs (unless, of course, if interrupts
|
||
are needed for inter-CPU communication).
|
||
|
||
However, I do not know of any CPU architecture that supports disabling
|
||
interrupts on one CPU from another CPU.
|
||
Instead, critical sections will need to be supported via spinlocks
|
||
as described below.
|
||
|
||
If interrupts can be taken by multiple CPUs then any data structures used
|
||
for interrupt handling would also need to become and array indexed by the
|
||
CPU number. Most architectures current use a data structure defined like:
|
||
|
||
.. code-block:: c
|
||
|
||
volatile uint32_t *g_current_regs;
|
||
|
||
Which would have to become an array like:
|
||
|
||
.. code-block:: c
|
||
|
||
volatile uint32_t *g_current_regs[CONFIG_SMP_NCPUS];
|
||
|
||
|
||
System Calls
|
||
============
|
||
|
||
System Calls are normally implemented via software interrupts.
|
||
|
||
The System Call software interrupt should run on the same CPU as does
|
||
the logic that generated the System Call or, alternatively,
|
||
the design must have some way of obtaining the index of the CPU
|
||
that generated the System Call.
|
||
|
||
|
||
Critical Sections
|
||
=================
|
||
|
||
A critical section is a set of statements that must be able to execute
|
||
exclusively. Higher level applications will, of course, use OS application
|
||
interfaces such as ``sem_wait()`` and ``sem_post()`` to manage critical
|
||
sections. But within the OS, for example, in the low level implementation
|
||
of ``sem_wait()`` and ``sem_post()``, more primitive, non-standard methods
|
||
must be used to implement critical sections.
|
||
|
||
|
||
.. _spinlocks:
|
||
|
||
Spinlocks
|
||
=========
|
||
|
||
A spinlock is a lock which causes a thread trying to acquire it to simply wait
|
||
in a loop (spin) while repeatedly checking if the lock is available.
|
||
The thread remains active but is not performing a useful task.
|
||
|
||
The use of such a lock is a kind of busy waiting and is used commonly
|
||
in SMP implementations to manage access to resources by multiple CPUs.
|
||
|
||
Spinlock Implementation
|
||
-----------------------
|
||
|
||
In a NuttX implementation, the spinlock would probably involve only:
|
||
|
||
* A memory location with one value, say ``SP_LOCKED``, meaning that the lock
|
||
is taken and another value, ``SP_UNLOCKED``, meaning that the lock
|
||
is available.
|
||
* An integer type memory location that contains the number of the CPU
|
||
holding the lock.
|
||
* An integer type memory location hold the number of counts on the lock.
|
||
* And a loop performs a test-and-set operation: The memory location
|
||
is read by a thread and set to true in one atomic operation.
|
||
If the read value is false, then the thread holds the lock.
|
||
Otherwise, it must loop trying repeatedly until the thread gets the lock.
|
||
|
||
The meaning of the lock is that CPU holding the lock has exclusive access
|
||
to a resource that is shared by multiple CPUs.
|
||
So there is never any reason for two threads on the same CPU to spin:
|
||
If the CPU already holds the lock, additional threads need simply
|
||
only increment the lock count.
|
||
|
||
* If the test-and-set fails in the logic that is spinning, but if the lock
|
||
is held by the logic that that CPU is running on, then the spin logic
|
||
should simply increment the count of locks (which needs to be atomic only
|
||
for single processor).
|
||
|
||
Could this cause one CPU to hog too much resource time?
|
||
Perhaps, been calls to the test-and-set logic, the spinlock should call
|
||
``sched_yield()`` which would at least let other threads
|
||
of the same priority run.
|
||
|
||
* Releasing the lock should be matter of decrementing the lock count
|
||
and if the lock count would decrement to zero, setting the lock value to
|
||
``SP_UNLOCKED``. This will, of course, allow another thread spinning
|
||
on the lock in a different CPU to take the lock for that CPU.
|
||
|
||
The following new, internal OS interfaces are proposed:
|
||
|
||
.. code-block:: c
|
||
|
||
void spin_lock(FAR spinlock_t *lock);
|
||
void spin_unlock(FAR spinlock_t *lock);
|
||
|
||
Where the type ``spinlock_t`` is defined in MCU-specific header files.
|
||
These new spinlock interfaces would also use the MCU-specific interface:
|
||
|
||
.. code-block:: c
|
||
|
||
spinlock_t up_testset(FAR spinlock_t *lock);
|
||
|
||
.. note::
|
||
|
||
A thread may take the lock while running on one CPU, but then later
|
||
be assigned to a different CPU, and then release the lock while
|
||
running on that other CPU. Is there a problem in this?
|
||
Yes, probably. One solution might be lock the thread
|
||
to a CPU if it holds the lock?
|
||
|
||
There is also a risk is that the thread holding the lock will be pre-empted
|
||
by the OS scheduler while holding the lock. If this happens, other threads
|
||
on other CPUs will be left spinning (repeatedly trying to acquire the lock),
|
||
while the thread holding the lock is not making progress towards releasing it.
|
||
The result is an indefinite postponement until the thread holding the lock
|
||
can finish and release it.
|
||
|
||
Spinlock logic can be common. However, there must be a unique instance
|
||
of that common spinlock logic in each OS operation that requires mutually
|
||
exclusive access by a CPU.
|
||
|
||
Now, what will we do with these spinlocks? Is there really a need for them?
|
||
Yes, probably. We will need examine every place in the OS that uses disabling
|
||
of pre-emption or disabling of interrupts to prevent other tasks
|
||
(and interrupts) from executing.
|
||
|
||
All of those cases need to be reconsidered and, most likely, protected
|
||
with spinlocks.
|
||
|
||
Let's next examine all of the cases of how resources are managed in NuttX.
|
||
|
||
Spinlocks in Semaphores, Signals, and Message Queues
|
||
----------------------------------------------------
|
||
|
||
A critical section using ``irqsave()`` and ``irqrestore()`` is already used
|
||
in the implementation of these inter-process communications to enforce
|
||
a critical section.
|
||
|
||
One a single CPU system, disabling interrupts will prevent context switches
|
||
(by prevent the asynchronous events that could cause a context switch)
|
||
and also prevents conflicts with interrupt level processing.
|
||
|
||
I believe that simply replacing ``irqsave()`` and ``irqrestore()`` with
|
||
new proposed functions ``enter_critical_section()`` and
|
||
``leave_critical_section()``, as described below under Disabling Interrupts,
|
||
should be sufficient.
|
||
|
||
These proposed functions include a spinlock to assure that they do enforce
|
||
a critical section.
|
||
|
||
Spinlocks and Data Caches
|
||
-------------------------
|
||
|
||
If spinlocks are used in a system with a data cache, then there may be
|
||
a problem with cache coherency in some CPU architectures.
|
||
|
||
When one CPU modifies the spinlock, the changes may not be visible
|
||
to another CPU if it does not share the data cache.
|
||
That would cause failure in the spinlock logic.
|
||
|
||
Flushing the D-cache on writes and invalidating before a read
|
||
is not a good option. Spinlocks are normally 8-bits in size and cache
|
||
lines are typically 32-bytes so that would have side effects unless
|
||
the spinlocks were made to be the same size as one cache line.
|
||
|
||
The better option is to add compiler independent "ornamentation"
|
||
to the spinlock so that the spinlocks are all linked together
|
||
into a separate, non-cacheable memory regions.
|
||
Because of region alignment and minimum region mapping sizes
|
||
this could still be wasteful of memory.
|
||
This would work in systems that have both data cache and either an MPU
|
||
(such as Cortex-m7) or an MMU (such as Cortex-Ax).
|
||
|
||
|
||
Disabling Pre-emption
|
||
=====================
|
||
|
||
Pre-emption is disabled via the interface ``sched_lock()``.
|
||
``sched_lock()`` currently works by preventing context switches from the
|
||
currently executing tasks.
|
||
|
||
This prevents other tasks from running (without disabling interrupts)
|
||
and gives the currently executing task exclusive access to the (single)
|
||
CPU resources.
|
||
Thus, ``sched_lock()`` and its companion, ``sched_unlcok()``,
|
||
are used to implement some critical sections.
|
||
|
||
Currnetly, Pre-emption is disabled using a simple lockcount in the TCB.
|
||
When the scheduling is locked, the lockcount is incremented;
|
||
when the scheduler is unlocked, the lockcount is decremented.
|
||
If the lockcount for the task at the head of the ``g_readytorun``
|
||
list has a ``lockcount > 0``, then pre-emption is disabled.
|
||
|
||
No special protection is required since only the executing task
|
||
can modify its lockcount.
|
||
|
||
Certainly, disabling context switches on one CPU would still be possible
|
||
in an SMP model, but it may not be possible to give a task exclusive access
|
||
to the (multiple) CPU resources without stopping the other CPUs:
|
||
Even though pre-emption is disabled, other threads will still be executing
|
||
on the other CPUS.
|
||
|
||
The full dynamics of the behavior of the scheduler logic in this case
|
||
is not certain.
|
||
However, I think that this would be an acceptable behavior provided that:
|
||
|
||
* There is a global lock count ``g_cpu_lockset`` that includes a bit
|
||
for each CPU: If the bit is ``1``, then the corresponding CPU has
|
||
the scheduler locked; if ``0``, then the CPU does not have the scheduler
|
||
locked.
|
||
* Scheduling logic would set the bit associated with the cpu in
|
||
``g_cpu_lockset`` when the TCB at the head of the
|
||
``g_assignedtasks[cpu]`` list transitions has ``lockount > 0``.
|
||
This might happen when ``sched_lock()`` is called, or after
|
||
a context switch that changes the TCB at the head of the
|
||
``g_assignedtasks[cpu]`` list.
|
||
* Similarly, the cpu bit in the global ``g_cpu_lockset`` would be cleared
|
||
when the TCB at the head of the ``g_assignedtasks[cpu]`` list has
|
||
``lockount == 0``. This might happen when ``sched_unlock()`` is called,
|
||
or after a context switch that changes the TCB at the head of the
|
||
``g_assignedtasks[cpu]`` list.
|
||
* Modification of the global ``g_cpu_lockset`` must be protected
|
||
by a simplified spinlock, ``g_cpu_schedlock``. That spinlock would be
|
||
taken when ``sched_lock()`` is called, and released when ``sched_unlock()``
|
||
is called. This assures that the scheduler does enforce the critical
|
||
section. NOTE: Because of this spinlock, there should never be more
|
||
than one bit set in ``g_cpu_lockset`` attempts to set additional bits
|
||
should be cause the CPU to block on the spinlock. However, additional
|
||
bits could get set in ``g_cpu_lockset`` due to the context switches
|
||
on the various CPUs.
|
||
* Each the time the head of a ``g_assignedtasks[]`` list changes
|
||
and the scheduler modifies ``g_cpu_lockset``, it must also set
|
||
``g_cpu_schedlock`` depending on the new state of ``g_cpu_lockset``.
|
||
* Logic that currently uses the currently running tasks lockcount
|
||
should instead use the global ``g_cpu_schedlock``.
|
||
A value of ``SP_UNLOCKED`` would mean that no CPU has pre-emption disabled;
|
||
``SP_LOCKED`` would mean that at least one CPU has pre-emption disabled.
|
||
|
||
Disabling pre-emption is a non-standard feature but the general capability
|
||
is common to many RTOS. But since feature is non-standard and perhaps
|
||
not realizable in the SMP model, another option would be
|
||
to simply eliminate it.
|
||
|
||
|
||
Disabling Interrupts
|
||
====================
|
||
|
||
Closely related to disabling pre-emption is the practice of disabling
|
||
interrupts to get exclusive access to resources.
|
||
|
||
Disabling interrupts is not really so different from disabling
|
||
pre-emption in practice.
|
||
It effectively disables pre-emption by preventing any asynchronous
|
||
events that could cause a context switch and, of course, in addition
|
||
prevents interrupt level processing.
|
||
|
||
So disabling of interrupts is also used in places to implement critical
|
||
sections and, because the similarity in behavior to disabling only
|
||
pre-emption, suffers from the same issues in the SMP environment.
|
||
|
||
Currently, interrupts on the single CPU are enabled and disabled with:
|
||
|
||
.. code-block:: c
|
||
|
||
irqstate_t irqsave(void);
|
||
void irqrestore(irqstate_t flags);
|
||
|
||
Those functions disable interrupts on the single CPU.
|
||
In the SMP environment, they would need to disable interrupts in all CPUs.
|
||
|
||
.. note::
|
||
|
||
The legacy ``irqsave()`` and ``irqrestore()`` have been replaced
|
||
with new functions implemented in the OS,
|
||
``enter_critical_section()`` and ``leave_critical_section()``.
|
||
|
||
These might be implemented as follows (highly simplified):
|
||
|
||
.. code-block:: c
|
||
|
||
spinlock_t g_spu_irqlock = SP_UNLOCKED;
|
||
|
||
#ifdef CONFIG_SMP
|
||
irqstate_t enter_critical_section(void)
|
||
{
|
||
irqstate_t flags = irqsave();
|
||
|
||
spinlock(&g_cpu_irqlock);
|
||
g_cpu_irqset |= (1 << cpu);
|
||
|
||
return flags;
|
||
}
|
||
|
||
void leave_critical_section(irqstate_t flags)
|
||
{
|
||
g_cpu_irqset &= ~(1 << cpu);
|
||
spinunlock(&g_cpu_irqlock);
|
||
irqrestore(flags);
|
||
}
|
||
|
||
There is an unhandled complexities in the above simplified logic.
|
||
Consider this scenario:
|
||
|
||
1. The thread calls ``enter_critical_section()``, disabling interrupts
|
||
on all CPUs and taking the spinlock.
|
||
2. The thread then suspends, waiting for an event. This is actually
|
||
a very standard behavior to suspend with interrupts disabled:
|
||
The system handles this gracefully be simply re-enabling interrupts
|
||
(if they were enabled by the next task to run).
|
||
3. Later, the event occurs, the task is again made ready-to-run,
|
||
and the interrupts are again disabled.
|
||
|
||
But,
|
||
|
||
1. There must be additional logic to release the spinlock
|
||
when the task is suspended.
|
||
2. There must be additional logic to re-acquire the spinlock
|
||
when the task restarts.
|
||
3. Is there any way that the spinlock could already be locked when the task
|
||
restarts? No, I don't think this is possible. If interrupts are disabled
|
||
and the spinlock is locked, then there should be no context switches.
|
||
|
||
There would be additional complexities if ``enter_critical_section()`` were
|
||
called during interrupt handling.
|
||
|
||
Interrupts are disabled during interrupt level processing, however, interrupt
|
||
level logic will attempt to establish critical sections even when
|
||
it does not need to do this: It will call ``enter_critical_section()`` anyway
|
||
because it will use some common logic with non interrupt level code.
|
||
|
||
There are many situations in which use of spinlocks as shown
|
||
in the simplified example will result in deadlock conditions.
|
||
|
||
As a result of these complexities, the full implementation of
|
||
``enter_critical_section()`` and ``leave_critical_section()`` are considerably
|
||
more complex.
|
||
See the logic in the file ``sched/irq/irq_csection.c`` if you are
|
||
really interested in the details.
|
||
|
||
|
||
Pre-Emption Controls and Critical Sections
|
||
==========================================
|
||
|
||
The effect of disabling pre-emption is to prevent to tasks from running
|
||
while on task has disabled pre-emption; the effect of entering a critical
|
||
section, on the other hand, is to:
|
||
|
||
1. Enforce exclusive access to the logic when in the critical section.
|
||
2. Keep the system stable while certain operations are performed.
|
||
3. Disable competing interrupt level activity when possible.
|
||
|
||
In order to keep the system stable within in the critical section
|
||
it is necessary, the critical section will modify the behavior of the
|
||
pre-emption controls.
|
||
The basic result is this modification is that new tasks are not permitted
|
||
to be started or resumed if:
|
||
|
||
1. Pre-emption is disabled, OR
|
||
2. Some other CPU other than the current CPU is in a critical section.
|
||
|
||
The CPU that has entered the critical section must have the ability
|
||
to start and stop tasks. Attempts to start new tasks from other CPUs when
|
||
one CPU is within the critical section is will result in the newly started
|
||
task being postponed in a pending task list, ``g_pendingtasks``.
|
||
|
||
Such pending tasks will only be allowed to run when:
|
||
|
||
1. All CPUs have re-enabled pre-emption, AND
|
||
2. All CPUs have left the critical section.
|
||
|
||
.. note::
|
||
|
||
It can be determined which CPU(s) have the critical section
|
||
by examining ``g_cpu_irqset``.
|
||
|
||
|
||
Signal Handlers
|
||
===============
|
||
|
||
There will be some issues related to how signals are delivered,
|
||
at least in regard to how signal handlers are executed.
|
||
|
||
I am thinking of the case where a signal is sent by a thread running
|
||
on one CPU to a thread running on another CPU that has a signal handler
|
||
installed. This would probably have to work as follows:
|
||
|
||
1. Stop the CPU on which the task is running ``using up_cpu_pause()``,
|
||
2. Schedule the signal action as is done in the existing logic, then
|
||
3. Re-start the CPU with ``up_cpu_resume()`` to resume execution with
|
||
the signal handler.
|
||
|
||
A special wrapper function for ``up_cpu_pause()`` is provided in the OS
|
||
to support this operation:
|
||
|
||
.. code-block:: c
|
||
|
||
int sched_tcb_pause(FAR struct tcb_s *tcb);
|
||
|
||
This function checks if the task associated with tcb is running on another CPU
|
||
and, if so, conditionally calls ``up_cpu_pause()`` to pause execution
|
||
on that CPU. It returns the CPU index of the paused CPU (or a negated
|
||
``errno`` value if no CPU was paused). While the CPU is paused, operations
|
||
can be performed on the data structures associated with the task.
|
||
Then the non-negative CPU index can then be used with
|
||
``up_cpu_resume()`` to restart the paused CPU.
|
||
|
||
This same sequence would have to be followed for other functions
|
||
that might need to modify the behavior of a running task such as
|
||
``task_delete()`` or ``task_restart()``.
|
||
|
||
|
||
Thread Affinity
|
||
===============
|
||
|
||
By default, a thread may run on any CPU.
|
||
There are some semi-standard interfaces that can be used to restrict
|
||
the set of CPUs that a thread may run on.
|
||
This set of CPUs is referred to as the threads affinity mask.
|
||
Semi-standard meaning used on Linux and available in ``GLIBC`` when
|
||
``__GNU_SOURCE`` is defined.
|
||
|
||
There are interfaces to set and get the affinity mask for a task prototyped
|
||
in ``sched.h``.
|
||
|
||
``cpusetsize`` is fixed in NuttX and must be equal to ``sizeof(cpu_set_t)``:
|
||
|
||
.. code-block:: c
|
||
|
||
#ifdef CONFIG_SMP
|
||
int sched_setaffinity(pid_t pid, size_t cpusetsize,
|
||
FAR const cpu_set_t *mask);
|
||
int sched_getaffinity(pid_t pid, size_t cpusetsize, FAR cpu_set_t *mask);
|
||
#endif
|
||
|
||
There are similar interfaces for a ``pthread`` prototyped in ``phtread.h``:
|
||
|
||
.. code-block:: c
|
||
|
||
#ifdef CONFIG_SMP
|
||
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize,
|
||
FAR const cpu_set_t *cpuset);
|
||
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize,
|
||
FAR cpu_set_t *cpuset);
|
||
#endif
|
||
|
||
.. note::
|
||
|
||
The ``_np`` in the naming is to remind you that
|
||
this **interface is non-POSIX**!
|
||
|
||
By default, a child task or pthread inherits the affinity mask of its parent.
|
||
The thread affinity mask for a ``pthread``, however, can also be set before
|
||
the thread is started via ``pthread_create()``:
|
||
|
||
.. code-block:: c
|
||
|
||
#ifdef CONFIG_SMP
|
||
|
||
int pthread_attr_setaffinity_np(FAR pthread_attr_t *attr,
|
||
size_t cpusetsize,
|
||
FAR const cpu_set_t *cpuset);
|
||
|
||
int pthread_attr_getaffinity_np(FAR const pthread_attr_t *attr,
|
||
size_t cpusetsize, cpu_set_t *cpuset);
|
||
|
||
#endif
|
||
|
||
In addition, macros are defined in the header file ``include/sched.h``
|
||
to abstract operations are CPU sets.
|
||
There are several such macros with names like ``CPU_ZERO()``, ``CPU_SET()``,
|
||
``CPU_CLR()``, etc.
|