A critical section is a short sequence of code where exclusive execution is assured by globally disabling other activities while that code sequence executes. When we discuss _critical section_s here we really refer to one of two mechanisms:
enter_critical_section(); the code sequence exits the critical section by calling leave_critical_section(). For the single CPU case, this amounts to simply disabling interrupts but is more complex in the SMP case where spinlocks are also involved.sched_lock()), interrupts remain enabled, but context switches may not occur; the current task is locked in place and cannot be suspended until the scheduler is unlocked (via sched_unlock()).The use of either mechanism will always harm real-time performance.
The effects of critical sections on real-time performance is discussed in Effects of Disabling Interrupts or Pre-Emption on Response Latency. The end result is that a certain amount of jitter is added to the real-time response.
Critical sections cannot be avoided within the OS and, as a consequence, a certain amount of "jitter" in the response time is expected. The important thing is to monitor the maximum time that critical sections are in place in order to manage that jitter so that the variability in response time is within an acceptable range.
NOTE: This discussion applies to Normal interrupt processing. Most of this discussion does not apply to High Performance, Zero Latency Interrupts. Those interrupts are not masked in the same fashion and none of the issues address in this Wiki page apply to those interrupts. That would be topic for another Wiki page.
In order to measure the time that tasks hold critical sections, the OS supports a Critical Section Monitor. This is internal instrumentation that records the time that a task holds a critical section. It also records the amount of time that interrupts are disabled globally. The Critical Section Monitor then retains the maximum time that the critical section is in place, both per-task and globally.
The Critical Section Monitor is enabled with the following setting in the configuration:
CONFIG_SCHED_CRITMONITOR=y
|
When the Critical Section Monitor is enabled, the OS will expect platform-specific logic to export two interfaces to support the timing:
uint32_t up_critmon_gettime(void);
void up_critmon_convert(uint32_t elapsed, FAR struct timespec *ts);
|
The first interface simply provides the current time value in unknown units. NOTE: This function may be called early before the timer has been initialized. In that event, the function should just return a start time of zero.
Nothing is assumed about the units of this time value. The following are assumed, however: (1) The time is an unsigned integer value, (2) the time is monotonically increasing, and (3) the elapsed time (also in unknown units) can be obtained by subtracting a start time from the current time.
The second interface simple converts an elapsed time into well known units for presentation by the ProcFS file system.
ARMv7-M platforms that support the Data Watchpoint and Trace (DWT) Unit support a very simply implementation of these timers using the DWT_CYCNT register:
#include <stdint.h> /* For uint32_t */
#include <time.h> /* For struct timespec */
#include <fixedmath.h> /* For b32_t definitions */
#include <nuttx/clock.h> /* For NSEC_PER_SEC */
#include "dwt.h" /* For DWT_CYCCNT */
#include "up_arch.h" /* For getreg32() */
#include <arch/board/board.h> /* For BOARD_CPU_FREQUENCY */
uint32_t up_critmon_gettime(void)
{
return getreg32(DWT_CYCCNT);
}
void up_critmon_convert(uint32_t elapsed, FAR struct timespec *ts)
{
b32_t b32elapsed;
b32elapsed = itob32(elapsed) / BOARD_CPU_FREQUENCY;
ts->tv_sec = b32toi(b32elapsed);
ts->tv_nsec = NSEC_PER_SEC * b32frac(b32elapsed) / b32ONE;
}
|
where BOARD_CPU_FREQUENCY is, of course, the board CPU frequency, the rate that drives the DWT_CYCNT counter. It may have different names on different platforms. For STM32 it is called STM32_SYSCLK_FREQUENCY.
Before it can be used the DWT CYCCNT counter must be started. It is normally started automatically if you have a debugger connected. Otherwise, you must explicitly start the counter with logic like the following in your lowest level board initialization:
#ifdef CONFIG_SCHED_CRITMONITOR
putreg32(0xc5acce55, ITM_LAR);
modifyreg32(DWT_CTRL, 0, DWT_CTRL_CYCCNTENA_MASK);
#endif
|
In NuttX critical sections are controlled on a per-task basis. For example, consider the following code sequence:
irqstate_t flags = enter_critical_section();
sleep(5);
leave_critical_section(flags);
|
The task, say Task A, establishes the critical section with enter_critical_section();, But when Task A is suspended by the sleep(5); statement, it relinquishes the critical section. The state of the system will then be determined by the next task to be resumed, say Task B: Typically, the next task will not be in a critical section and so the critical section is broken while the task sleeps. That critical section will be re-established when that Task A runs again after the sleep time expires.
However, if Task B that is resumed is also within a critical section, then the critical section will be extended even longer! This is why the global time that the critical section in place may be longer than any time that an individual thread holds the critical section.
The OS reports these maximum times via the ProcFS file system, typically mounted at /proc:
/proc/<ID>/critmon pseudo-file reports the per-thread maximum value for thread ID = <ID>. There is one instance of this critmon file for each active task in the system./proc/critmon pseuo-file reports similar information for the global state of the CPU.The form of the output from the /proc/<ID>/critmon file is:
X.XXXXXXXXX,X.XXXXXXXXX
|
Where X.XXXXXXXXX is the time in seconds with nanosecond precision (but not necessarily accuracy, accuracy is dependent on the timing clock source). The first number is the maximum time that the held pre-emption disabled; the second number number is the longest duration that the critical section was held.
This file cat be read from NSH like:
nsh> cat /proc/1/critmon
0.000009610,0.000001165
|
The form of the output from the /proc/critmon file is simlar:
X,X.XXXXXXXXX,X.XXXXXXXXX
|
Where the first X is the CPU number and the following two numbers have the same interpretation as for /proc/<ID>/critmon. In the single CPU case, there will be one line in the pseudo-file with X=0; in the SMP case there will be multiple lines, one for each CPU.
This file can also be read from NSH:
nsh> cat /proc/critmon
0,0.000009902,0.000023590
|
These statistics are cleared each time that the pseudo-file is read so that the reported values are the maximum since the last time that the ProcFS pseudo file was read.
Also available is a application daemon at apps/sysem/critmon. This daemon periodically reads the ProcFS files described above and dumps the output to stdout. This daemon is enabled with:
CONFIG_SYSTEM_CRITMONITOR=y
|
Hooks to start and stop the daemon can be enabled as NSH built-in command. Then the daemon can be started with:
nsh> critmon_start
Csection Monitor: Started: 3
Csection Monitor: Running: 3
nsh>
PRE-EMPTION CSECTION PID DESCRIPTION
MAX DISABLE MAX TIME
0.000100767 0.000005242 --- CPU 0
0.000000292 0.000023590 0 Idle Task
0.000036696 0.000004078 1 init
0.000000000 0.000014562 3 Csection Monitor
...
|
And can be stopped with:
nsh> critmon_stop
Csection Monitor: Stopping: 3
Csection Monitor: Stopped: 3
|
The IRQ Monitor is additional OS instrumentation. A full discusssion of the IRQ Monitor is beyond the scope of this Wiki Page. Suffice it to say:
CONFIG_SCHED_IRQMONITOR=y./proc/irqs.nsh> irqinfo command.From this information we can calculate the worst case response time from interrupt request until a task runs that can process the the interrupt. That worst cast response time, Tresp, is given by:
Where:
NOTES:
Where :
What can you do if the timing data indicates that you cannot meet your deadline? You have these options:
NOTE: There are a few places in the OS were preemption is disabled via sched_lock() in order to establish a critical section. That is an incorrect use of sched_lock(). sched_lock() simply prevents the currently executing task from being suspended. For the case of the single CPU platform, that does effectively create a critical section: Since no other task can run, the locking task does have exclusive access to all resources that are not shared with interrupt level logic.
But in the multi-CPU SMP case that is not true. sched_lock() still keeps the current task running on CPU from being suspended, but it does not support any exclusivity in accesses because there will be other tasks running on other CPUs that may access the same resources.